/* * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ // This file implements the notification handlers for btcd-side notifications. package main import ( "encoding/hex" "fmt" "sync" "time" "github.com/conformal/btcjson" "github.com/conformal/btcscript" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/txstore" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" ) func parseBlock(block *btcws.BlockDetails) (*txstore.Block, int, error) { if block == nil { return nil, btcutil.TxIndexUnknown, nil } blksha, err := btcwire.NewShaHashFromStr(block.Hash) if err != nil { return nil, btcutil.TxIndexUnknown, err } b := &txstore.Block{ Height: block.Height, Hash: *blksha, Time: time.Unix(block.Time, 0), } return b, block.Index, nil } type notificationHandler func(btcjson.Cmd) error var notificationHandlers = map[string]notificationHandler{ btcws.BlockConnectedNtfnMethod: NtfnBlockConnected, btcws.BlockDisconnectedNtfnMethod: NtfnBlockDisconnected, btcws.RecvTxNtfnMethod: NtfnRecvTx, btcws.RedeemingTxNtfnMethod: NtfnRedeemingTx, btcws.RescanProgressNtfnMethod: NtfnRescanProgress, } // NtfnRecvTx handles the btcws.RecvTxNtfn notification. func NtfnRecvTx(n btcjson.Cmd) error { rtx, ok := n.(*btcws.RecvTxNtfn) if !ok { return fmt.Errorf("%v handler: unexpected type", n.Method()) } bs, err := GetCurBlock() if err != nil { return fmt.Errorf("%v handler: cannot get current block: %v", n.Method(), err) } rawTx, err := hex.DecodeString(rtx.HexTx) if err != nil { return fmt.Errorf("%v handler: bad hexstring: %v", n.Method(), err) } tx, err := btcutil.NewTxFromBytes(rawTx) if err != nil { return fmt.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err) } block, txIdx, err := parseBlock(rtx.Block) if err != nil { return fmt.Errorf("%v handler: bad block: %v", n.Method(), err) } tx.SetIndex(txIdx) // For transactions originating from this wallet, the sent tx history should // be recorded before the received history. If wallet created this tx, wait // for the sent history to finish being recorded before continuing. // // TODO(jrick) this is wrong due to tx malleability. Cannot safely use the // txsha as an identifier. req := SendTxHistSyncRequest{ txsha: *tx.Sha(), response: make(chan SendTxHistSyncResponse), } SendTxHistSyncChans.access <- req resp := <-req.response if resp.ok { // Wait until send history has been recorded. <-resp.c SendTxHistSyncChans.remove <- *tx.Sha() } // For every output, find all accounts handling that output address (if any) // and record the received txout. for outIdx, txout := range tx.MsgTx().TxOut { var accounts []*Account // Errors don't matter here. If addrs is nil, the range below // does nothing. _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, activeNet.Params) for _, addr := range addrs { a, err := AcctMgr.AccountByAddress(addr) if err != nil { continue } accounts = append(accounts, a) } for _, a := range accounts { txr, err := a.TxStore.InsertTx(tx, block) if err != nil { return err } cred, err := txr.AddCredit(uint32(outIdx), false) if err != nil { return err } AcctMgr.ds.ScheduleTxStoreWrite(a) // Notify frontends of tx. If the tx is unconfirmed, it is always // notified and the outpoint is marked as notified. If the outpoint // has already been notified and is now in a block, a txmined notifiction // should be sent once to let frontends that all previous send/recvs // for this unconfirmed tx are now confirmed. op := *cred.OutPoint() previouslyNotifiedReq := NotifiedRecvTxRequest{ op: op, response: make(chan NotifiedRecvTxResponse), } NotifiedRecvTxChans.access <- previouslyNotifiedReq if <-previouslyNotifiedReq.response { NotifiedRecvTxChans.remove <- op } else { // Notify frontends of new recv tx and mark as notified. NotifiedRecvTxChans.add <- op ltr, err := cred.ToJSON(a.Name(), bs.Height, a.Wallet.Net()) if err != nil { return err } NotifyNewTxDetails(allClients, a.Name(), ltr) } // Notify frontends of new account balance. confirmed := a.CalculateBalance(1) unconfirmed := a.CalculateBalance(0) - confirmed NotifyWalletBalance(allClients, a.name, confirmed) NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed) } } return nil } // NtfnBlockConnected handles btcd notifications resulting from newly // connected blocks to the main blockchain. // // TODO(jrick): Send block time with notification. This will be used // to mark wallet files with a possibly-better earliest block height, // and will greatly reduce rescan times for wallets created with an // out of sync btcd. func NtfnBlockConnected(n btcjson.Cmd) error { bcn, ok := n.(*btcws.BlockConnectedNtfn) if !ok { return fmt.Errorf("%v handler: unexpected type", n.Method()) } hash, err := btcwire.NewShaHashFromStr(bcn.Hash) if err != nil { return fmt.Errorf("%v handler: invalid hash string", n.Method()) } // Update the blockstamp for the newly-connected block. bs := &wallet.BlockStamp{ Height: bcn.Height, Hash: *hash, } curBlock.Lock() curBlock.BlockStamp = *bs curBlock.Unlock() // btcd notifies btcwallet about transactions first, and then sends // the new block notification. New balance notifications for txs // in blocks are therefore sent here after all tx notifications // have arrived and finished being processed by the handlers. workers := NotifyBalanceRequest{ block: *hash, wg: make(chan *sync.WaitGroup), } NotifyBalanceSyncerChans.access <- workers if wg := <-workers.wg; wg != nil { wg.Wait() NotifyBalanceSyncerChans.remove <- *hash } AcctMgr.BlockNotify(bs) // Pass notification to frontends too. marshaled, err := n.MarshalJSON() // The parsed notification is expected to be marshalable. if err != nil { panic(err) } allClients <- marshaled return nil } // NtfnBlockDisconnected handles btcd notifications resulting from // blocks disconnected from the main chain in the event of a chain // switch and notifies frontends of the new blockchain height. func NtfnBlockDisconnected(n btcjson.Cmd) error { bdn, ok := n.(*btcws.BlockDisconnectedNtfn) if !ok { return fmt.Errorf("%v handler: unexpected type", n.Method()) } hash, err := btcwire.NewShaHashFromStr(bdn.Hash) if err != nil { return fmt.Errorf("%v handler: invalid hash string", n.Method()) } // Rollback Utxo and Tx data stores. if err = AcctMgr.Rollback(bdn.Height, hash); err != nil { return err } // Pass notification to frontends too. marshaled, err := n.MarshalJSON() // A btcws.BlockDisconnectedNtfn is expected to marshal without error. // If it does, it indicates that one of its struct fields is of a // non-marshalable type. if err != nil { panic(err) } allClients <- marshaled return nil } // NtfnRedeemingTx handles btcd redeemingtx notifications resulting from a // transaction spending a watched outpoint. func NtfnRedeemingTx(n btcjson.Cmd) error { cn, ok := n.(*btcws.RedeemingTxNtfn) if !ok { return fmt.Errorf("%v handler: unexpected type", n.Method()) } rawTx, err := hex.DecodeString(cn.HexTx) if err != nil { return fmt.Errorf("%v handler: bad hexstring: %v", n.Method(), err) } tx, err := btcutil.NewTxFromBytes(rawTx) if err != nil { return fmt.Errorf("%v handler: bad transaction bytes: %v", n.Method(), err) } block, txIdx, err := parseBlock(cn.Block) if err != nil { return fmt.Errorf("%v handler: bad block: %v", n.Method(), err) } tx.SetIndex(txIdx) return AcctMgr.RecordSpendingTx(tx, block) } // NtfnRescanProgress handles btcd rescanprogress notifications resulting // from a partially completed rescan. func NtfnRescanProgress(n btcjson.Cmd) error { cn, ok := n.(*btcws.RescanProgressNtfn) if !ok { return fmt.Errorf("%v handler: unexpected type", n.Method()) } // Notify the rescan manager of the completed partial progress for // the current rescan. AcctMgr.rm.MarkProgress(cn.LastProcessed) return nil }