/* * 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. */ package tx import ( "bytes" "container/list" "encoding/binary" "errors" "io" "time" "github.com/conformal/btcjson" "github.com/conformal/btcscript" "github.com/conformal/btcutil" "github.com/conformal/btcwire" ) var ( // ErrInvalidFormat represents an error where the expected // format of serialized data was not matched. ErrInvalidFormat = errors.New("invalid format") // ErrBadLength represents an error when writing a slice // where the length does not match the expected. ErrBadLength = errors.New("bad length") // ErrUnsupportedVersion represents an error where a serialized // object is marked with a version that is no longer supported // during deserialization. ErrUnsupportedVersion = errors.New("version no longer supported") // ErrInconsistantStore represents an error for when an inconsistancy // is detected during inserting or returning transaction records. ErrInconsistantStore = errors.New("inconsistant transaction store") ) // Record is a common interface shared by SignedTx and RecvTxOut transaction // store records. type Record interface { Block() *BlockDetails Height() int32 Time() time.Time Tx() *btcutil.Tx TxSha() *btcwire.ShaHash TxInfo(string, int32, btcwire.BitcoinNet) []btcjson.ListTransactionsResult } type txRecord interface { Block() *BlockDetails Height() int32 Time() time.Time TxSha() *btcwire.ShaHash record(store *Store) Record blockTx() blockTx setBlock(*BlockDetails) readFrom(io.Reader) (int64, error) writeTo(io.Writer) (int64, error) } func sortedInsert(l *list.List, tx txRecord) { for e := l.Back(); e != nil; e = e.Prev() { v := e.Value.(txRecord) if !v.Time().After(tx.Time()) { // equal or before l.InsertAfter(tx, e) return } } // No list elements, or all previous elements come after the date of tx. l.PushFront(tx) } type blockTx struct { txSha btcwire.ShaHash height int32 } func (btx *blockTx) readFrom(r io.Reader) (int64, error) { // Read txsha n, err := io.ReadFull(r, btx.txSha[:]) n64 := int64(n) if err != nil { return n64, err } // Read height heightBytes := make([]byte, 4) n, err = io.ReadFull(r, heightBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } btx.height = int32(binary.LittleEndian.Uint32(heightBytes)) return n64, nil } func (btx *blockTx) writeTo(w io.Writer) (int64, error) { // Write txsha n, err := w.Write(btx.txSha[:]) n64 := int64(n) if err != nil { return n64, err } // Write height heightBytes := make([]byte, 4) binary.LittleEndian.PutUint32(heightBytes, uint32(btx.height)) n, err = w.Write(heightBytes) n64 += int64(n) if err != nil { return n64, err } return n64, nil } type blockOutPoint struct { op btcwire.OutPoint height int32 } // Store implements a transaction store for storing and managing wallet // transactions. type Store struct { txs map[blockTx]*btcutil.Tx // all backing transactions referenced by records sorted *list.List // ordered (by date) list of all wallet tx records signed map[blockTx]*signedTx recv map[blockOutPoint]*recvTxOut unspent map[btcwire.OutPoint]*recvTxOut } // NewStore allocates and initializes a new transaction store. func NewStore() *Store { store := Store{ txs: make(map[blockTx]*btcutil.Tx), sorted: list.New(), signed: make(map[blockTx]*signedTx), recv: make(map[blockOutPoint]*recvTxOut), unspent: make(map[btcwire.OutPoint]*recvTxOut), } return &store } // All Store versions (both old and current). const ( versFirst uint32 = iota // versRecvTxIndex is the version where the txout index // was added to the RecvTx struct. versRecvTxIndex // versMarkSentChange is the version where serialized SentTx // added a flags field, used for marking a sent transaction // as change. versMarkSentChange // versCombined is the version where the old utxo and tx stores // were combined into a single data structure. versCombined // versCurrent is the current tx file version. versCurrent = versCombined ) // Serializing a Store results in writing three basic groups of // data: backing txs (which are needed for the other two groups), // received transaction outputs (both spent and unspent), and // signed (or sent) transactions which spend previous outputs. // These are the byte headers prepending each type. const ( backingTxHeader byte = iota recvTxOutHeader signedTxHeader ) // ReadFrom satisifies the io.ReaderFrom interface by deserializing a // transaction from an io.Reader. func (s *Store) ReadFrom(r io.Reader) (int64, error) { // Read current file version. uint32Bytes := make([]byte, 4) n, err := io.ReadFull(r, uint32Bytes) n64 := int64(n) if err != nil { return n64, err } vers := binary.LittleEndian.Uint32(uint32Bytes) // Reading files with versions before versCombined is unsupported. if vers < versCombined { return n64, ErrUnsupportedVersion } // Reset store. s.txs = make(map[blockTx]*btcutil.Tx) s.sorted = list.New() s.signed = make(map[blockTx]*signedTx) s.recv = make(map[blockOutPoint]*recvTxOut) s.unspent = make(map[btcwire.OutPoint]*recvTxOut) // Read backing transactions and records. for { // Read byte header. If this errors with io.EOF, we're done. header := make([]byte, 1) n, err = io.ReadFull(r, header) n64 += int64(n) if err == io.EOF { return n64, nil } switch header[0] { case backingTxHeader: // Read block height. n, err = io.ReadFull(r, uint32Bytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } height := int32(binary.LittleEndian.Uint32(uint32Bytes)) // Read serialized transaction. tx := new(msgTx) txN, err := tx.readFrom(r) n64 += txN if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } // Add backing tx to store. utx := btcutil.NewTx((*btcwire.MsgTx)(tx)) s.txs[blockTx{*utx.Sha(), height}] = utx case recvTxOutHeader: // Read received transaction output record. rtx := new(recvTxOut) txN, err := rtx.readFrom(r) n64 += txN if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } // It is an error for the backing transaction to have // not already been read. if _, ok := s.txs[rtx.blockTx()]; !ok { return n64, ErrInconsistantStore } // Add entries to store. s.sorted.PushBack(rtx) k := blockOutPoint{rtx.outpoint, rtx.Height()} s.recv[k] = rtx if !rtx.Spent() { s.unspent[rtx.outpoint] = rtx } case signedTxHeader: // Read signed (sent) transaction record. stx := new(signedTx) txN, err := stx.readFrom(r) n64 += txN if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } // It is an error for the backing transaction to have // not already been read. if _, ok := s.txs[stx.blockTx()]; !ok { return n64, ErrInconsistantStore } // Add entries to store. s.sorted.PushBack(stx) s.signed[stx.blockTx()] = stx default: return n64, errors.New("bad magic byte") } } return n64, nil } // WriteTo satisifies the io.WriterTo interface by serializing a transaction // store to an io.Writer. func (s *Store) WriteTo(w io.Writer) (int64, error) { // Write current file version. uint32Bytes := make([]byte, 4) binary.LittleEndian.PutUint32(uint32Bytes, versCurrent) n, err := w.Write(uint32Bytes) n64 := int64(n) if err != nil { return n64, err } // Write all backing transactions. for btx, tx := range s.txs { // Write backing tx header. n, err = w.Write([]byte{backingTxHeader}) n64 += int64(n) if err != nil { return n64, err } // Write block height. binary.LittleEndian.PutUint32(uint32Bytes, uint32(btx.height)) n, err = w.Write(uint32Bytes) n64 += int64(n) if err != nil { return n64, err } // Write serialized transaction txN, err := (*msgTx)(tx.MsgTx()).writeTo(w) n64 += txN if err != nil { return n64, err } } // Write each record. The byte header is dependant on the // underlying type. for e := s.sorted.Front(); e != nil; e = e.Next() { v := e.Value.(txRecord) switch v.(type) { case *recvTxOut: n, err = w.Write([]byte{recvTxOutHeader}) case *signedTx: n, err = w.Write([]byte{signedTxHeader}) } n64 += int64(n) if err != nil { return n64, err } recordN, err := v.writeTo(w) n64 += recordN if err != nil { return n64, err } } return n64, nil } // InsertSignedTx inserts a signed-by-wallet transaction record into the // store, returning the record. Duplicates and double spend correction is // handled automatically. Transactions may be added without block details, // and later added again with block details once the tx has been mined. func (s *Store) InsertSignedTx(tx *btcutil.Tx, created time.Time, block *BlockDetails) (*SignedTx, error) { // Partially create the signedTx. Everything is set except the // total btc input, which is set below. st := &signedTx{ txSha: *tx.Sha(), created: created, block: block, } err := s.insertTx(tx, st) if err != nil { return nil, ErrInconsistantStore } return st.record(s).(*SignedTx), nil } // Rollback removes block details for all transactions at or beyond a // removed block at a given blockchain height. Any updated // transactions are considered unmined. Now-invalid transactions are // removed as new transactions creating double spends in the new better // chain are added to the store. func (s *Store) Rollback(height int32) { for e := s.sorted.Front(); e != nil; e = e.Next() { record := e.Value.(txRecord) block := record.Block() if block == nil { // Unmined, no block details to remove. continue } txSha := record.TxSha() if block.Height >= height { oldKey := blockTx{*txSha, block.Height} record.setBlock(nil) switch v := record.(type) { case *signedTx: k := oldKey delete(s.signed, k) k.height = -1 s.signed[k] = v case *recvTxOut: k := blockOutPoint{v.outpoint, block.Height} delete(s.recv, k) k.height = -1 s.recv[k] = v } if utx, ok := s.txs[oldKey]; ok { k := oldKey delete(s.txs, k) k.height = -1 s.txs[k] = utx } } } } // UnminedSignedTxs returns the underlying transactions for all // signed-by-wallet transactions which are not known to have been // mined in a block. func (s *Store) UnminedSignedTxs() []*btcutil.Tx { unmined := make([]*btcutil.Tx, 0, len(s.signed)) for _, stx := range s.signed { if stx.block == nil { unmined = append(unmined, s.txs[stx.blockTx()]) } } return unmined } // InsertRecvTxOut inserts a received transaction output record into the store, // returning the record. Duplicates and double spend correction is handled // automatically. Outputs may be added with block=nil, and then added again // with non-nil BlockDetails to update the record and all other records // using the transaction with the block. func (s *Store) InsertRecvTxOut(tx *btcutil.Tx, outIdx uint32, change bool, received time.Time, block *BlockDetails) (*RecvTxOut, error) { rt := &recvTxOut{ outpoint: *btcwire.NewOutPoint(tx.Sha(), outIdx), change: change, received: received, block: block, } err := s.insertTx(tx, rt) if err != nil { return nil, err } return rt.record(s).(*RecvTxOut), nil } func (s *Store) insertTx(utx *btcutil.Tx, record txRecord) error { if ds := s.findDoubleSpend(utx); ds != nil { switch { case ds.txSha == *utx.Sha(): // identical tx if ds.height != record.Height() { // Detect insert inconsistancies. If matching // tx was found, but this record's block is unset, // a rollback was missed. block := record.Block() if block == nil { return ErrInconsistantStore } s.setTxBlock(utx.Sha(), block) return nil } default: // Double-spend or mutation. Both are handled the same // (remove any now-invalid entries), and then insert the // new record. s.removeDoubleSpends(ds) } } s.insertUniqueTx(utx, record) return nil } func (s *Store) insertUniqueTx(utx *btcutil.Tx, record txRecord) { k := blockTx{*utx.Sha(), record.Height()} s.txs[k] = utx switch e := record.(type) { case *signedTx: if _, ok := s.signed[k]; ok { // Avoid adding a duplicate. return } // All the inputs should be currently unspent. Tally the total // input from each, and mark as spent. for _, txin := range utx.MsgTx().TxIn { op := txin.PreviousOutpoint if rt, ok := s.unspent[op]; ok { tx := s.txs[rt.blockTx()] e.totalIn += tx.MsgTx().TxOut[op.Index].Value rt.spentBy = &k delete(s.unspent, txin.PreviousOutpoint) } } s.signed[k] = e case *recvTxOut: blockOP := blockOutPoint{e.outpoint, record.Height()} if _, ok := s.recv[blockOP]; ok { // Avoid adding a duplicate. return } s.recv[blockOP] = e s.unspent[e.outpoint] = e // all recv'd txouts are added unspent } sortedInsert(s.sorted, record) } // doubleSpend checks all inputs between transaction a and b, returning true // if any two inputs share the same previous outpoint. func doubleSpend(a, b *btcwire.MsgTx) bool { ain := make(map[btcwire.OutPoint]struct{}) for i := range a.TxIn { ain[a.TxIn[i].PreviousOutpoint] = struct{}{} } for i := range b.TxIn { if _, ok := ain[b.TxIn[i].PreviousOutpoint]; ok { return true } } return false } func (s *Store) findDoubleSpend(tx *btcutil.Tx) *blockTx { // This MUST seach the ordered record list in in reverse order to // find the double spends of the most recent matching outpoint, as // spending the same outpoint is legal provided a previous transaction // output with an equivalent transaction sha is fully spent. for e := s.sorted.Back(); e != nil; e = e.Prev() { record := e.Value.(txRecord) storeTx := record.record(s).Tx() if doubleSpend(tx.MsgTx(), storeTx.MsgTx()) { btx := record.blockTx() return &btx } } return nil } func (s *Store) removeDoubleSpendsFromMaps(oldKey *blockTx, removed map[blockTx]struct{}) { // Lookup old backing tx. tx := s.txs[*oldKey] // Lookup a signed tx record. If found, remove it and mark the map // removal. if _, ok := s.signed[*oldKey]; ok { delete(s.signed, *oldKey) removed[*oldKey] = struct{}{} } // For each old txout, if a received txout record exists, remove it. // If the txout has been spent, the spending tx is invalid as well, so // all entries for it are removed as well. for i := range tx.MsgTx().TxOut { blockOP := blockOutPoint{ op: *btcwire.NewOutPoint(&oldKey.txSha, uint32(i)), height: oldKey.height, } if rtx, ok := s.recv[blockOP]; ok { delete(s.recv, blockOP) delete(s.unspent, blockOP.op) removed[*oldKey] = struct{}{} if rtx.spentBy != nil { s.removeDoubleSpendsFromMaps(rtx.spentBy, removed) } } } // Remove old backing tx. delete(s.txs, *oldKey) } func (s *Store) removeDoubleSpends(oldKey *blockTx) { // Keep a set of block transactions for all removed entries. This is // used to remove all dead records from the sorted linked list. removed := make(map[blockTx]struct{}) // Remove entries from store maps. s.removeDoubleSpendsFromMaps(oldKey, removed) // Remove any record with a matching block transaction from the sorted // record linked list. var enext *list.Element for e := s.sorted.Front(); e != nil; e = enext { enext = e.Next() record := e.Value.(txRecord) if _, ok := removed[record.blockTx()]; ok { s.sorted.Remove(e) } } } func (s *Store) setTxBlock(txSha *btcwire.ShaHash, block *BlockDetails) { // Lookup unmined backing tx. prevKey := blockTx{*txSha, -1} tx := s.txs[prevKey] // Lookup a signed tx record. If found, modify the record to // set the block and update the store key. if stx, ok := s.signed[prevKey]; ok { stx.setBlock(block) delete(s.signed, prevKey) s.signed[stx.blockTx()] = stx } // For each txout, if a recveived txout record exists, modify // the record to set the block and update the store key. for txOutIndex := range tx.MsgTx().TxOut { op := btcwire.NewOutPoint(txSha, uint32(txOutIndex)) prevKey := blockOutPoint{*op, -1} if rtx, ok := s.recv[prevKey]; ok { rtx.setBlock(block) delete(s.recv, prevKey) newKey := blockOutPoint{*op, rtx.Height()} s.recv[newKey] = rtx } } // Switch out keys for the backing tx map. delete(s.txs, prevKey) newKey := blockTx{*txSha, block.Height} s.txs[newKey] = tx } // UnspentOutputs returns all unspent received transaction outputs. // The order is undefined. func (s *Store) UnspentOutputs() []*RecvTxOut { unspent := make([]*RecvTxOut, 0, len(s.unspent)) for _, record := range s.unspent { unspent = append(unspent, record.record(s).(*RecvTxOut)) } return unspent } // confirmed checks whether a transaction at height txHeight has met // minConf confirmations for a blockchain at height chainHeight. func confirmed(minConf int, txHeight, chainHeight int32) bool { if minConf == 0 { return true } if txHeight != -1 && int(chainHeight-txHeight+1) >= minConf { return true } return false } // Balance returns a wallet balance (total value of all unspent // transaction outputs) given a minimum of minConf confirmations, // calculated at a current chain height of curHeight. The balance is // returned in units of satoshis. func (s *Store) Balance(minConf int, chainHeight int32) int64 { bal := int64(0) for _, rt := range s.unspent { if confirmed(minConf, rt.Height(), chainHeight) { tx := s.txs[rt.blockTx()] msgTx := tx.MsgTx() txOut := msgTx.TxOut[rt.outpoint.Index] bal += txOut.Value } } return bal } // SortedRecords returns a chronologically-ordered slice of Records. func (s *Store) SortedRecords() []Record { records := make([]Record, 0, s.sorted.Len()) for e := s.sorted.Front(); e != nil; e = e.Next() { record := e.Value.(txRecord) records = append(records, record.record(s)) } return records } type msgTx btcwire.MsgTx func (tx *msgTx) readFrom(r io.Reader) (int64, error) { // Read from a TeeReader to return the number of read bytes. buf := new(bytes.Buffer) tr := io.TeeReader(r, buf) if err := (*btcwire.MsgTx)(tx).Deserialize(tr); err != nil { if buf.Len() != 0 && err == io.EOF { err = io.ErrUnexpectedEOF } return int64(buf.Len()), err } return int64((*btcwire.MsgTx)(tx).SerializeSize()), nil } func (tx *msgTx) writeTo(w io.Writer) (int64, error) { // Write to a buffer and then copy to w so the total number // of bytes written can be returned to the caller. Writing // to a bytes.Buffer never fails except for OOM, so omit the // serialization error check. buf := new(bytes.Buffer) (*btcwire.MsgTx)(tx).Serialize(buf) return io.Copy(w, buf) } type signedTx struct { txSha btcwire.ShaHash created time.Time totalIn int64 block *BlockDetails // nil if unmined } func (st *signedTx) blockTx() blockTx { return blockTx{st.txSha, st.Height()} } func (st *signedTx) readFrom(r io.Reader) (int64, error) { // Fill in calculated fields with serialized data on success. var err error defer func() { if err != nil { return } }() // Read txSha n, err := io.ReadFull(r, st.txSha[:]) n64 := int64(n) if err != nil { return n64, err } // Read creation time timeBytes := make([]byte, 8) n, err = io.ReadFull(r, timeBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } st.created = time.Unix(int64(binary.LittleEndian.Uint64(timeBytes)), 0) // Read total BTC in totalInBytes := make([]byte, 8) n, err = io.ReadFull(r, totalInBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } st.totalIn = int64(binary.LittleEndian.Uint64(totalInBytes)) // Read flags flagByte := make([]byte, 1) n, err = io.ReadFull(r, flagByte) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } flags := flagByte[0] // Read block details if specified in flags if flags&(1<<0) != 0 { st.block = new(BlockDetails) n, err := st.block.readFrom(r) n64 += n if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } } else { st.block = nil } return n64, nil } func (st *signedTx) writeTo(w io.Writer) (int64, error) { // Write txSha n, err := w.Write(st.txSha[:]) n64 := int64(n) if err != nil { return n64, err } // Write creation time timeBytes := make([]byte, 8) binary.LittleEndian.PutUint64(timeBytes, uint64(st.created.Unix())) n, err = w.Write(timeBytes) n64 += int64(n) if err != nil { return n64, err } // Write total BTC in totalInBytes := make([]byte, 8) binary.LittleEndian.PutUint64(totalInBytes, uint64(st.totalIn)) n, err = w.Write(totalInBytes) n64 += int64(n) if err != nil { return n64, err } // Create and write flags var flags byte if st.block != nil { flags |= 1 << 0 } n, err = w.Write([]byte{flags}) n64 += int64(n) if err != nil { return n64, err } // Write block details if set if st.block != nil { n, err := st.block.writeTo(w) n64 += n if err != nil { return n64, err } } return n64, nil } func (st *signedTx) TxSha() *btcwire.ShaHash { return &st.txSha } func (st *signedTx) Time() time.Time { return st.created } func (st *signedTx) setBlock(details *BlockDetails) { st.block = details } func (st *signedTx) Block() *BlockDetails { return st.block } // Height returns the blockchain height of the transaction. If the // transaction is unmined, this returns -1. func (st *signedTx) Height() int32 { height := int32(-1) if st.block != nil { height = st.block.Height } return height } // TotalSent returns the total number of satoshis spent by all transaction // inputs. func (st *signedTx) TotalSent() int64 { return st.totalIn } func (st *signedTx) record(s *Store) Record { tx := s.txs[st.blockTx()] totalOut := int64(0) for _, txOut := range tx.MsgTx().TxOut { totalOut += txOut.Value } record := &SignedTx{ signedTx: *st, tx: tx, fee: st.totalIn - totalOut, } return record } // SignedTx is a type representing a transaction partially or fully signed // by wallet keys. type SignedTx struct { signedTx tx *btcutil.Tx fee int64 } // Fee returns the fee (total inputs - total outputs) of the transaction. func (st *SignedTx) Fee() int64 { return st.fee } // Tx returns the underlying transaction managed by the store. func (st *SignedTx) Tx() *btcutil.Tx { return st.tx } // TxInfo returns a slice of objects that may be marshaled as a JSON array // of JSON objects for a listtransactions RPC reply. func (st *SignedTx) TxInfo(account string, chainHeight int32, net btcwire.BitcoinNet) []btcjson.ListTransactionsResult { reply := make([]btcjson.ListTransactionsResult, len(st.tx.MsgTx().TxOut)) var confirmations int32 if st.block != nil { confirmations = chainHeight - st.block.Height + 1 } for i, txout := range st.tx.MsgTx().TxOut { address := "Unknown" _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, net) if len(addrs) == 1 { address = addrs[0].EncodeAddress() } info := btcjson.ListTransactionsResult{ Account: account, Address: address, Category: "send", Amount: float64(-txout.Value) / float64(btcutil.SatoshiPerBitcoin), Fee: float64(st.Fee()) / float64(btcutil.SatoshiPerBitcoin), Confirmations: int64(confirmations), TxID: st.txSha.String(), Time: st.created.Unix(), TimeReceived: st.created.Unix(), } if st.block != nil { info.BlockHash = st.block.Hash.String() info.BlockIndex = int64(st.block.Index) info.BlockTime = st.block.Time.Unix() } reply[i] = info } return reply } // BlockDetails holds details about a transaction contained in a block. type BlockDetails struct { Height int32 Hash btcwire.ShaHash Index int32 Time time.Time } func (block *BlockDetails) readFrom(r io.Reader) (int64, error) { // Read height heightBytes := make([]byte, 4) n, err := io.ReadFull(r, heightBytes) n64 := int64(n) if err != nil { return n64, err } block.Height = int32(binary.LittleEndian.Uint32(heightBytes)) // Read hash n, err = io.ReadFull(r, block.Hash[:]) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } // Read index indexBytes := make([]byte, 4) n, err = io.ReadFull(r, indexBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } block.Index = int32(binary.LittleEndian.Uint32(indexBytes)) // Read unix time timeBytes := make([]byte, 8) n, err = io.ReadFull(r, timeBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } block.Time = time.Unix(int64(binary.LittleEndian.Uint64(timeBytes)), 0) return n64, err } func (block *BlockDetails) writeTo(w io.Writer) (int64, error) { // Write height heightBytes := make([]byte, 4) binary.LittleEndian.PutUint32(heightBytes, uint32(block.Height)) n, err := w.Write(heightBytes) n64 := int64(n) if err != nil { return n64, err } // Write hash n, err = w.Write(block.Hash[:]) n64 += int64(n) if err != nil { return n64, err } // Write index indexBytes := make([]byte, 4) binary.LittleEndian.PutUint32(indexBytes, uint32(block.Index)) n, err = w.Write(indexBytes) n64 += int64(n) if err != nil { return n64, err } // Write unix time timeBytes := make([]byte, 8) binary.LittleEndian.PutUint64(timeBytes, uint64(block.Time.Unix())) n, err = w.Write(timeBytes) n64 += int64(n) if err != nil { return n64, err } return n64, nil } type recvTxOut struct { outpoint btcwire.OutPoint change bool locked bool received time.Time block *BlockDetails // nil if unmined spentBy *blockTx // nil if unspent } func (rt *recvTxOut) blockTx() blockTx { return blockTx{rt.outpoint.Hash, rt.Height()} } func (rt *recvTxOut) readFrom(r io.Reader) (int64, error) { // Read outpoint (Sha, index) n, err := io.ReadFull(r, rt.outpoint.Hash[:]) n64 := int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } indexBytes := make([]byte, 4) n, err = io.ReadFull(r, indexBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } rt.outpoint.Index = binary.LittleEndian.Uint32(indexBytes) // Read time received timeReceivedBytes := make([]byte, 8) n, err = io.ReadFull(r, timeReceivedBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } rt.received = time.Unix(int64(binary.LittleEndian.Uint64(timeReceivedBytes)), 0) // Create and read flags (change, is spent, block set) flagBytes := make([]byte, 1) n, err = io.ReadFull(r, flagBytes) n64 += int64(n) if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } flags := flagBytes[0] // Set change based on flags rt.change = flags&(1<<0) != 0 rt.locked = flags&(1<<1) != 0 // Read block details if specified in flags if flags&(1<<2) != 0 { rt.block = new(BlockDetails) n, err := rt.block.readFrom(r) n64 += n if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } } else { rt.block = nil } // Read spent by data if specified in flags if flags&(1<<3) != 0 { rt.spentBy = new(blockTx) n, err := rt.spentBy.readFrom(r) n64 += n if err == io.EOF { err = io.ErrUnexpectedEOF } if err != nil { return n64, err } } else { rt.spentBy = nil } return n64, nil } func (rt *recvTxOut) writeTo(w io.Writer) (int64, error) { // Write outpoint (Sha, index) n, err := w.Write(rt.outpoint.Hash[:]) n64 := int64(n) if err != nil { return n64, err } indexBytes := make([]byte, 4) binary.LittleEndian.PutUint32(indexBytes, rt.outpoint.Index) n, err = w.Write(indexBytes) n64 += int64(n) if err != nil { return n64, err } // Write time received timeReceivedBytes := make([]byte, 8) binary.LittleEndian.PutUint64(timeReceivedBytes, uint64(rt.received.Unix())) n, err = w.Write(timeReceivedBytes) n64 += int64(n) if err != nil { return n64, err } // Create and write flags (change, is spent, block set) var flags byte if rt.change { flags |= 1 << 0 } if rt.locked { flags |= 1 << 1 } if rt.block != nil { flags |= 1 << 2 } if rt.spentBy != nil { flags |= 1 << 3 } n, err = w.Write([]byte{flags}) n64 += int64(n) if err != nil { return n64, err } // Write block details if set if rt.block != nil { n, err := rt.block.writeTo(w) n64 += n if err != nil { return n64, err } } // Write spent by data if set (Sha, block height) if rt.spentBy != nil { n, err := rt.spentBy.writeTo(w) n64 += n if err != nil { return n64, err } } return n64, nil } // TxSha returns the sha of the transaction containing this output. func (rt *recvTxOut) TxSha() *btcwire.ShaHash { return &rt.outpoint.Hash } // OutPoint returns the outpoint to be included when creating transaction // inputs referencing this output. func (rt *recvTxOut) OutPoint() *btcwire.OutPoint { return &rt.outpoint } // Time returns the time the transaction containing this output was received. func (rt *recvTxOut) Time() time.Time { return rt.received } // Change returns whether the received output was created for a change address. func (rt *recvTxOut) Change() bool { return rt.change } // Spent returns whether the transaction output has been spent by a later // transaction. func (rt *recvTxOut) Spent() bool { return rt.spentBy != nil } // SpentBy returns the tx sha and blockchain height of the transaction // spending an output. func (rt *recvTxOut) SpentBy() (txSha *btcwire.ShaHash, height int32) { if rt.spentBy == nil { return nil, 0 } return &rt.spentBy.txSha, rt.spentBy.height } // Locked returns the current lock state of an unspent transaction output. func (rt *recvTxOut) Locked() bool { return rt.locked } // SetLocked locks or unlocks an unspent transaction output. func (rt *recvTxOut) SetLocked(locked bool) { rt.locked = locked } // Block returns details of the block containing this transaction, or nil // if the tx is unmined. func (rt *recvTxOut) Block() *BlockDetails { return rt.block } // Height returns the blockchain height of the transaction containing // this output. If the transaction is unmined, this returns -1. func (rt *recvTxOut) Height() int32 { height := int32(-1) if rt.block != nil { height = rt.block.Height } return height } func (rt *recvTxOut) setBlock(details *BlockDetails) { rt.block = details } func (rt *recvTxOut) record(s *Store) Record { record := &RecvTxOut{ recvTxOut: *rt, tx: s.txs[rt.blockTx()], } return record } // RecvTxOut is a type additional information for transaction outputs which // are spendable by a wallet. type RecvTxOut struct { recvTxOut tx *btcutil.Tx } // Addresses parses the pubkey script, extracting all addresses for a // standard script. func (rt *RecvTxOut) Addresses(net btcwire.BitcoinNet) (btcscript.ScriptClass, []btcutil.Address, int, error) { tx := rt.tx.MsgTx() return btcscript.ExtractPkScriptAddrs(tx.TxOut[rt.outpoint.Index].PkScript, net) } // IsCoinbase returns whether the received transaction output is an output // a coinbase transaction. func (rt *RecvTxOut) IsCoinbase() bool { if rt.recvTxOut.block == nil { return false } return rt.recvTxOut.block.Index == 0 } // PkScript returns the pubkey script of the output. func (rt *RecvTxOut) PkScript() []byte { tx := rt.tx.MsgTx() return tx.TxOut[rt.outpoint.Index].PkScript } // Value returns the number of satoshis sent by the output. func (rt *RecvTxOut) Value() int64 { tx := rt.tx.MsgTx() return tx.TxOut[rt.outpoint.Index].Value } // Tx returns the transaction which contains this output. func (rt *RecvTxOut) Tx() *btcutil.Tx { return rt.tx } // TxInfo returns a slice of objects that may be marshaled as a JSON array // of JSON objects for a listtransactions RPC reply. func (rt *RecvTxOut) TxInfo(account string, chainHeight int32, net btcwire.BitcoinNet) []btcjson.ListTransactionsResult { tx := rt.tx.MsgTx() outidx := rt.outpoint.Index txout := tx.TxOut[outidx] address := "Unknown" _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, net) if len(addrs) == 1 { address = addrs[0].EncodeAddress() } result := btcjson.ListTransactionsResult{ Account: account, Category: "receive", Address: address, Amount: float64(txout.Value) / float64(btcutil.SatoshiPerBitcoin), TxID: rt.outpoint.Hash.String(), TimeReceived: rt.received.Unix(), } if rt.block != nil { result.BlockHash = rt.block.Hash.String() result.BlockIndex = int64(rt.block.Index) result.BlockTime = rt.block.Time.Unix() result.Confirmations = int64(chainHeight - rt.block.Height + 1) } else { result.Confirmations = 0 } return []btcjson.ListTransactionsResult{result} }