decred.org/dcrdex@v1.0.5/client/asset/btc/spv_wrapper.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 // spvWallet implements a Wallet backed by a built-in btcwallet + Neutrino. 5 // 6 // There are a few challenges presented in using an SPV wallet for DEX. 7 // 1. Finding non-wallet related blockchain data requires possession of the 8 // pubkey script, not just transaction hash and output index 9 // 2. Finding non-wallet related blockchain data can often entail extensive 10 // scanning of compact filters. We can limit these scans with more 11 // information, such as the match time, which would be the earliest a 12 // transaction could be found on-chain. 13 // 3. We don't see a mempool. We're blind to new transactions until they are 14 // mined. This requires special handling by the caller. We've been 15 // anticipating this, so Core and Swapper are permissive of missing acks for 16 // audit requests. 17 18 package btc 19 20 import ( 21 "context" 22 "encoding/hex" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "io/fs" 28 "math" 29 "os" 30 "path/filepath" 31 "sync" 32 "sync/atomic" 33 "time" 34 35 "decred.org/dcrdex/client/asset" 36 "decred.org/dcrdex/dex" 37 dexbtc "decred.org/dcrdex/dex/networks/btc" 38 "github.com/btcsuite/btcd/btcec/v2" 39 "github.com/btcsuite/btcd/btcjson" 40 "github.com/btcsuite/btcd/btcutil" 41 "github.com/btcsuite/btcd/btcutil/gcs" 42 "github.com/btcsuite/btcd/btcutil/psbt" 43 "github.com/btcsuite/btcd/chaincfg" 44 "github.com/btcsuite/btcd/chaincfg/chainhash" 45 "github.com/btcsuite/btcd/wire" 46 "github.com/btcsuite/btcwallet/waddrmgr" 47 "github.com/btcsuite/btcwallet/wallet" 48 "github.com/btcsuite/btcwallet/walletdb" 49 _ "github.com/btcsuite/btcwallet/walletdb/bdb" // bdb init() registers a driver 50 "github.com/btcsuite/btcwallet/wtxmgr" 51 "github.com/lightninglabs/neutrino" 52 "github.com/lightninglabs/neutrino/headerfs" 53 ) 54 55 const ( 56 WalletTransactionNotFound = dex.ErrorKind("wallet transaction not found") 57 SpentStatusUnknown = dex.ErrorKind("spend status not known") 58 // NOTE: possibly unexport the two above error kinds. 59 60 // defaultBroadcastWait is long enough for btcwallet's PublishTransaction 61 // method to record the outgoing transaction and queue it for broadcasting. 62 // This rough duration is necessary since with neutrino as the wallet's 63 // chain service, its chainClient.SendRawTransaction call is blocking for up 64 // to neutrino.Config.BroadcastTimeout while peers either respond to the inv 65 // request with a getdata or time out. However, in virtually all cases, we 66 // just need to know that btcwallet was able to create and store the 67 // transaction record, and pass it to the chain service. 68 defaultBroadcastWait = 2 * time.Second 69 70 maxFutureBlockTime = 2 * time.Hour // see MaxTimeOffsetSeconds in btcd/blockchain/validate.go 71 neutrinoDBName = "neutrino.db" 72 logDirName = "logs" 73 logFileName = "neutrino.log" 74 defaultAcctNum = 0 75 defaultAcctName = "default" 76 ) 77 78 var wAddrMgrBkt = []byte("waddrmgr") 79 80 // BTCWallet is roughly the (btcwallet/wallet.*Wallet) interface, with some 81 // additional required methods added. 82 type BTCWallet interface { 83 PublishTransaction(tx *wire.MsgTx, label string) error 84 CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) 85 ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) 86 FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) 87 ResetLockedOutpoints() 88 LockOutpoint(op wire.OutPoint) 89 UnlockOutpoint(op wire.OutPoint) 90 LockedOutpoints() []btcjson.TransactionInput 91 NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) 92 NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) 93 PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) 94 Unlock(passphrase []byte, lock <-chan time.Time) error 95 Lock() 96 Locked() bool 97 SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, 98 satPerKb btcutil.Amount, coinSelectionStrategy wallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) 99 HaveAddress(a btcutil.Address) (bool, error) 100 WaitForShutdown() 101 ChainSynced() bool // currently unused 102 AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) 103 // The below methods are not implemented by *wallet.Wallet, so must be 104 // implemented by the BTCWallet implementation. 105 WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) 106 SyncedTo() waddrmgr.BlockStamp 107 SignTx(*wire.MsgTx) error 108 BlockNotifications(context.Context) <-chan *BlockNotification 109 RescanAsync() error 110 ForceRescan() 111 Start() (SPVService, error) 112 Stop() 113 Birthday() time.Time 114 Peers() ([]*asset.WalletPeer, error) 115 AddPeer(string) error 116 RemovePeer(string) error 117 GetTransactions(startHeight, endHeight int32, accountName string, cancel <-chan struct{}) (*wallet.GetTransactionsResult, error) 118 ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) 119 TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) 120 } 121 122 // BlockNotification is block hash and height delivered by a BTCWallet when it 123 // is finished processing a block. 124 type BlockNotification struct { 125 Hash chainhash.Hash 126 Height int32 127 } 128 129 // SPVService is satisfied by *neutrino.ChainService, with the exception of the 130 // Peers method, which has a generic interface in place of neutrino.ServerPeer. 131 type SPVService interface { 132 GetBlockHash(int64) (*chainhash.Hash, error) 133 BestBlock() (*headerfs.BlockStamp, error) 134 Peers() []SPVPeer 135 AddPeer(addr string) error 136 GetBlockHeight(hash *chainhash.Hash) (int32, error) 137 GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) 138 GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) 139 GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) 140 Stop() error 141 } 142 143 // SPVPeer is satisfied by *neutrino.ServerPeer, but is generalized to 144 // accommodate underlying implementations other than lightninglabs/neutrino. 145 type SPVPeer interface { 146 StartingHeight() int32 147 LastBlock() int32 148 Addr() string 149 } 150 151 // btcChainService wraps *neutrino.ChainService in order to translate the 152 // neutrino.ServerPeer to the SPVPeer interface type. 153 type btcChainService struct { 154 *neutrino.ChainService 155 } 156 157 func (s *btcChainService) Peers() []SPVPeer { 158 rawPeers := s.ChainService.Peers() 159 peers := make([]SPVPeer, 0, len(rawPeers)) 160 for _, p := range rawPeers { 161 peers = append(peers, p) 162 } 163 return peers 164 } 165 166 func (s *btcChainService) AddPeer(addr string) error { 167 return s.ChainService.ConnectNode(addr, true) 168 } 169 170 func (s *btcChainService) RemovePeer(addr string) error { 171 return s.ChainService.RemoveNodeByAddr(addr) 172 } 173 174 var _ SPVService = (*btcChainService)(nil) 175 176 // BTCWalletConstructor is a function to construct a BTCWallet. 177 type BTCWalletConstructor func(dir string, cfg *WalletConfig, chainParams *chaincfg.Params, log dex.Logger) BTCWallet 178 179 func extendAddresses(extIdx, intIdx uint32, btcw *wallet.Wallet) error { 180 scopedKeyManager, err := btcw.Manager.FetchScopedKeyManager(waddrmgr.KeyScopeBIP0084) 181 if err != nil { 182 return err 183 } 184 185 return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error { 186 ns := dbtx.ReadWriteBucket(wAddrMgrBkt) 187 if extIdx > 0 { 188 if err := scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx); err != nil { 189 return err 190 } 191 } 192 if intIdx > 0 { 193 return scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx) 194 } 195 return nil 196 }) 197 } 198 199 // spvWallet is an in-process btcwallet.Wallet + neutrino light-filter-based 200 // Bitcoin wallet. spvWallet controls an instance of btcwallet.Wallet directly 201 // and does not run or connect to the RPC server. 202 type spvWallet struct { 203 chainParams *chaincfg.Params 204 cfg *WalletConfig 205 wallet BTCWallet 206 cl SPVService 207 acctNum uint32 208 acctName string 209 dir string 210 decodeAddr dexbtc.AddressDecoder 211 212 log dex.Logger 213 214 tipChan chan *BlockVector 215 syncTarget int32 216 lastPrenatalHeight int32 217 218 *BlockFiltersScanner 219 } 220 221 var _ Wallet = (*spvWallet)(nil) 222 var _ tipNotifier = (*spvWallet)(nil) 223 224 // Reconfigure attempts to reconfigure the rpcClient for the new settings. Live 225 // reconfiguration is only attempted if the new wallet type is walletTypeSPV. 226 func (w *spvWallet) Reconfigure(cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) { 227 // If the wallet type is not SPV, then we can't reconfigure the wallet. 228 if cfg.Type != walletTypeSPV { 229 restartRequired = true 230 return 231 } 232 233 // Check if the SPV wallet exists. If it doesn't, then we can't reconfigure it. 234 exists, err := walletExists(w.dir, w.chainParams) 235 if err != nil { 236 return false, err 237 } 238 if !exists { 239 return false, errors.New("wallet not found") 240 } 241 242 return false, nil 243 } 244 245 // tipFeed satisfies the tipNotifier interface, signaling that *spvWallet 246 // will take precedence in sending block notifications. 247 func (w *spvWallet) tipFeed() <-chan *BlockVector { 248 return w.tipChan 249 } 250 251 func (w *spvWallet) RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) { 252 // Not needed for spv wallet. 253 return nil, errors.New("RawRequest not available on spv") 254 } 255 256 func (w *spvWallet) OwnsAddress(addr btcutil.Address) (bool, error) { 257 return w.wallet.HaveAddress(addr) 258 } 259 260 func (w *spvWallet) SendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { 261 // Publish the transaction in a goroutine so the caller may wait for a given 262 // period before it goes asynchronous and it is assumed that btcwallet at 263 // least succeeded with its DB updates and queueing of the transaction for 264 // rebroadcasting. In the future, a new btcwallet method should be added 265 // that returns after performing its internal actions, but broadcasting 266 // asynchronously and sending the outcome in a channel or promise. 267 res := make(chan error, 1) 268 go func() { 269 tStart := time.Now() 270 defer close(res) 271 if err := w.wallet.PublishTransaction(tx, ""); err != nil { 272 w.log.Errorf("PublishTransaction(%v) failure: %v", tx.TxHash(), err) 273 res <- err 274 return 275 } 276 defer func() { 277 w.log.Tracef("PublishTransaction(%v) completed in %v", tx.TxHash(), time.Since(tStart)) 278 }() // after outpoint unlocking and signalling 279 res <- nil 280 }() 281 282 select { 283 case err := <-res: 284 if err != nil { 285 return nil, err 286 } 287 case <-time.After(defaultBroadcastWait): 288 w.log.Debugf("No error from PublishTransaction after %v for txn %v. "+ 289 "Assuming wallet accepted it.", defaultBroadcastWait, tx.TxHash()) 290 } 291 292 // bitcoind would unlock these, btcwallet does not. Although it seems like 293 // they are no longer returned from ListUnspent after publishing, it must 294 // not be returned by LockedOutpoints (listlockunspent) for the lockedSats 295 // computations to be correct. 296 for _, txIn := range tx.TxIn { 297 w.wallet.UnlockOutpoint(txIn.PreviousOutPoint) 298 } 299 300 txHash := tx.TxHash() // down here in case... the msgTx was mutated? 301 return &txHash, nil 302 } 303 304 func (w *spvWallet) GetBlockHeaderVerbose(blockHash *chainhash.Hash) (*wire.BlockHeader, error) { 305 return w.cl.GetBlockHeader(blockHash) 306 } 307 308 func (w *spvWallet) GetBlock(blockHash chainhash.Hash) (*wire.MsgBlock, error) { 309 block, err := w.cl.GetBlock(blockHash) 310 if err != nil { 311 return nil, fmt.Errorf("neutrino GetBlock error: %v", err) 312 } 313 314 return block.MsgBlock(), nil 315 } 316 317 func (w *spvWallet) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { 318 return w.cl.GetBlockHash(blockHeight) 319 } 320 321 func (w *spvWallet) GetBlockHeight(h *chainhash.Hash) (int32, error) { 322 return w.cl.GetBlockHeight(h) 323 } 324 325 func (w *spvWallet) GetBestBlockHash() (*chainhash.Hash, error) { 326 blk := w.wallet.SyncedTo() 327 return &blk.Hash, nil 328 } 329 330 // GetBestBlockHeight returns the height of the best block processed by the 331 // wallet, which indicates the height at which the compact filters have been 332 // retrieved and scanned for wallet addresses. This is may be less than 333 // getChainHeight, which indicates the height that the chain service has reached 334 // in its retrieval of block headers and compact filter headers. 335 func (w *spvWallet) GetBestBlockHeight() (int32, error) { 336 return w.wallet.SyncedTo().Height, nil 337 } 338 339 // getChainStamp satisfies chainStamper for manual median time calculations. 340 func (w *spvWallet) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { 341 hdr, err := w.cl.GetBlockHeader(blockHash) 342 if err != nil { 343 return 344 } 345 return hdr.Timestamp, &hdr.PrevBlock, nil 346 } 347 348 // MedianTime is the median time for the current best block. 349 func (w *spvWallet) MedianTime() (time.Time, error) { 350 blk := w.wallet.SyncedTo() 351 return CalcMedianTime(w.getChainStamp, &blk.Hash) 352 } 353 354 // GetChainHeight is only for confirmations since it does not reflect the wallet 355 // manager's sync height, just the chain service. 356 func (w *spvWallet) GetChainHeight() (int32, error) { 357 blk, err := w.cl.BestBlock() 358 if err != nil { 359 return -1, err 360 } 361 return blk.Height, err 362 } 363 364 func (w *spvWallet) PeerCount() (uint32, error) { 365 return uint32(len(w.cl.Peers())), nil 366 } 367 368 func (w *spvWallet) peers() ([]*asset.WalletPeer, error) { 369 return w.wallet.Peers() 370 } 371 372 func (w *spvWallet) addPeer(addr string) error { 373 return w.wallet.AddPeer(addr) 374 } 375 376 func (w *spvWallet) removePeer(addr string) error { 377 return w.wallet.RemovePeer(addr) 378 } 379 380 // syncHeight is the best known sync height among peers. 381 func (w *spvWallet) syncHeight() int32 { 382 var maxHeight int32 383 for _, p := range w.cl.Peers() { 384 tipHeight := p.StartingHeight() 385 lastBlockHeight := p.LastBlock() 386 if lastBlockHeight > tipHeight { 387 tipHeight = lastBlockHeight 388 } 389 if tipHeight > maxHeight { 390 maxHeight = tipHeight 391 } 392 } 393 return maxHeight 394 } 395 396 // SyncStatus is information about the wallet's sync status. 397 // 398 // The neutrino wallet has a two stage sync: 399 // 1. chain service fetching block headers and filter headers 400 // 2. wallet address manager retrieving and scanning filters 401 // 402 // We only report a single sync height, so we are going to show some progress in 403 // the chain service sync stage that comes before the wallet has performed any 404 // address recovery/rescan, and switch to the wallet's sync height when it 405 // reports non-zero height. 406 func (w *spvWallet) SyncStatus() (*asset.SyncStatus, error) { 407 // Chain service headers (block and filter) height. 408 chainBlk, err := w.cl.BestBlock() 409 if err != nil { 410 return nil, err 411 } 412 413 currentHeight := chainBlk.Height 414 415 var target int32 416 if len(w.cl.Peers()) > 0 { 417 target = w.syncHeight() 418 } else { // use cached value if available 419 target = atomic.LoadInt32(&w.syncTarget) 420 } 421 422 if target == 0 { 423 return new(asset.SyncStatus), nil 424 } 425 426 var synced bool 427 var blk *BlockVector 428 // Wallet address manager sync height. 429 if chainBlk.Timestamp.After(w.wallet.Birthday()) { 430 // After the wallet's birthday, the wallet address manager should begin 431 // syncing. Although block time stamps are not necessarily monotonically 432 // increasing, this is a reasonable condition at which the wallet's sync 433 // height should be consulted instead of the chain service's height. 434 walletBlock := w.wallet.SyncedTo() 435 if walletBlock.Height == 0 { 436 // The wallet is about to start its sync, so just return the last 437 // chain service height prior to wallet birthday until it begins. 438 h := atomic.LoadInt32(&w.lastPrenatalHeight) 439 return &asset.SyncStatus{ 440 Synced: false, 441 TargetHeight: uint64(target), 442 Blocks: uint64(h), 443 }, nil 444 } 445 blk = &BlockVector{ 446 Height: int64(walletBlock.Height), 447 Hash: walletBlock.Hash, 448 } 449 currentHeight = walletBlock.Height 450 synced = currentHeight >= target // maybe && w.wallet.ChainSynced() 451 } else { 452 // Chain service still syncing. 453 blk = &BlockVector{ 454 Height: int64(currentHeight), 455 Hash: chainBlk.Hash, 456 } 457 atomic.StoreInt32(&w.lastPrenatalHeight, currentHeight) 458 } 459 460 if target > 0 && atomic.SwapInt32(&w.syncTarget, target) == 0 { 461 w.tipChan <- blk 462 } 463 464 return &asset.SyncStatus{ 465 Synced: synced, 466 TargetHeight: uint64(target), 467 Blocks: uint64(blk.Height), 468 }, nil 469 } 470 471 // ownsInputs determines if we own the inputs of the tx. 472 func (w *spvWallet) ownsInputs(txid string) bool { 473 txHash, err := chainhash.NewHashFromStr(txid) 474 if err != nil { 475 w.log.Warnf("Error decoding txid %q: %v", txid, err) 476 return false 477 } 478 txDetails, err := w.wallet.WalletTransaction(txHash) 479 if err != nil { 480 w.log.Warnf("walletTransaction(%v) error: %v", txid, err) 481 return false 482 } 483 484 for _, txIn := range txDetails.MsgTx.TxIn { 485 _, _, _, _, err = w.wallet.FetchInputInfo(&txIn.PreviousOutPoint) 486 if err != nil { 487 if !errors.Is(err, wallet.ErrNotMine) { 488 w.log.Warnf("FetchInputInfo error: %v", err) 489 } 490 return false 491 } 492 } 493 return true 494 } 495 496 func (w *spvWallet) ListTransactionsSinceBlock(blockHeight int32) ([]*ListTransactionsResult, error) { 497 tip, err := w.cl.BestBlock() 498 if err != nil { 499 return nil, fmt.Errorf("BestBlock error: %v", err) 500 } 501 502 // We use GetTransactions instead of ListSinceBlock, because ListSinceBlock 503 // does not include transactions that pay to a change address, which 504 // Redeem, Refund, and RedeemBond do. 505 res, err := w.wallet.GetTransactions(blockHeight, tip.Height, w.acctName, nil) 506 if err != nil { 507 return nil, err 508 } 509 510 txs := make([]*ListTransactionsResult, 0, len(res.MinedTransactions)+len(res.UnminedTransactions)) 511 512 toLTR := func(tx *wallet.TransactionSummary, blockHeight uint32, blockTime uint64) *ListTransactionsResult { 513 fee := tx.Fee.ToBTC() 514 return &ListTransactionsResult{ 515 TxID: tx.Hash.String(), 516 BlockHeight: blockHeight, 517 BlockTime: blockTime, 518 Fee: &fee, 519 Send: len(tx.MyInputs) > 0, 520 } 521 } 522 523 for _, block := range res.MinedTransactions { 524 for _, tx := range block.Transactions { 525 txs = append(txs, toLTR(&tx, uint32(block.Height), uint64(block.Timestamp))) 526 } 527 } 528 529 for _, tx := range res.UnminedTransactions { 530 txs = append(txs, toLTR(&tx, 0, 0)) 531 } 532 533 return txs, nil 534 } 535 536 // balances retrieves a wallet's balance details. 537 func (w *spvWallet) Balances() (*GetBalancesResult, error) { 538 // Determine trusted vs untrusted coins with listunspent. 539 unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) 540 if err != nil { 541 return nil, fmt.Errorf("error listing unspent outputs: %w", err) 542 } 543 var trusted, untrusted btcutil.Amount 544 for _, txout := range unspents { 545 if txout.Confirmations > 0 || w.ownsInputs(txout.TxID) { 546 trusted += btcutil.Amount(toSatoshi(txout.Amount)) 547 continue 548 } 549 untrusted += btcutil.Amount(toSatoshi(txout.Amount)) 550 } 551 552 // listunspent does not include immature coinbase outputs or locked outputs. 553 bals, err := w.wallet.CalculateAccountBalances(w.acctNum, 0 /* confs */) 554 if err != nil { 555 return nil, err 556 } 557 w.log.Tracef("Bals: spendable = %v (%v trusted, %v untrusted, %v assumed locked), immature = %v", 558 bals.Spendable, trusted, untrusted, bals.Spendable-trusted-untrusted, bals.ImmatureReward) 559 // Locked outputs would be in wallet.Balances.Spendable. Assume they would 560 // be considered trusted and add them back in. 561 if all := trusted + untrusted; bals.Spendable > all { 562 trusted += bals.Spendable - all 563 } 564 565 return &GetBalancesResult{ 566 Mine: Balances{ 567 Trusted: trusted.ToBTC(), 568 Untrusted: untrusted.ToBTC(), 569 Immature: bals.ImmatureReward.ToBTC(), 570 }, 571 }, nil 572 } 573 574 // ListUnspent retrieves list of the wallet's UTXOs. 575 func (w *spvWallet) ListUnspent() ([]*ListUnspentResult, error) { 576 unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName) 577 if err != nil { 578 return nil, err 579 } 580 res := make([]*ListUnspentResult, 0, len(unspents)) 581 for _, utxo := range unspents { 582 // If the utxo is unconfirmed, we should determine whether it's "safe" 583 // by seeing if we control the inputs of its transaction. 584 safe := utxo.Confirmations > 0 || w.ownsInputs(utxo.TxID) 585 586 // These hex decodings are unlikely to fail because they come directly 587 // from the listunspent result. Regardless, they should not result in an 588 // error for the caller as we can return the valid utxos. 589 pkScript, err := hex.DecodeString(utxo.ScriptPubKey) 590 if err != nil { 591 w.log.Warnf("ScriptPubKey decode failure: %v", err) 592 continue 593 } 594 595 redeemScript, err := hex.DecodeString(utxo.RedeemScript) 596 if err != nil { 597 w.log.Warnf("ScriptPubKey decode failure: %v", err) 598 continue 599 } 600 601 res = append(res, &ListUnspentResult{ 602 TxID: utxo.TxID, 603 Vout: utxo.Vout, 604 Address: utxo.Address, 605 // Label: , 606 ScriptPubKey: pkScript, 607 Amount: utxo.Amount, 608 Confirmations: uint32(utxo.Confirmations), 609 RedeemScript: redeemScript, 610 Spendable: utxo.Spendable, 611 // Solvable: , 612 SafePtr: &safe, 613 }) 614 } 615 return res, nil 616 } 617 618 // LockUnspent locks and unlocks outputs for spending. An output that is part of 619 // an order, but not yet spent, should be locked until spent or until the order 620 // is canceled or fails. 621 func (w *spvWallet) LockUnspent(unlock bool, ops []*Output) error { 622 switch { 623 case unlock && len(ops) == 0: 624 w.wallet.ResetLockedOutpoints() 625 default: 626 for _, op := range ops { 627 op := wire.OutPoint{Hash: op.Pt.TxHash, Index: op.Pt.Vout} 628 if unlock { 629 w.wallet.UnlockOutpoint(op) 630 } else { 631 w.wallet.LockOutpoint(op) 632 } 633 } 634 } 635 return nil 636 } 637 638 // ListLockUnspent returns a slice of outpoints for all unspent outputs marked 639 // as locked by a wallet. 640 func (w *spvWallet) ListLockUnspent() ([]*RPCOutpoint, error) { 641 outpoints := w.wallet.LockedOutpoints() 642 pts := make([]*RPCOutpoint, 0, len(outpoints)) 643 for _, pt := range outpoints { 644 pts = append(pts, &RPCOutpoint{ 645 TxID: pt.Txid, 646 Vout: pt.Vout, 647 }) 648 } 649 return pts, nil 650 } 651 652 // ChangeAddress gets a new internal address from the wallet. The address will 653 // be bech32-encoded (P2WPKH). 654 func (w *spvWallet) ChangeAddress() (btcutil.Address, error) { 655 return w.wallet.NewChangeAddress(w.acctNum, waddrmgr.KeyScopeBIP0084) 656 } 657 658 // ExternalAddress gets a new bech32-encoded (P2WPKH) external address from the 659 // wallet. 660 func (w *spvWallet) ExternalAddress() (btcutil.Address, error) { 661 return w.wallet.NewAddress(w.acctNum, waddrmgr.KeyScopeBIP0084) 662 } 663 664 // signTx attempts to have the wallet sign the transaction inputs. 665 func (w *spvWallet) SignTx(tx *wire.MsgTx) (*wire.MsgTx, error) { 666 // Can't use btcwallet.Wallet.SignTransaction, because it doesn't work for 667 // segwit transactions (for real?). 668 return tx, w.wallet.SignTx(tx) 669 } 670 671 // PrivKeyForAddress retrieves the private key associated with the specified 672 // address. 673 func (w *spvWallet) PrivKeyForAddress(addr string) (*btcec.PrivateKey, error) { 674 a, err := w.decodeAddr(addr, w.chainParams) 675 if err != nil { 676 return nil, err 677 } 678 return w.wallet.PrivKeyForAddress(a) 679 } 680 681 // Unlock unlocks the wallet. 682 func (w *spvWallet) Unlock(pw []byte) error { 683 return w.wallet.Unlock(pw, nil) 684 } 685 686 // Lock locks the wallet. 687 func (w *spvWallet) Lock() error { 688 w.wallet.Lock() 689 return nil 690 } 691 692 // EstimateSendTxFee callers should provide at least one output value. 693 func (w *spvWallet) EstimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) { 694 minTxSize := uint64(tx.SerializeSize()) 695 var sendAmount uint64 696 for _, txOut := range tx.TxOut { 697 sendAmount += uint64(txOut.Value) 698 } 699 700 unspents, err := w.ListUnspent() 701 if err != nil { 702 return 0, fmt.Errorf("error listing unspent outputs: %w", err) 703 } 704 705 utxos, _, _, err := ConvertUnspent(0, unspents, w.chainParams) 706 if err != nil { 707 return 0, fmt.Errorf("error converting unspent outputs: %w", err) 708 } 709 710 enough := SendEnough(sendAmount, feeRate, subtract, minTxSize, true, false) 711 sum, _, inputsSize, _, _, _, _, err := TryFund(utxos, enough) 712 if err != nil { 713 return 0, err 714 } 715 716 txSize := minTxSize + inputsSize 717 estFee := feeRate * txSize 718 remaining := sum - sendAmount 719 720 // Check if there will be a change output if there is enough remaining. 721 estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate 722 var changeValue uint64 723 if remaining > estFeeWithChange { 724 changeValue = remaining - estFeeWithChange 725 } 726 727 if subtract { 728 // fees are already included in sendAmount, anything else is change. 729 changeValue = remaining 730 } 731 732 var finalFee uint64 733 if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) { 734 // remaining cannot cover a non-dust change and the fee for the change. 735 finalFee = estFee + remaining 736 } else { 737 // additional fee will be paid for non-dust change 738 finalFee = estFeeWithChange 739 } 740 741 if subtract { 742 sendAmount -= finalFee 743 } 744 if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) { 745 return 0, errors.New("output value is dust") 746 } 747 748 return finalFee, nil 749 } 750 751 // SwapConfirmations attempts to get the number of confirmations and the spend 752 // status for the specified tx output. For swap outputs that were not generated 753 // by this wallet, startTime must be supplied to limit the search. Use the match 754 // time assigned by the server. 755 func (w *spvWallet) SwapConfirmations(txHash *chainhash.Hash, vout uint32, pkScript []byte, 756 startTime time.Time) (confs uint32, spent bool, err error) { 757 758 // First, check if it's a wallet transaction. We probably won't be able 759 // to see the spend status, since the wallet doesn't track the swap contract 760 // output, but we can get the block if it's been mined. 761 blockHash, confs, spent, err := w.confirmations(txHash, vout) 762 if err == nil { 763 return confs, spent, nil 764 } 765 var assumedMempool bool 766 switch err { 767 case WalletTransactionNotFound: 768 w.log.Tracef("swapConfirmations - WalletTransactionNotFound: %v:%d", txHash, vout) 769 case SpentStatusUnknown: 770 w.log.Tracef("swapConfirmations - SpentStatusUnknown: %v:%d (block %v, confs %d)", 771 txHash, vout, blockHash, confs) 772 if blockHash == nil { 773 // We generated this swap, but it probably hasn't been mined yet. 774 // It's SpentStatusUnknown because the wallet doesn't track the 775 // spend status of the swap contract output itself, since it's not 776 // recognized as a wallet output. We'll still try to find the 777 // confirmations with other means, but if we can't find it, we'll 778 // report it as a zero-conf unspent output. This ignores the remote 779 // possibility that the output could be both in mempool and spent. 780 assumedMempool = true 781 } 782 default: 783 return 0, false, err 784 } 785 786 // If we still don't have the block hash, we may have it stored. Check the 787 // dex database first. This won't give us the confirmations and spent 788 // status, but it will allow us to short circuit a longer scan if we already 789 // know the output is spent. 790 if blockHash == nil { 791 blockHash, _ = w.MainchainBlockForStoredTx(txHash) 792 } 793 794 // Our last option is neutrino. 795 w.log.Tracef("swapConfirmations - scanFilters: %v:%d (block %v, start time %v)", 796 txHash, vout, blockHash, startTime) 797 walletBlock := w.wallet.SyncedTo() // where cfilters are received and processed 798 walletTip := walletBlock.Height 799 utxo, err := w.ScanFilters(txHash, vout, pkScript, walletTip, startTime, blockHash) 800 if err != nil { 801 return 0, false, err 802 } 803 804 if utxo.Spend == nil && utxo.BlockHash == nil { 805 if assumedMempool { 806 w.log.Tracef("swapConfirmations - scanFilters did not find %v:%d, assuming in mempool.", 807 txHash, vout) 808 // NOT asset.CoinNotFoundError since this is normal for mempool 809 // transactions with an SPV wallet. 810 return 0, false, nil 811 } 812 return 0, false, fmt.Errorf("output %s:%v not found with search parameters startTime = %s, pkScript = %x", 813 txHash, vout, startTime, pkScript) 814 } 815 816 if utxo.BlockHash != nil { 817 bestHeight, err := w.GetChainHeight() 818 if err != nil { 819 return 0, false, fmt.Errorf("getBestBlockHeight error: %v", err) 820 } 821 confs = uint32(bestHeight) - utxo.BlockHeight + 1 822 } 823 824 if utxo.Spend != nil { 825 // In the off-chance that a spend was found but not the output itself, 826 // confs will be incorrect here. 827 // In situations where we're looking for the counter-party's swap, we 828 // revoke if it's found to be spent, without inspecting the confs, so 829 // accuracy of confs is not significant. When it's our output, we'll 830 // know the block and won't end up here. (even if we did, we just end up 831 // sending out some inaccurate Data-severity notifications to the UI 832 // until the match progresses) 833 return confs, true, nil 834 } 835 836 // unspent 837 return confs, false, nil 838 } 839 840 func (w *spvWallet) Locked() bool { 841 return w.wallet.Locked() 842 } 843 844 func (w *spvWallet) WalletLock() error { 845 w.wallet.Lock() 846 return nil 847 } 848 849 func (w *spvWallet) WalletUnlock(pw []byte) error { 850 return w.Unlock(pw) 851 } 852 853 func (w *spvWallet) getBlockHeaderVerbose(blockHash *chainhash.Hash) (*wire.BlockHeader, error) { 854 return w.cl.GetBlockHeader(blockHash) 855 } 856 857 // returns a bool value to indicate whether this block is a part of main chain. 858 // For orphaned blocks header.Confirmations is negative. 859 func (w *spvWallet) GetBlockHeader(blockHash *chainhash.Hash) (header *BlockHeader, mainchain bool, err error) { 860 hdr, err := w.cl.GetBlockHeader(blockHash) 861 if err != nil { 862 return nil, false, err 863 } 864 865 tip, err := w.cl.BestBlock() 866 if err != nil { 867 return nil, false, fmt.Errorf("BestBlock error: %v", err) 868 } 869 870 blockHeight, err := w.cl.GetBlockHeight(blockHash) 871 if err != nil { 872 return nil, false, err 873 } 874 875 confirmations := int64(-1) 876 mainchain = w.BlockIsMainchain(blockHash, blockHeight) 877 if mainchain { 878 confirmations = int64(confirms(blockHeight, tip.Height)) 879 } 880 881 return &BlockHeader{ 882 Hash: hdr.BlockHash().String(), 883 Confirmations: confirmations, 884 Height: int64(blockHeight), 885 Time: hdr.Timestamp.Unix(), 886 PreviousBlockHash: hdr.PrevBlock.String(), 887 }, mainchain, nil 888 } 889 890 func (w *spvWallet) GetBestBlockHeader() (*BlockHeader, error) { 891 hash, err := w.GetBestBlockHash() 892 if err != nil { 893 return nil, err 894 } 895 hdr, _, err := w.GetBlockHeader(hash) 896 return hdr, err 897 } 898 899 func (w *spvWallet) logFilePath() string { 900 return filepath.Join(w.dir, logDirName, logFileName) 901 } 902 903 // Connect will start the wallet and begin syncing. 904 func (w *spvWallet) Connect(ctx context.Context, wg *sync.WaitGroup) (err error) { 905 w.cl, err = w.wallet.Start() 906 if err != nil { 907 return err 908 } 909 910 blockNotes := w.wallet.BlockNotifications(ctx) 911 912 // Nanny for the caches checkpoints and txBlocks caches. 913 wg.Add(1) 914 go func() { 915 defer wg.Done() 916 defer w.wallet.Stop() 917 918 ticker := time.NewTicker(time.Minute * 20) 919 defer ticker.Stop() 920 expiration := time.Hour * 2 921 for { 922 select { 923 case <-ticker.C: 924 w.BlockFiltersScanner.CleanCaches(expiration) 925 926 case blk := <-blockNotes: 927 syncTarget := atomic.LoadInt32(&w.syncTarget) 928 if syncTarget == 0 || (blk.Height < syncTarget && blk.Height%10_000 != 0) { 929 continue 930 } 931 932 select { 933 case w.tipChan <- &BlockVector{ 934 Hash: blk.Hash, 935 Height: int64(blk.Height), 936 }: 937 default: 938 w.log.Warnf("tip report channel was blocking") 939 } 940 941 case <-ctx.Done(): 942 return 943 } 944 } 945 }() 946 947 return nil 948 } 949 950 // moveWalletData will move all wallet files to a backup directory, but leaving 951 // the logs folder. 952 func (w *spvWallet) moveWalletData(backupDir string) error { 953 timeString := time.Now().Format("2006-01-02T15_04_05") 954 backupFolder := filepath.Join(backupDir, w.chainParams.Name, timeString) 955 err := os.MkdirAll(backupFolder, 0744) 956 if err != nil { 957 return err 958 } 959 960 // Copy wallet logs folder since we do not move it. 961 backupLogDir := filepath.Join(backupFolder, logDirName) 962 walletLogDir := filepath.Join(w.dir, logDirName) 963 if err := copyDir(walletLogDir, backupLogDir); err != nil { 964 return err 965 } 966 967 // Move contents of the wallet dir, except the logs folder. 968 return filepath.WalkDir(w.dir, func(path string, d fs.DirEntry, err error) error { 969 if err != nil { 970 return err 971 } 972 if path == w.dir { // top 973 return nil 974 } 975 if d.IsDir() && d.Name() == logDirName { 976 return filepath.SkipDir 977 } 978 rel, err := filepath.Rel(w.dir, path) 979 if err != nil { 980 return err 981 } 982 err = os.Rename(path, filepath.Join(backupFolder, rel)) 983 if err != nil { 984 return err 985 } 986 if d.IsDir() { // we just moved a folder, including the contents 987 return filepath.SkipDir 988 } 989 return nil 990 }) 991 } 992 993 // copyFile copies a file from src to dst. 994 func copyFile(src, dst string) error { 995 out, err := os.Create(dst) 996 if err != nil { 997 return err 998 } 999 defer out.Close() 1000 1001 in, err := os.Open(src) 1002 if err != nil { 1003 return err 1004 } 1005 defer in.Close() 1006 1007 _, err = io.Copy(out, in) 1008 return err 1009 } 1010 1011 // copyDir recursively copies the directories and files in source directory to 1012 // destination directory without preserving the original file permissions. The 1013 // destination folder must not exist. 1014 func copyDir(src, dst string) error { 1015 entries, err := os.ReadDir(src) 1016 if err != nil { 1017 return err 1018 } 1019 1020 fi, err := os.Stat(dst) 1021 if err != nil { 1022 if !errors.Is(err, os.ErrNotExist) { 1023 return err 1024 } 1025 err = os.MkdirAll(dst, 0744) 1026 if err != nil { 1027 return err 1028 } 1029 } else if !fi.IsDir() { 1030 return fmt.Errorf("%q is not a directory", dst) 1031 } 1032 1033 for _, fd := range entries { 1034 fName := fd.Name() 1035 srcFile := filepath.Join(src, fName) 1036 dstFile := filepath.Join(dst, fName) 1037 if fd.IsDir() { 1038 err = copyDir(srcFile, dstFile) 1039 } else if fd.Type().IsRegular() { 1040 err = copyFile(srcFile, dstFile) 1041 } 1042 if err != nil { 1043 return err 1044 } 1045 } 1046 1047 return nil 1048 } 1049 1050 // numDerivedAddresses returns the number of internal and external addresses 1051 // that the wallet has derived. 1052 func (w *spvWallet) numDerivedAddresses() (internal, external uint32, err error) { 1053 props, err := w.wallet.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum) 1054 if err != nil { 1055 return 0, 0, err 1056 } 1057 1058 return props.InternalKeyCount, props.ExternalKeyCount, nil 1059 } 1060 1061 // Fingerprint returns an identifier for this wallet. It is the hash of the 1062 // compressed serialization of the account pub key. 1063 func (w *spvWallet) Fingerprint() (string, error) { 1064 props, err := w.wallet.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum) 1065 if err != nil { 1066 return "", err 1067 } 1068 1069 if props.AccountPubKey == nil { 1070 return "", fmt.Errorf("no account key available") 1071 } 1072 1073 pk, err := props.AccountPubKey.ECPubKey() 1074 if err != nil { 1075 return "", err 1076 } 1077 1078 return hex.EncodeToString(btcutil.Hash160(pk.SerializeCompressed())), nil 1079 } 1080 1081 // GetTxOut finds an unspent transaction output and its number of confirmations. 1082 // To match the behavior of the RPC method, even if an output is found, if it's 1083 // known to be spent, no *wire.TxOut and no error will be returned. 1084 func (w *spvWallet) GetTxOut(txHash *chainhash.Hash, vout uint32, pkScript []byte, startTime time.Time) (*wire.TxOut, uint32, error) { 1085 // Check for a wallet transaction first 1086 txDetails, err := w.wallet.WalletTransaction(txHash) 1087 var blockHash *chainhash.Hash 1088 if err != nil && !errors.Is(err, WalletTransactionNotFound) { 1089 return nil, 0, fmt.Errorf("walletTransaction error: %w", err) 1090 } 1091 1092 if txDetails != nil { 1093 spent, found := outputSpendStatus(txDetails, vout) 1094 if found { 1095 if spent { 1096 return nil, 0, nil 1097 } 1098 if len(txDetails.MsgTx.TxOut) <= int(vout) { 1099 return nil, 0, fmt.Errorf("wallet transaction %s doesn't have enough outputs for vout %d", txHash, vout) 1100 } 1101 1102 var confs uint32 1103 if txDetails.Block.Height > 0 { 1104 tip, err := w.cl.BestBlock() 1105 if err != nil { 1106 return nil, 0, fmt.Errorf("BestBlock error: %v", err) 1107 } 1108 confs = uint32(confirms(txDetails.Block.Height, tip.Height)) 1109 } 1110 1111 msgTx := &txDetails.MsgTx 1112 if len(msgTx.TxOut) <= int(vout) { 1113 return nil, 0, fmt.Errorf("wallet transaction %s found, but not enough outputs for vout %d", txHash, vout) 1114 } 1115 return msgTx.TxOut[vout], confs, nil 1116 } 1117 if txDetails.Block.Hash != (chainhash.Hash{}) { 1118 blockHash = &txDetails.Block.Hash 1119 } 1120 } 1121 1122 // We don't really know if it's spent, so we'll need to scan. 1123 walletBlock := w.wallet.SyncedTo() // where cfilters are received and processed 1124 walletTip := walletBlock.Height 1125 utxo, err := w.ScanFilters(txHash, vout, pkScript, walletTip, startTime, blockHash) 1126 if err != nil { 1127 return nil, 0, err 1128 } 1129 1130 if utxo == nil || utxo.Spend != nil || utxo.BlockHash == nil { 1131 return nil, 0, nil 1132 } 1133 1134 tip, err := w.cl.BestBlock() 1135 if err != nil { 1136 return nil, 0, fmt.Errorf("BestBlock error: %v", err) 1137 } 1138 1139 confs := uint32(confirms(int32(utxo.BlockHeight), tip.Height)) 1140 1141 return utxo.TxOut, confs, nil 1142 } 1143 1144 // matchPkScript pulls the filter for the block and attempts to match the 1145 // supplied scripts. 1146 func (w *spvWallet) MatchPkScript(blockHash *chainhash.Hash, scripts [][]byte) (bool, error) { 1147 filter, err := w.cl.GetCFilter(*blockHash, wire.GCSFilterRegular) 1148 if err != nil { 1149 return false, fmt.Errorf("GetCFilter error: %w", err) 1150 } 1151 1152 if filter.N() == 0 { 1153 return false, fmt.Errorf("unexpected empty filter for %s", blockHash) 1154 } 1155 1156 var filterKey [gcs.KeySize]byte 1157 copy(filterKey[:], blockHash[:gcs.KeySize]) 1158 1159 matchFound, err := filter.MatchAny(filterKey, scripts) 1160 if err != nil { 1161 return false, fmt.Errorf("MatchAny error: %w", err) 1162 } 1163 return matchFound, nil 1164 } 1165 1166 // SearchBlockForRedemptions attempts to find spending info for the specified 1167 // contracts by searching every input of all txs in the provided block range. 1168 func (w *spvWallet) SearchBlockForRedemptions(ctx context.Context, reqs map[OutPoint]*FindRedemptionReq, 1169 blockHash chainhash.Hash) (discovered map[OutPoint]*FindRedemptionResult) { 1170 1171 // Just match all the scripts together. 1172 scripts := make([][]byte, 0, len(reqs)) 1173 for _, req := range reqs { 1174 scripts = append(scripts, req.pkScript) 1175 } 1176 1177 discovered = make(map[OutPoint]*FindRedemptionResult, len(reqs)) 1178 1179 matchFound, err := w.MatchPkScript(&blockHash, scripts) 1180 if err != nil { 1181 w.log.Errorf("matchPkScript error: %v", err) 1182 return 1183 } 1184 1185 if !matchFound { 1186 return 1187 } 1188 1189 // There is at least one match. Pull the block. 1190 block, err := w.cl.GetBlock(blockHash) 1191 if err != nil { 1192 w.log.Errorf("neutrino GetBlock error: %v", err) 1193 return 1194 } 1195 1196 for _, msgTx := range block.MsgBlock().Transactions { 1197 newlyDiscovered := FindRedemptionsInTxWithHasher(ctx, true, reqs, msgTx, w.chainParams, hashTx) 1198 for outPt, res := range newlyDiscovered { 1199 discovered[outPt] = res 1200 } 1201 } 1202 return 1203 } 1204 1205 // FindRedemptionsInMempool is unsupported for SPV. 1206 func (w *spvWallet) FindRedemptionsInMempool(ctx context.Context, reqs map[OutPoint]*FindRedemptionReq) (discovered map[OutPoint]*FindRedemptionResult) { 1207 return 1208 } 1209 1210 // confirmations looks for the confirmation count and spend status on a 1211 // transaction output that pays to this wallet. 1212 func (w *spvWallet) confirmations(txHash *chainhash.Hash, vout uint32) (blockHash *chainhash.Hash, confs uint32, spent bool, err error) { 1213 details, err := w.wallet.WalletTransaction(txHash) 1214 if err != nil { 1215 return nil, 0, false, err 1216 } 1217 1218 if details.Block.Hash != (chainhash.Hash{}) { 1219 blockHash = &details.Block.Hash 1220 height, err := w.GetChainHeight() 1221 if err != nil { 1222 return nil, 0, false, err 1223 } 1224 confs = uint32(confirms(details.Block.Height, height)) 1225 } 1226 1227 spent, found := outputSpendStatus(details, vout) 1228 if found { 1229 return blockHash, confs, spent, nil 1230 } 1231 1232 return blockHash, confs, false, SpentStatusUnknown 1233 } 1234 1235 // GetWalletTransaction checks the wallet database for the specified 1236 // transaction. Only transactions with output scripts that pay to the wallet or 1237 // transactions that spend wallet outputs are stored in the wallet database. 1238 // This is pretty much copy-paste from btcwallet 'gettransaction' JSON-RPC 1239 // handler. 1240 func (w *spvWallet) GetWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) { 1241 // Option # 1 just copies from UnstableAPI.TxDetails. Duplicating the 1242 // unexported bucket key feels dirty. 1243 // 1244 // var details *wtxmgr.TxDetails 1245 // err := walletdb.View(w.Database(), func(dbtx walletdb.ReadTx) error { 1246 // txKey := []byte("wtxmgr") 1247 // txmgrNs := dbtx.ReadBucket(txKey) 1248 // var err error 1249 // details, err = w.TxStore.TxDetails(txmgrNs, txHash) 1250 // return err 1251 // }) 1252 1253 // Option #2 1254 // This is what the JSON-RPC does (and has since at least May 2018). 1255 details, err := w.wallet.WalletTransaction(txHash) 1256 if err != nil { 1257 if errors.Is(err, WalletTransactionNotFound) { 1258 return nil, asset.CoinNotFoundError // for the asset.Wallet interface 1259 } 1260 return nil, err 1261 } 1262 1263 syncBlock := w.wallet.SyncedTo() 1264 1265 // TODO: The serialized transaction is already in the DB, so reserializing 1266 // might be avoided here. According to btcwallet, details.SerializedTx is 1267 // "optional" (?), but we might check for it. 1268 txRaw, err := serializeMsgTx(&details.MsgTx) 1269 if err != nil { 1270 return nil, err 1271 } 1272 1273 ret := &GetTransactionResult{ 1274 TxID: txHash.String(), 1275 Bytes: txRaw, // 'Hex' field name is a lie, kinda 1276 Time: uint64(details.Received.Unix()), 1277 TimeReceived: uint64(details.Received.Unix()), 1278 } 1279 1280 if details.Block.Height >= 0 { 1281 ret.BlockHash = details.Block.Hash.String() 1282 ret.BlockTime = uint64(details.Block.Time.Unix()) 1283 // ret.BlockHeight = uint64(details.Block.Height) 1284 ret.Confirmations = uint64(confirms(details.Block.Height, syncBlock.Height)) 1285 } 1286 1287 return ret, nil 1288 1289 /* 1290 var debitTotal, creditTotal btcutil.Amount // credits excludes change 1291 for _, deb := range details.Debits { 1292 debitTotal += deb.Amount 1293 } 1294 for _, cred := range details.Credits { 1295 if !cred.Change { 1296 creditTotal += cred.Amount 1297 } 1298 } 1299 1300 // Fee can only be determined if every input is a debit. 1301 if len(details.Debits) == len(details.MsgTx.TxIn) { 1302 var outputTotal btcutil.Amount 1303 for _, output := range details.MsgTx.TxOut { 1304 outputTotal += btcutil.Amount(output.Value) 1305 } 1306 ret.Fee = (debitTotal - outputTotal).ToBTC() 1307 } 1308 1309 ret.Amount = creditTotal.ToBTC() 1310 return ret, nil 1311 */ 1312 } 1313 1314 func (w *spvWallet) AddressUsed(addrStr string) (bool, error) { 1315 addr, err := w.decodeAddr(addrStr, w.chainParams) 1316 if err != nil { 1317 return false, fmt.Errorf("error decoding address: %w", err) 1318 } 1319 1320 const minConfs = 0 1321 amt, err := w.wallet.TotalReceivedForAddr(addr, minConfs) 1322 if err != nil { 1323 return false, fmt.Errorf("error getting address received: %v", err) 1324 } 1325 return amt != 0, nil 1326 } 1327 1328 func confirms(txHeight, curHeight int32) int32 { 1329 switch { 1330 case txHeight == -1, txHeight > curHeight: 1331 return 0 1332 default: 1333 return curHeight - txHeight + 1 1334 } 1335 } 1336 1337 // outputSpendStatus will return the spend status of the output if it's found 1338 // in the TxDetails.Credits. 1339 func outputSpendStatus(details *wtxmgr.TxDetails, vout uint32) (spend, found bool) { 1340 for _, credit := range details.Credits { 1341 if credit.Index == vout { 1342 return credit.Spent, true 1343 } 1344 } 1345 return false, false 1346 }