diff --git a/account.go b/account.go index 27e1ddd..1c99721 100644 --- a/account.go +++ b/account.go @@ -27,6 +27,7 @@ import ( "github.com/conformal/btcwire" "github.com/conformal/btcws" "sync" + "time" ) var accounts = NewAccountStore() @@ -451,36 +452,44 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo } return false } - sender, ok := v["sender"].(string) - if !ok { - log.Error("Tx Handler: Unspecified sender.") - return false - } receiver, ok := v["receiver"].(string) if !ok { log.Error("Tx Handler: Unspecified receiver.") return false } - blockhashBE, ok := v["blockhash"].(string) - if !ok { - log.Error("Tx Handler: Unspecified block hash.") - return false - } height, ok := v["height"].(float64) if !ok { log.Error("Tx Handler: Unspecified height.") return false } - txhashBE, ok := v["txhash"].(string) + blockHashBE, ok := v["blockhash"].(string) + if !ok { + log.Error("Tx Handler: Unspecified block hash.") + return false + } + fblockIndex, ok := v["blockindex"].(float64) + if !ok { + log.Error("Tx Handler: Unspecified block index.") + return false + } + blockIndex := int32(fblockIndex) + fblockTime, ok := v["blocktime"].(float64) + if !ok { + log.Error("Tx Handler: Unspecified block time.") + return false + } + blockTime := int64(fblockTime) + txhashBE, ok := v["txid"].(string) if !ok { log.Error("Tx Handler: Unspecified transaction hash.") return false } - index, ok := v["index"].(float64) + ftxOutIndex, ok := v["txoutindex"].(float64) if !ok { - log.Error("Tx Handler: Unspecified transaction index.") + log.Error("Tx Handler: Unspecified transaction output index.") return false } + txOutIndex := int32(ftxOutIndex) amt, ok := v["amount"].(float64) if !ok { log.Error("Tx Handler: Unspecified amount.") @@ -499,18 +508,16 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo // btcd sends the block and tx hashes as BE strings. Convert both // to a LE ShaHash. - blockhash, err := btcwire.NewShaHashFromStr(blockhashBE) + blockHash, err := btcwire.NewShaHashFromStr(blockHashBE) if err != nil { log.Errorf("Tx Handler: Block hash string cannot be parsed: %v", err) return false } - txhash, err := btcwire.NewShaHashFromStr(txhashBE) + txID, err := btcwire.NewShaHashFromStr(txhashBE) if err != nil { log.Errorf("Tx Handler: Tx hash string cannot be parsed: %v", err) return false } - // TODO(jrick): btcd does not find the sender yet. - senderHash, _, _ := btcutil.DecodeAddress(sender) receiverHash, _, err := btcutil.DecodeAddress(receiver) if err != nil { log.Errorf("Tx Handler: receiver address can not be decoded: %v", err) @@ -519,12 +526,15 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo // Add to TxStore. t := &tx.RecvTx{ - Amt: uint64(amt), + TxID: *txID, + TimeReceived: time.Now().Unix(), + BlockHeight: int32(height), + BlockHash: *blockHash, + BlockIndex: blockIndex, + BlockTime: blockTime, + Amount: int64(amt), + ReceiverHash: receiverHash, } - copy(t.TxHash[:], txhash[:]) - copy(t.BlockHash[:], blockhash[:]) - copy(t.SenderAddr[:], senderHash) - copy(t.ReceiverAddr[:], receiverHash) a.TxStore.Lock() txs := a.TxStore.s @@ -532,14 +542,13 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo a.TxStore.dirty = true a.TxStore.Unlock() - // Add to UtxoStore if unspent. if !spent { // First, iterate through all stored utxos. If an unconfirmed utxo // (not present in a block) has the same outpoint as this utxo, // update the block height and hash. a.UtxoStore.RLock() for _, u := range a.UtxoStore.s { - if bytes.Equal(u.Out.Hash[:], txhash[:]) && u.Out.Index == uint32(index) { + if bytes.Equal(u.Out.Hash[:], txID[:]) && u.Out.Index == uint32(txOutIndex) { // Found a either a duplicate, or a change UTXO. If not change, // ignore it. a.UtxoStore.RUnlock() @@ -548,7 +557,7 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo } a.UtxoStore.Lock() - copy(u.BlockHash[:], blockhash[:]) + copy(u.BlockHash[:], blockHash[:]) u.Height = int32(height) a.UtxoStore.dirty = true a.UtxoStore.Unlock() @@ -566,10 +575,10 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo Height: int32(height), Subscript: pkscript, } - copy(u.Out.Hash[:], txhash[:]) - u.Out.Index = uint32(index) + copy(u.Out.Hash[:], txID[:]) + u.Out.Index = uint32(txOutIndex) copy(u.AddrHash[:], receiverHash) - copy(u.BlockHash[:], blockhash[:]) + copy(u.BlockHash[:], blockHash[:]) a.UtxoStore.Lock() a.UtxoStore.s = append(a.UtxoStore.s, u) a.UtxoStore.dirty = true diff --git a/cmd.go b/cmd.go index 4bf78b1..3276b4c 100644 --- a/cmd.go +++ b/cmd.go @@ -103,6 +103,8 @@ func checkCreateAccountDir(path string) error { // Wallets opened from this function are not set to track against a // btcd connection. func OpenAccount(cfg *config, account string) (*Account, error) { + var finalErr error + adir := accountdir(cfg, account) if err := checkCreateAccountDir(adir); err != nil { return nil, err @@ -134,6 +136,25 @@ func OpenAccount(cfg *config, account string) (*Account, error) { name: account, } + // Read tx file. If this fails, return a ErrNoTxs error and let + // the caller decide if a rescan is necessary. + if txfile, err = os.Open(txfilepath); err != nil { + log.Errorf("cannot open tx file: %s", err) + // This is not a error we should immediately return with, + // but other errors can be more important, so only return + // this if none of the others are hit. + finalErr = ErrNoTxs + } else { + defer txfile.Close() + var txs tx.TxStore + if _, err = txs.ReadFrom(txfile); err != nil { + log.Errorf("cannot read tx file: %s", err) + finalErr = ErrNoTxs + } else { + a.TxStore.s = txs + } + } + // Read utxo file. If this fails, return a ErrNoUtxos error so a // rescan can be done since the wallet creation block. var utxos tx.UtxoStore @@ -144,25 +165,12 @@ func OpenAccount(cfg *config, account string) (*Account, error) { defer utxofile.Close() if _, err = utxos.ReadFrom(utxofile); err != nil { log.Errorf("cannot read utxo file: %s", err) - return a, ErrNoUtxos + finalErr = ErrNoUtxos + } else { + a.UtxoStore.s = utxos } - a.UtxoStore.s = utxos - // Read tx file. If this fails, return a ErrNoTxs error and let - // the caller decide if a rescan is necessary. - if txfile, err = os.Open(txfilepath); err != nil { - log.Errorf("cannot open tx file: %s", err) - return a, ErrNoTxs - } - defer txfile.Close() - var txs tx.TxStore - if _, err = txs.ReadFrom(txfile); err != nil { - log.Errorf("cannot read tx file: %s", err) - return a, ErrNoTxs - } - a.TxStore.s = txs - - return a, nil + return a, finalErr } // GetCurBlock returns the blockchain height and SHA hash of the most diff --git a/cmdmgr.go b/cmdmgr.go index 7ec807a..ddf1697 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "github.com/conformal/btcjson" + "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" @@ -45,6 +46,7 @@ var rpcHandlers = map[string]cmdHandler{ "getnewaddress": GetNewAddress, "importprivkey": ImportPrivKey, "listaccounts": ListAccounts, + "listtransactions": ListTransactions, "sendfrom": SendFrom, "sendmany": SendMany, "settxfee": SetTxFee, @@ -458,6 +460,59 @@ func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) { ReplySuccess(frontend, cmd.Id(), pairs) } +// ListTransactions replies to a listtransactions request by returning a +// JSON object with details of sent and recevied wallet transactions. +func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) { + // Type assert icmd to access parameters. + cmd, ok := icmd.(*btcjson.ListTransactionsCmd) + if !ok { + ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) + return + } + + // Check that the account specified in the request exists. + a, ok := accounts.m[cmd.Account] + if !ok { + ReplyError(frontend, cmd.Id(), + &btcjson.ErrWalletInvalidAccountName) + return + } + + // Get current block. The block height used for calculating + // the number of tx confirmations. + bs, err := GetCurBlock() + if err != nil { + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return + } + + a.mtx.RLock() + a.TxStore.RLock() + var txInfoList []map[string]interface{} + lastLookupIdx := len(a.TxStore.s) - cmd.Count + // Search in reverse order: lookup most recently-added first. + for i := len(a.TxStore.s) - 1; i >= cmd.From && i >= lastLookupIdx; i-- { + switch e := a.TxStore.s[i].(type) { + case *tx.SendTx: + infos := e.TxInfo(a.Name(), bs.Height, a.Net()) + txInfoList = append(txInfoList, infos...) + + case *tx.RecvTx: + info := e.TxInfo(a.Name(), bs.Height, a.Net()) + txInfoList = append(txInfoList, info) + } + } + a.mtx.RUnlock() + a.TxStore.RUnlock() + + // Reply with the list of tx information. + ReplySuccess(frontend, cmd.Id(), txInfoList) +} + // SendFrom creates a new transaction spending unspent transaction // outputs for a wallet to another payment address. Leftover inputs // not sent to the payment address or a fee for the miner are sent @@ -668,41 +723,70 @@ func SendMany(frontend chan []byte, icmd btcjson.Cmd) { } func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, - result interface{}, err *btcjson.Error, a *Account, + result interface{}, e *btcjson.Error, a *Account, txInfo *CreatedTx) bool { - if err != nil { - ReplyError(frontend, icmd.Id(), err) + if e != nil { + ReplyError(frontend, icmd.Id(), e) return true } + txIDStr, ok := result.(string) + if !ok { + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "Unexpected type from btcd reply", + } + ReplyError(frontend, icmd.Id(), e) + return true + } + txID, err := btcwire.NewShaHashFromStr(txIDStr) + if err != nil { + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "Invalid hash string from btcd reply", + } + ReplyError(frontend, icmd.Id(), e) + return true + } + + // Add to transaction store. + sendtx := &tx.SendTx{ + TxID: *txID, + Time: txInfo.time.Unix(), + BlockHeight: -1, + Fee: txInfo.fee, + Receivers: txInfo.outputs, + } + a.TxStore.Lock() + a.TxStore.s = append(a.TxStore.s, sendtx) + a.TxStore.dirty = true + a.TxStore.Unlock() + // Remove previous unspent outputs now spent by the tx. a.UtxoStore.Lock() modified := a.UtxoStore.s.Remove(txInfo.inputs) + a.UtxoStore.dirty = a.UtxoStore.dirty || modified // Add unconfirmed change utxo (if any) to UtxoStore. if txInfo.changeUtxo != nil { a.UtxoStore.s = append(a.UtxoStore.s, txInfo.changeUtxo) a.ReqSpentUtxoNtfn(txInfo.changeUtxo) - modified = true - } - - if modified { a.UtxoStore.dirty = true - a.UtxoStore.Unlock() - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) - } - - // Notify all frontends of account's new unconfirmed and - // confirmed balance. - confirmed := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - confirmed - NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) - NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed) - } else { - a.UtxoStore.Unlock() } + a.UtxoStore.Unlock() + + // Disk sync tx and utxo stores. + if err := a.writeDirtyToDisk(); err != nil { + log.Errorf("cannot sync dirty wallet: %v", err) + } + + // Notify all frontends of account's new unconfirmed and + // confirmed balance. + confirmed := a.CalculateBalance(1) + unconfirmed := a.CalculateBalance(0) - confirmed + NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) + NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed) // btcd cannot be trusted to successfully relay the tx to the // Bitcoin network. Even if this succeeds, the rawtx must be @@ -713,7 +797,7 @@ func handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, // Add hex string of raw tx to sent tx pool. If btcd disconnects // and is reconnected, these txs are resent. UnminedTxs.Lock() - UnminedTxs.m[TXID(result.(string))] = txInfo + UnminedTxs.m[TXID(*txID)] = txInfo UnminedTxs.Unlock() log.Debugf("successfully sent transaction %v", result) diff --git a/createtx.go b/createtx.go index a3c5b64..97b800b 100644 --- a/createtx.go +++ b/createtx.go @@ -61,19 +61,22 @@ var TxFee = struct { // for change (if any). type CreatedTx struct { rawTx []byte + time time.Time inputs []*tx.Utxo + outputs []tx.Pair + btcout int64 + fee int64 changeAddr string changeUtxo *tx.Utxo } // TXID is a transaction hash identifying a transaction. -type TXID string +type TXID btcwire.ShaHash // UnminedTXs holds a map of transaction IDs as keys mapping to a -// hex string of a raw transaction. If sending a raw transaction -// succeeds, the tx is added to this map and checked again after each -// new block. If the new block contains a tx, it is removed from -// this map. Otherwise, btcwallet will resend the tx to btcd. +// CreatedTx structure. If sending a raw transaction succeeds, the +// tx is added to this map and checked again after each new block. +// If the new block contains a tx, it is removed from this map. var UnminedTxs = struct { sync.Mutex m map[TXID]*CreatedTx @@ -179,6 +182,10 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr return nil, err } + // outputs is a tx.Pair slice representing each output that is created + // by the transaction. + outputs := make([]tx.Pair, 0, len(pairs)+1) + // Add outputs to new tx. for addr, amt := range pairs { addr160, _, err := btcutil.DecodeAddress(addr) @@ -193,6 +200,13 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr } txout := btcwire.NewTxOut(int64(amt), pkScript) msgtx.AddTxOut(txout) + + // Create amount, address pair and add to outputs. + out := tx.Pair{ + Amount: amt, + PubkeyHash: addr160, + } + outputs = append(outputs, out) } // Check if there are leftover unspent outputs, and return coins back to @@ -238,6 +252,13 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr Subscript: pkScript, } copy(changeUtxo.AddrHash[:], changeAddrHash) + + // Add change to outputs. + out := tx.Pair{ + Amount: int64(change), + PubkeyHash: changeAddrHash, + } + outputs = append(outputs, out) } // Selected unspent outputs become new transaction's inputs. @@ -296,5 +317,15 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr buf := new(bytes.Buffer) msgtx.BtcEncode(buf, btcwire.ProtocolVersion) - return &CreatedTx{buf.Bytes(), inputs, changeAddr, changeUtxo}, nil + info := &CreatedTx{ + rawTx: buf.Bytes(), + time: time.Now(), + inputs: inputs, + outputs: outputs, + btcout: int64(btcout), + fee: fee, + changeAddr: changeAddr, + changeUtxo: changeUtxo, + } + return info, nil } diff --git a/sockets.go b/sockets.go index 4b45da8..73ac96e 100644 --- a/sockets.go +++ b/sockets.go @@ -17,6 +17,7 @@ package main import ( + "bytes" "code.google.com/p/go.net/websocket" "crypto/tls" "crypto/x509" @@ -25,6 +26,7 @@ import ( "errors" "fmt" "github.com/conformal/btcjson" + "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" @@ -527,15 +529,47 @@ func NtfnTxMined(n btcws.Notification) { log.Errorf("%v handler: unexpected type", n.Id()) return } + txid, err := btcwire.NewShaHashFromStr(tmn.TxID) if err != nil { log.Errorf("%v handler: invalid hash string", n.Id()) return } + blockhash, err := btcwire.NewShaHashFromStr(tmn.BlockHash) + if err != nil { + log.Errorf("%v handler: invalid block hash string", n.Id()) + return + } + + // Lookup tx in store and add block information. + accounts.Lock() +out: + for _, a := range accounts.m { + a.TxStore.Lock() + + // Search in reverse order, more likely to find it + // sooner that way. + for i := len(a.TxStore.s) - 1; i >= 0; i-- { + sendtx, ok := a.TxStore.s[i].(*tx.SendTx) + if ok { + if bytes.Equal(txid.Bytes(), sendtx.TxID[:]) { + copy(sendtx.BlockHash[:], blockhash.Bytes()) + sendtx.BlockHeight = tmn.BlockHeight + sendtx.BlockIndex = int32(tmn.Index) + sendtx.BlockTime = tmn.BlockTime + a.TxStore.Unlock() + break out + } + } + } + + a.TxStore.Unlock() + } + accounts.Unlock() // Remove mined transaction from pool. UnminedTxs.Lock() - delete(UnminedTxs.m, TXID(txid[:])) + delete(UnminedTxs.m, TXID(*txid)) UnminedTxs.Unlock() } diff --git a/tx/tx.go b/tx/tx.go index b12b8bc..d002412 100644 --- a/tx/tx.go +++ b/tx/tx.go @@ -22,14 +22,31 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/conformal/btcutil" "github.com/conformal/btcwire" "io" ) +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") +) + // Byte headers prepending received and sent serialized transactions. const ( - RecvTxHeader byte = iota - SendTxHeader + recvTxHeader byte = iota + sendTxHeader +) + +// File format versions. +const ( + utxoFileVersion uint32 = 0 + txFileVersion uint32 = 0 ) // UtxoStore is a type used for holding all Utxo structures for all @@ -61,29 +78,224 @@ type PkScript []byte // TxStore is a slice holding RecvTx and SendTx pointers. type TxStore []interface{} +const ( + addressUnknown byte = iota + addressKnown +) + +// pubkeyHash is a slice holding 20 bytes (for a known pubkey hash +// of a Bitcoin address), or nil (for an unknown address). +type pubkeyHash []byte + +// Enforce that pubkeyHash satisifies the io.ReaderFrom and +// io.WriterTo interfaces. +var pubkeyHashVar = pubkeyHash([]byte{}) +var _ io.ReaderFrom = &pubkeyHashVar +var _ io.WriterTo = &pubkeyHashVar + +// ReadFrom satisifies the io.ReaderFrom interface. +func (p *pubkeyHash) ReadFrom(r io.Reader) (int64, error) { + var read int64 + + // Read header byte. + header := make([]byte, 1) + n, err := r.Read(header) + if err != nil { + return int64(n), err + } + read += int64(n) + + switch header[0] { + case addressUnknown: + *p = nil + return read, nil + + case addressKnown: + addrHash := make([]byte, ripemd160.Size) + n, err := binaryRead(r, binary.LittleEndian, &addrHash) + if err != nil { + return read + int64(n), err + } + read += int64(n) + *p = addrHash + return read, nil + + default: + return read, ErrInvalidFormat + } +} + +// WriteTo satisifies the io.WriterTo interface. +func (p *pubkeyHash) WriteTo(w io.Writer) (int64, error) { + var written int64 + + switch { + case *p == nil: + n, err := w.Write([]byte{addressUnknown}) + return int64(n), err + + case len(*p) == ripemd160.Size: + // Write header. + n, err := w.Write([]byte{addressKnown}) + if err != nil { + return int64(n), err + } + written += int64(n) + + // Write hash160. + n, err = w.Write(*p) + if err != nil { + return written + int64(n), err + } + written += int64(n) + return written, err + + default: // bad! + return 0, ErrBadLength + } +} + // RecvTx is a type storing information about a transaction that was // received by an address in a wallet. type RecvTx struct { - TxHash btcwire.ShaHash + TxID btcwire.ShaHash + TimeReceived int64 + BlockHeight int32 BlockHash btcwire.ShaHash - Height int32 - Amt uint64 // Measured in Satoshis - SenderAddr [ripemd160.Size]byte - ReceiverAddr [ripemd160.Size]byte + BlockIndex int32 + BlockTime int64 + Amount int64 // Measured in Satoshis + ReceiverHash pubkeyHash +} + +// Pairs is a Pair slice with custom serialization and unserialization +// functions. +type Pairs []Pair + +// Enforce that Pairs satisifies the io.ReaderFrom and io.WriterTo +// interfaces. +var pairsVar = Pairs([]Pair{}) +var _ io.ReaderFrom = &pairsVar +var _ io.WriterTo = &pairsVar + +// ReadFrom reades a Pair slice from r. Part of the io.ReaderFrom interface. +func (p *Pairs) ReadFrom(r io.Reader) (int64, error) { + var read int64 + + nPairsBytes := make([]byte, 4) // Raw bytes for a uint32. + n, err := r.Read(nPairsBytes) + if err != nil { + return int64(n), err + } + read += int64(n) + nPairs := binary.LittleEndian.Uint32(nPairsBytes) + s := make([]Pair, nPairs) + + for i := range s { + n, err := s[i].ReadFrom(r) + if err != nil { + return read + n, err + } + read += n + } + + *p = s + return read, nil +} + +// WriteTo writes a Pair slice to w. Part of the io.WriterTo interface. +func (p *Pairs) WriteTo(w io.Writer) (int64, error) { + var written int64 + + nPairs := uint32(len(*p)) + nPairsBytes := make([]byte, 4) // Raw bytes for a uint32 + binary.LittleEndian.PutUint32(nPairsBytes, nPairs) + n, err := w.Write(nPairsBytes) + if err != nil { + return int64(n), err + } + written += int64(n) + + s := *p + for i := range s { + n, err := s[i].WriteTo(w) + if err != nil { + return written + n, err + } + written += n + } + + return written, nil +} + +// Pair represents an amount paid to a single pubkey hash. Pair includes +// custom serialization and unserialization functions by implementing the +// io.ReaderFromt and io.WriterTo interfaces. +type Pair struct { + PubkeyHash pubkeyHash + Amount int64 // Measured in Satoshis +} + +// Enforce that Pair satisifies the io.ReaderFrom and io.WriterTo +// interfaces. +var _ io.ReaderFrom = &Pair{} +var _ io.WriterTo = &Pair{} + +// ReadFrom reads a serialized Pair from r. Part of the io.ReaderFrom +// interface. +func (p *Pair) ReadFrom(r io.Reader) (int64, error) { + var read int64 + + n, err := p.PubkeyHash.ReadFrom(r) + if err != nil { + return n, err + } + read += n + + amountBytes := make([]byte, 8) // raw bytes for a uint64 + nr, err := r.Read(amountBytes) + if err != nil { + return read + int64(nr), err + } + read += int64(nr) + p.Amount = int64(binary.LittleEndian.Uint64(amountBytes)) + + return read, nil +} + +// WriteTo serializes a Pair, writing it to w. Part of the +// io.WriterTo interface. +func (p *Pair) WriteTo(w io.Writer) (int64, error) { + var written int64 + + n, err := p.PubkeyHash.WriteTo(w) + if err != nil { + return n, err + } + written += n + + amountBytes := make([]byte, 8) // raw bytes for a uint64 + binary.LittleEndian.PutUint64(amountBytes, uint64(p.Amount)) + nw, err := w.Write(amountBytes) + if err != nil { + return written + int64(nw), err + } + written += int64(nw) + + return written, nil } // SendTx is a type storing information about a transaction that was // sent by an address in a wallet. type SendTx struct { - TxHash btcwire.ShaHash - BlockHash btcwire.ShaHash - Height int32 - Fee uint64 // Measured in Satoshis - SenderAddr [ripemd160.Size]byte - ReceiverAddrs []struct { - Addr [ripemd160.Size]byte - Amt uint64 // Measured in Satoshis - } + TxID btcwire.ShaHash + Time int64 + BlockHeight int32 + BlockHash btcwire.ShaHash + BlockIndex int32 + BlockTime int64 + Fee int64 // Measured in Satoshis + Receivers Pairs } // We want to use binaryRead and binaryWrite instead of binary.Read @@ -117,19 +329,28 @@ func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64 // ReadFrom satisifies the io.ReaderFrom interface. Utxo structs are // read in from r until an io.EOF is reached. If an io.EOF is reached // before a Utxo is finished being read, err will be non-nil. -func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) { +func (u *UtxoStore) ReadFrom(r io.Reader) (int64, error) { var read int64 + + // Read the file version. This is currently not used. + versionBytes := make([]byte, 4) // bytes for a uint32 + n, err := r.Read(versionBytes) + if err != nil { + return int64(n), err + } + read = int64(n) + for { // Read Utxo utxo := new(Utxo) - read, err = utxo.ReadFrom(r) + n, err := utxo.ReadFrom(r) if err != nil { - if read == 0 && err == io.EOF { - return n, nil + if n == 0 && err == io.EOF { + err = nil } - return n + read, err + return read + n, err } - n += read + read += n *u = append(*u, utxo) } } @@ -137,18 +358,29 @@ func (u *UtxoStore) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. Each Utxo is written // to w, prepended by a single byte header to distinguish between // confirmed and unconfirmed outputs. -func (u *UtxoStore) WriteTo(w io.Writer) (n int64, err error) { +func (u *UtxoStore) WriteTo(w io.Writer) (int64, error) { var written int64 + + // Write file version. This is currently not used. + versionBytes := make([]byte, 4) // bytes for a uint32 + binary.LittleEndian.PutUint32(versionBytes, utxoFileVersion) + n, err := w.Write(versionBytes) + if err != nil { + return int64(n), err + } + written = int64(n) + + // Write each utxo in the store. for _, utxo := range *u { // Write Utxo - written, err = utxo.WriteTo(w) + n, err := utxo.WriteTo(w) if err != nil { - return n + written, err + return written + n, err } - n += written + written += n } - return n, nil + return written, nil } // Rollback removes all utxos from and after the block specified @@ -219,9 +451,12 @@ func (u *UtxoStore) Remove(toRemove []*Utxo) (modified bool) { // ReadFrom satisifies the io.ReaderFrom interface. A Utxo is read // from r with the format: // -// [AddrHash (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (4 bytes), BlockHash (32 bytes)] -// -// Each field is read little endian. +// AddrHash (20 bytes) +// Out (36 bytes) +// Subscript (varies) +// Amt (8 bytes, little endian) +// Height (4 bytes, little endian) +// BlockHash (32 bytes) func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) { datas := []interface{}{ &u.AddrHash, @@ -249,9 +484,12 @@ func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. A Utxo is written to // w in the format: // -// [AddrHash (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (4 bytes), BlockHash (32 bytes)] -// -// Each field is written little endian. +// AddrHash (20 bytes) +// Out (36 bytes) +// Subscript (varies) +// Amt (8 bytes, little endian) +// Height (4 bytes, little endian) +// BlockHash (32 bytes) func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) { datas := []interface{}{ &u.AddrHash, @@ -323,9 +561,8 @@ func (o *OutPoint) WriteTo(w io.Writer) (n int64, err error) { // ReadFrom satisifies the io.ReaderFrom interface. A PkScript is read // from r with the format: // -// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)] -// -// Length is read little endian. +// Length (4 byte, little endian) +// ScriptBytes (Length bytes) func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) { var scriptlen uint32 var read int64 @@ -349,9 +586,8 @@ func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. A PkScript is written // to w in the format: // -// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)] -// -// Length is written little endian. +// Length (4 byte, little endian) +// ScriptBytes (Length bytes) func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) { var written int64 written, err = binaryWrite(w, binary.LittleEndian, uint32(len(*s))) @@ -372,42 +608,54 @@ func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) { // ReadFrom satisifies the io.ReaderFrom interface. A TxStore is read // in from r with the format: // -// [[TxHeader (1 byte), Tx (varies in size)]...] -func (txs *TxStore) ReadFrom(r io.Reader) (n int64, err error) { +// Version (4 bytes, little endian) +// [(TxHeader (1 byte), Tx (varies in size))...] +func (txs *TxStore) ReadFrom(r io.Reader) (int64, error) { + var read int64 + + // Read the file version. This is currently not used. + versionBytes := make([]byte, 4) // bytes for a uint32 + n, err := r.Read(versionBytes) + if err != nil { + return int64(n), err + } + read += int64(n) + store := []interface{}{} defer func() { *txs = store }() - var read int64 for { // Read header var header byte - read, err = binaryRead(r, binary.LittleEndian, &header) + n, err := binaryRead(r, binary.LittleEndian, &header) if err != nil { // io.EOF is not an error here. if err == io.EOF { - return n + read, nil + err = nil } - return n + read, err + return read + n, err } - n += read + read += n var tx io.ReaderFrom switch header { - case RecvTxHeader: + case recvTxHeader: tx = new(RecvTx) - case SendTxHeader: + + case sendTxHeader: tx = new(SendTx) + default: return n, fmt.Errorf("unknown Tx header") } // Read tx - read, err = tx.ReadFrom(r) + n, err = tx.ReadFrom(r) if err != nil { - return n + read, err + return read + n, err } - n += read + read += n store = append(store, tx) } @@ -416,35 +664,48 @@ func (txs *TxStore) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. A TxStore is written // to w in the format: // -// [[TxHeader (1 byte), Tx (varies in size)]...] -func (txs *TxStore) WriteTo(w io.Writer) (n int64, err error) { - store := ([]interface{})(*txs) +// Version (4 bytes, little endian) +// [(TxHeader (1 byte), Tx (varies in size))...] +func (txs *TxStore) WriteTo(w io.Writer) (int64, error) { var written int64 + + // Write file version. This is currently not used. + versionBytes := make([]byte, 4) // bytes for a uint32 + binary.LittleEndian.PutUint32(versionBytes, utxoFileVersion) + n, err := w.Write(versionBytes) + if err != nil { + return int64(n), err + } + written = int64(n) + + store := ([]interface{})(*txs) for _, tx := range store { switch tx.(type) { case *RecvTx: - written, err = binaryWrite(w, binary.LittleEndian, RecvTxHeader) + n, err := binaryWrite(w, binary.LittleEndian, recvTxHeader) if err != nil { - return n + written, err + return written + n, err } - n += written + written += n + case *SendTx: - written, err = binaryWrite(w, binary.LittleEndian, SendTxHeader) + n, err := binaryWrite(w, binary.LittleEndian, sendTxHeader) if err != nil { - return n + written, err + return written + n, err } - n += written + written += n + default: - return n, fmt.Errorf("unknown type in TxStore") + return written, fmt.Errorf("unknown type in TxStore") } wt := tx.(io.WriterTo) - written, err = wt.WriteTo(w) + n, err := wt.WriteTo(w) if err != nil { - return n + written, err + return written + n, err } - n += written + written += n } - return n, nil + return written, nil } // Rollback removes all txs from and after the block specified by a @@ -470,25 +731,24 @@ func (txs *TxStore) Rollback(height int32, hash *btcwire.ShaHash) (modified bool }() for i := len(s) - 1; i >= 0; i-- { - var txheight int32 - var txhash *btcwire.ShaHash - switch s[i].(type) { + var txBlockHeight int32 + var txBlockHash *btcwire.ShaHash + switch tx := s[i].(type) { case *RecvTx: - tx := s[i].(*RecvTx) - if height > tx.Height { + if height > tx.BlockHeight { break } - txheight = tx.Height - txhash = &tx.BlockHash + txBlockHeight = tx.BlockHeight + txBlockHash = &tx.BlockHash + case *SendTx: - tx := s[i].(*SendTx) - if height > tx.Height { + if height > tx.BlockHeight { break } - txheight = tx.Height - txhash = &tx.BlockHash + txBlockHeight = tx.BlockHeight + txBlockHash = &tx.BlockHash } - if height == txheight && *hash == *txhash { + if height == txBlockHeight && *hash == *txBlockHash { endlen = i } } @@ -498,21 +758,34 @@ func (txs *TxStore) Rollback(height int32, hash *btcwire.ShaHash) (modified bool // ReadFrom satisifies the io.ReaderFrom interface. A RecTx is read // in from r with the format: // -// [TxHash (32 bytes), BlockHash (32 bytes), Height (4 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] -// -// Each field is read little endian. +// TxID (32 bytes) +// TimeReceived (8 bytes, little endian) +// BlockHeight (4 bytes, little endian) +// BlockHash (32 bytes) +// BlockIndex (4 bytes, little endian) +// BlockTime (8 bytes, little endian) +// Amt (8 bytes, little endian) +// ReceiverAddr (varies) func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) { datas := []interface{}{ - &tx.TxHash, + &tx.TxID, + &tx.TimeReceived, + &tx.BlockHeight, &tx.BlockHash, - &tx.Height, - &tx.Amt, - &tx.SenderAddr, - &tx.ReceiverAddr, + &tx.BlockIndex, + &tx.BlockTime, + &tx.Amount, + &tx.ReceiverHash, } var read int64 for _, data := range datas { - read, err = binaryRead(r, binary.LittleEndian, data) + switch e := data.(type) { + case io.ReaderFrom: + read, err = e.ReadFrom(r) + default: + read, err = binaryRead(r, binary.LittleEndian, data) + } + if err != nil { return n + read, err } @@ -524,21 +797,34 @@ func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo satisifies the io.WriterTo interface. A RecvTx is written to // w in the format: // -// [TxHash (32 bytes), BlockHash (32 bytes), Height (4 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] -// -// Each field is written little endian. +// TxID (32 bytes) +// TimeReceived (8 bytes, little endian) +// BlockHeight (4 bytes, little endian) +// BlockHash (32 bytes) +// BlockIndex (4 bytes, little endian) +// BlockTime (8 bytes, little endian) +// Amt (8 bytes, little endian) +// ReceiverAddr (varies) func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) { datas := []interface{}{ - &tx.TxHash, + &tx.TxID, + &tx.TimeReceived, + &tx.BlockHeight, &tx.BlockHash, - &tx.Height, - &tx.Amt, - &tx.SenderAddr, - &tx.ReceiverAddr, + &tx.BlockIndex, + &tx.BlockTime, + &tx.Amount, + &tx.ReceiverHash, } var written int64 for _, data := range datas { - written, err = binaryWrite(w, binary.LittleEndian, data) + switch e := data.(type) { + case io.WriterTo: + written, err = e.WriteTo(w) + default: + written, err = binaryWrite(w, binary.LittleEndian, data) + } + if err != nil { return n + written, err } @@ -547,94 +833,161 @@ func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) { return n, nil } +// TxInfo returns a slice of maps that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +func (tx *RecvTx) TxInfo(account string, curheight int32, + net btcwire.BitcoinNet) map[string]interface{} { + + address, err := btcutil.EncodeAddress(tx.ReceiverHash, net) + if err != nil { + address = "Unknown" + } + + txInfo := map[string]interface{}{ + "category": "receive", + "account": account, + "address": address, + "amount": tx.Amount, + "txid": tx.TxID.String(), + "timereceived": tx.TimeReceived, + } + + if tx.BlockHeight != -1 { + txInfo["blockhash"] = tx.BlockHash.String() + txInfo["blockindex"] = tx.BlockIndex + txInfo["blocktime"] = tx.BlockTime + txInfo["confirmations"] = curheight - tx.BlockHeight + 1 + } + + return txInfo +} + // ReadFrom satisifies the io.WriterTo interface. A SendTx is read // from r with the format: // -// [TxHash (32 bytes), Height (4 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...] -// -// Each field is read little endian. +// TxID (32 bytes) +// Time (8 bytes, little endian) +// BlockHeight (4 bytes, little endian) +// BlockHash (32 bytes) +// BlockIndex (4 bytes, little endian) +// BlockTime (8 bytes, little endian) +// Fee (8 bytes, little endian) +// Receivers (varies) func (tx *SendTx) ReadFrom(r io.Reader) (n int64, err error) { - var nReceivers uint32 - datas := []interface{}{ - &tx.TxHash, - &tx.Height, - &tx.Fee, - &tx.SenderAddr, - &nReceivers, - } var read int64 + + datas := []interface{}{ + &tx.TxID, + &tx.Time, + &tx.BlockHeight, + &tx.BlockHash, + &tx.BlockIndex, + &tx.BlockTime, + &tx.Fee, + &tx.Receivers, + } for _, data := range datas { - read, err = binaryRead(r, binary.LittleEndian, data) + switch e := data.(type) { + case io.ReaderFrom: + read, err = e.ReadFrom(r) + default: + read, err = binaryRead(r, binary.LittleEndian, data) + } + if err != nil { return n + read, err } n += read } - if nReceivers == 0 { - // XXX: Is this valid? Entire output is a fee for the miner? - return n, nil - } - tx.ReceiverAddrs = make([]struct { - Addr [ripemd160.Size]byte - Amt uint64 - }, - nReceivers) - for i := uint32(0); i < nReceivers; i++ { - datas := []interface{}{ - &tx.ReceiverAddrs[i].Addr, - &tx.ReceiverAddrs[i].Amt, - } - for _, data := range datas { - read, err = binaryRead(r, binary.LittleEndian, data) - if err != nil { - return n + read, err - } - n += read - } - } return n, nil } // WriteTo satisifies the io.WriterTo interface. A SendTx is written to // w in the format: // -// [TxHash (32 bytes), Height (4 bytes), Fee (8 bytes), SenderAddr (20 bytes), len(ReceiverAddrs) (4 bytes), ReceiverAddrs[Addr (20 bytes), Amt (8 bytes)]...] -// -// Each field is written little endian. +// TxID (32 bytes) +// Time (8 bytes, little endian) +// BlockHeight (4 bytes, little endian) +// BlockHash (32 bytes) +// BlockIndex (4 bytes, little endian) +// BlockTime (8 bytes, little endian) +// Fee (8 bytes, little endian) +// Receivers (varies) func (tx *SendTx) WriteTo(w io.Writer) (n int64, err error) { - nReceivers := uint32(len(tx.ReceiverAddrs)) - if int64(nReceivers) != int64(len(tx.ReceiverAddrs)) { - return n, errors.New("too many receiving addresses") - } - datas := []interface{}{ - &tx.TxHash, - &tx.Height, - &tx.Fee, - &tx.SenderAddr, - nReceivers, - } var written int64 + + datas := []interface{}{ + &tx.TxID, + &tx.Time, + &tx.BlockHeight, + &tx.BlockHash, + &tx.BlockIndex, + &tx.BlockTime, + &tx.Fee, + &tx.Receivers, + } for _, data := range datas { - written, err = binaryWrite(w, binary.LittleEndian, data) + switch e := data.(type) { + case io.WriterTo: + written, err = e.WriteTo(w) + default: + written, err = binaryWrite(w, binary.LittleEndian, data) + } + if err != nil { return n + written, err } n += written } - for i := range tx.ReceiverAddrs { - datas := []interface{}{ - &tx.ReceiverAddrs[i].Addr, - &tx.ReceiverAddrs[i].Amt, - } - for _, data := range datas { - written, err = binaryWrite(w, binary.LittleEndian, data) - if err != nil { - return n + written, err - } - n += written - } - } return n, nil } + +// TxInfo returns a slice of maps that may be marshaled as a JSON array +// of JSON objects for a listtransactions RPC reply. +func (tx *SendTx) TxInfo(account string, curheight int32, + net btcwire.BitcoinNet) []map[string]interface{} { + + reply := make([]map[string]interface{}, len(tx.Receivers)) + + var confirmations int32 + if tx.BlockHeight != -1 { + confirmations = curheight - tx.BlockHeight + 1 + } + + // error is ignored since the length will always be correct. + txID, _ := btcwire.NewShaHash(tx.TxID[:]) + txIDStr := txID.String() + + // error is ignored since the length will always be correct. + blockHash, _ := btcwire.NewShaHash(tx.BlockHash[:]) + blockHashStr := blockHash.String() + + for i, pair := range tx.Receivers { + // EncodeAddress cannot error with these inputs. + address, err := btcutil.EncodeAddress(pair.PubkeyHash, net) + if err != nil { + address = "Unknown" + } + info := map[string]interface{}{ + "account": account, + "address": address, + "category": "send", + "amount": -pair.Amount, + "fee": float64(tx.Fee) / float64(btcutil.SatoshiPerBitcoin), + "confirmations": confirmations, + "txid": txIDStr, + "time": tx.Time, + "timereceived": tx.Time, + } + if tx.BlockHeight != -1 { + info["blockhash"] = blockHashStr + info["blockindex"] = tx.BlockIndex + info["blocktime"] = tx.BlockTime + } + reply[i] = info + } + + return reply +} diff --git a/tx/tx_test.go b/tx/tx_test.go index fdc5b39..b3d353f 100644 --- a/tx/tx_test.go +++ b/tx/tx_test.go @@ -19,7 +19,6 @@ package tx import ( "bytes" "code.google.com/p/go.crypto/ripemd160" - "encoding/binary" "github.com/conformal/btcwire" "github.com/davecgh/go-spew/spew" "io" @@ -29,55 +28,53 @@ import ( var ( recvtx = &RecvTx{ - TxHash: [btcwire.HashSize]byte{ + TxID: [btcwire.HashSize]byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, }, - Amt: 69, - SenderAddr: [ripemd160.Size]byte{ + BlockHash: [btcwire.HashSize]byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, + }, + BlockHeight: 69, + Amount: 69, + ReceiverHash: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, }, - ReceiverAddr: [ripemd160.Size]byte{ - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, - 34, 35, 36, 37, 38, 39, - }, } sendtx = &SendTx{ - TxHash: [btcwire.HashSize]byte{ + TxID: [btcwire.HashSize]byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, }, - SenderAddr: [ripemd160.Size]byte{ + Time: 12345, + BlockHash: [btcwire.HashSize]byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, }, - ReceiverAddrs: []struct { - Addr [ripemd160.Size]byte - Amt uint64 - }{ - struct { - Addr [ripemd160.Size]byte - Amt uint64 - }{ - Amt: 69, - Addr: [ripemd160.Size]byte{ + BlockHeight: 69, + BlockTime: 54321, + BlockIndex: 3, + Receivers: []Pair{ + Pair{ + PubkeyHash: []byte{ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, }, + Amount: 69, }, - struct { - Addr [ripemd160.Size]byte - Amt uint64 - }{ - Amt: 96, - Addr: [ripemd160.Size]byte{ + Pair{ + PubkeyHash: []byte{ 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, }, + Amount: 96, }, }, } @@ -145,24 +142,36 @@ func TestUtxoStoreWriteRead(t *testing.T) { utxo.Subscript = []byte{} utxo.Amt = uint64(i + 3) utxo.Height = int32(i + 4) + utxo.BlockHash = [btcwire.HashSize]byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, + } *store1 = append(*store1, utxo) } bufWriter := &bytes.Buffer{} - n, err := store1.WriteTo(bufWriter) + nWritten, err := store1.WriteTo(bufWriter) if err != nil { t.Error(err) } + if nWritten != int64(bufWriter.Len()) { + t.Errorf("Wrote %v bytes but write buffer has %v bytes.", nWritten, bufWriter.Len()) + } storeBytes := bufWriter.Bytes() + bufReader := bytes.NewBuffer(storeBytes) + if nWritten != int64(bufReader.Len()) { + t.Errorf("Wrote %v bytes but read buffer has %v bytes.", nWritten, bufReader.Len()) + } store2 := new(UtxoStore) - n, err = store2.ReadFrom(bytes.NewBuffer(storeBytes)) + nRead, err := store2.ReadFrom(bufReader) if err != nil { t.Error(err) } - if int(n) != len(storeBytes) { - t.Error("Incorrect number of bytes read.") + if nWritten != nRead { + t.Errorf("Bytes written (%v) does not match bytes read (%v).", nWritten, nRead) } if !reflect.DeepEqual(store1, store2) { @@ -170,15 +179,15 @@ func TestUtxoStoreWriteRead(t *testing.T) { t.Error("Stores do not match.") } - truncatedLen := 100 + truncatedLen := 101 truncatedReadBuf := bytes.NewBuffer(storeBytes[:truncatedLen]) store3 := new(UtxoStore) - n, err = store3.ReadFrom(truncatedReadBuf) + n, err := store3.ReadFrom(truncatedReadBuf) if err != io.EOF { t.Errorf("Expected err = io.EOF reading from truncated buffer, got: %v", err) } if int(n) != truncatedLen { - t.Error("Incorrect number of bytes read from truncated buffer.") + t.Errorf("Incorrect number of bytes (%v) read from truncated buffer (len %v).", n, truncatedLen) } } @@ -189,10 +198,6 @@ func TestRecvTxWriteRead(t *testing.T) { t.Error(err) return } - if int(n) != binary.Size(recvtx) { - t.Error("Writing Tx: Size Mismatch") - return - } txBytes := bufWriter.Bytes() tx := new(RecvTx) @@ -201,10 +206,6 @@ func TestRecvTxWriteRead(t *testing.T) { t.Error(err) return } - if int(n) != binary.Size(tx) { - t.Error("Reading Tx: Size Mismatch") - return - } if !reflect.DeepEqual(recvtx, tx) { t.Error("Txs do not match.") @@ -240,7 +241,8 @@ func TestSendTxWriteRead(t *testing.T) { return } if n1 != n2 { - t.Error("Number of bytes written and read mismatch.") + t.Errorf("Number of bytes written and read mismatch, %d != %d", + n1, n2) return }