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