Add support for listtransactions RPC request.

This change adds support for the listtransactions RPC command.  To
properly reply to this command, additonal information about received
transactions was added, and is now saved in an account's tx.bin file.
Additionally, when sending a transaction, a *tx.SendTx is now saved to
the Tx store, and is included in listtransactions replies under the
"send" category.

WARNING: All account's tx.bin and utxo.bin files should be removed
before running with this change, or else the files may not be read
correctly.  Removing tx.bin is not an issue as it was not being used
before, and was being saved with incorrect data.  Removing utxo.bin is
not an issue as it will just trigger a rescan on next start.  File
format versions are now included in both files, so automatic updates
from previous file formats will be possible with future changes.

Fixes .
This commit is contained in:
Josh Rickmar 2013-11-22 13:42:25 -05:00
parent a246fc91d6
commit 413f23ea18
7 changed files with 794 additions and 273 deletions

View file

@ -27,6 +27,7 @@ import (
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
"sync" "sync"
"time"
) )
var accounts = NewAccountStore() var accounts = NewAccountStore()
@ -451,36 +452,44 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo
} }
return false return false
} }
sender, ok := v["sender"].(string)
if !ok {
log.Error("Tx Handler: Unspecified sender.")
return false
}
receiver, ok := v["receiver"].(string) receiver, ok := v["receiver"].(string)
if !ok { if !ok {
log.Error("Tx Handler: Unspecified receiver.") log.Error("Tx Handler: Unspecified receiver.")
return false return false
} }
blockhashBE, ok := v["blockhash"].(string)
if !ok {
log.Error("Tx Handler: Unspecified block hash.")
return false
}
height, ok := v["height"].(float64) height, ok := v["height"].(float64)
if !ok { if !ok {
log.Error("Tx Handler: Unspecified height.") log.Error("Tx Handler: Unspecified height.")
return false 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 { if !ok {
log.Error("Tx Handler: Unspecified transaction hash.") log.Error("Tx Handler: Unspecified transaction hash.")
return false return false
} }
index, ok := v["index"].(float64) ftxOutIndex, ok := v["txoutindex"].(float64)
if !ok { if !ok {
log.Error("Tx Handler: Unspecified transaction index.") log.Error("Tx Handler: Unspecified transaction output index.")
return false return false
} }
txOutIndex := int32(ftxOutIndex)
amt, ok := v["amount"].(float64) amt, ok := v["amount"].(float64)
if !ok { if !ok {
log.Error("Tx Handler: Unspecified amount.") 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 // btcd sends the block and tx hashes as BE strings. Convert both
// to a LE ShaHash. // to a LE ShaHash.
blockhash, err := btcwire.NewShaHashFromStr(blockhashBE) blockHash, err := btcwire.NewShaHashFromStr(blockHashBE)
if err != nil { if err != nil {
log.Errorf("Tx Handler: Block hash string cannot be parsed: %v", err) log.Errorf("Tx Handler: Block hash string cannot be parsed: %v", err)
return false return false
} }
txhash, err := btcwire.NewShaHashFromStr(txhashBE) txID, err := btcwire.NewShaHashFromStr(txhashBE)
if err != nil { if err != nil {
log.Errorf("Tx Handler: Tx hash string cannot be parsed: %v", err) log.Errorf("Tx Handler: Tx hash string cannot be parsed: %v", err)
return false return false
} }
// TODO(jrick): btcd does not find the sender yet.
senderHash, _, _ := btcutil.DecodeAddress(sender)
receiverHash, _, err := btcutil.DecodeAddress(receiver) receiverHash, _, err := btcutil.DecodeAddress(receiver)
if err != nil { if err != nil {
log.Errorf("Tx Handler: receiver address can not be decoded: %v", err) 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. // Add to TxStore.
t := &tx.RecvTx{ 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() a.TxStore.Lock()
txs := a.TxStore.s txs := a.TxStore.s
@ -532,14 +542,13 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo
a.TxStore.dirty = true a.TxStore.dirty = true
a.TxStore.Unlock() a.TxStore.Unlock()
// Add to UtxoStore if unspent.
if !spent { if !spent {
// First, iterate through all stored utxos. If an unconfirmed utxo // First, iterate through all stored utxos. If an unconfirmed utxo
// (not present in a block) has the same outpoint as this utxo, // (not present in a block) has the same outpoint as this utxo,
// update the block height and hash. // update the block height and hash.
a.UtxoStore.RLock() a.UtxoStore.RLock()
for _, u := range a.UtxoStore.s { 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, // Found a either a duplicate, or a change UTXO. If not change,
// ignore it. // ignore it.
a.UtxoStore.RUnlock() a.UtxoStore.RUnlock()
@ -548,7 +557,7 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo
} }
a.UtxoStore.Lock() a.UtxoStore.Lock()
copy(u.BlockHash[:], blockhash[:]) copy(u.BlockHash[:], blockHash[:])
u.Height = int32(height) u.Height = int32(height)
a.UtxoStore.dirty = true a.UtxoStore.dirty = true
a.UtxoStore.Unlock() a.UtxoStore.Unlock()
@ -566,10 +575,10 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo
Height: int32(height), Height: int32(height),
Subscript: pkscript, Subscript: pkscript,
} }
copy(u.Out.Hash[:], txhash[:]) copy(u.Out.Hash[:], txID[:])
u.Out.Index = uint32(index) u.Out.Index = uint32(txOutIndex)
copy(u.AddrHash[:], receiverHash) copy(u.AddrHash[:], receiverHash)
copy(u.BlockHash[:], blockhash[:]) copy(u.BlockHash[:], blockHash[:])
a.UtxoStore.Lock() a.UtxoStore.Lock()
a.UtxoStore.s = append(a.UtxoStore.s, u) a.UtxoStore.s = append(a.UtxoStore.s, u)
a.UtxoStore.dirty = true a.UtxoStore.dirty = true

42
cmd.go
View file

@ -103,6 +103,8 @@ func checkCreateAccountDir(path string) error {
// Wallets opened from this function are not set to track against a // Wallets opened from this function are not set to track against a
// btcd connection. // btcd connection.
func OpenAccount(cfg *config, account string) (*Account, error) { func OpenAccount(cfg *config, account string) (*Account, error) {
var finalErr error
adir := accountdir(cfg, account) adir := accountdir(cfg, account)
if err := checkCreateAccountDir(adir); err != nil { if err := checkCreateAccountDir(adir); err != nil {
return nil, err return nil, err
@ -134,6 +136,25 @@ func OpenAccount(cfg *config, account string) (*Account, error) {
name: account, 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 // Read utxo file. If this fails, return a ErrNoUtxos error so a
// rescan can be done since the wallet creation block. // rescan can be done since the wallet creation block.
var utxos tx.UtxoStore var utxos tx.UtxoStore
@ -144,25 +165,12 @@ func OpenAccount(cfg *config, account string) (*Account, error) {
defer utxofile.Close() defer utxofile.Close()
if _, err = utxos.ReadFrom(utxofile); err != nil { if _, err = utxos.ReadFrom(utxofile); err != nil {
log.Errorf("cannot read utxo file: %s", err) 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 return a, finalErr
// 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
} }
// GetCurBlock returns the blockchain height and SHA hash of the most // GetCurBlock returns the blockchain height and SHA hash of the most

126
cmdmgr.go
View file

@ -22,6 +22,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
@ -45,6 +46,7 @@ var rpcHandlers = map[string]cmdHandler{
"getnewaddress": GetNewAddress, "getnewaddress": GetNewAddress,
"importprivkey": ImportPrivKey, "importprivkey": ImportPrivKey,
"listaccounts": ListAccounts, "listaccounts": ListAccounts,
"listtransactions": ListTransactions,
"sendfrom": SendFrom, "sendfrom": SendFrom,
"sendmany": SendMany, "sendmany": SendMany,
"settxfee": SetTxFee, "settxfee": SetTxFee,
@ -458,6 +460,59 @@ func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) {
ReplySuccess(frontend, cmd.Id(), pairs) 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 // SendFrom creates a new transaction spending unspent transaction
// outputs for a wallet to another payment address. Leftover inputs // outputs for a wallet to another payment address. Leftover inputs
// not sent to the payment address or a fee for the miner are sent // 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, 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 { txInfo *CreatedTx) bool {
if err != nil { if e != nil {
ReplyError(frontend, icmd.Id(), err) ReplyError(frontend, icmd.Id(), e)
return true 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. // Remove previous unspent outputs now spent by the tx.
a.UtxoStore.Lock() a.UtxoStore.Lock()
modified := a.UtxoStore.s.Remove(txInfo.inputs) modified := a.UtxoStore.s.Remove(txInfo.inputs)
a.UtxoStore.dirty = a.UtxoStore.dirty || modified
// Add unconfirmed change utxo (if any) to UtxoStore. // Add unconfirmed change utxo (if any) to UtxoStore.
if txInfo.changeUtxo != nil { if txInfo.changeUtxo != nil {
a.UtxoStore.s = append(a.UtxoStore.s, txInfo.changeUtxo) a.UtxoStore.s = append(a.UtxoStore.s, txInfo.changeUtxo)
a.ReqSpentUtxoNtfn(txInfo.changeUtxo) a.ReqSpentUtxoNtfn(txInfo.changeUtxo)
modified = true
}
if modified {
a.UtxoStore.dirty = true 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 // btcd cannot be trusted to successfully relay the tx to the
// Bitcoin network. Even if this succeeds, the rawtx must be // 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 // Add hex string of raw tx to sent tx pool. If btcd disconnects
// and is reconnected, these txs are resent. // and is reconnected, these txs are resent.
UnminedTxs.Lock() UnminedTxs.Lock()
UnminedTxs.m[TXID(result.(string))] = txInfo UnminedTxs.m[TXID(*txID)] = txInfo
UnminedTxs.Unlock() UnminedTxs.Unlock()
log.Debugf("successfully sent transaction %v", result) log.Debugf("successfully sent transaction %v", result)

View file

@ -61,19 +61,22 @@ var TxFee = struct {
// for change (if any). // for change (if any).
type CreatedTx struct { type CreatedTx struct {
rawTx []byte rawTx []byte
time time.Time
inputs []*tx.Utxo inputs []*tx.Utxo
outputs []tx.Pair
btcout int64
fee int64
changeAddr string changeAddr string
changeUtxo *tx.Utxo changeUtxo *tx.Utxo
} }
// TXID is a transaction hash identifying a transaction. // 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 // UnminedTXs holds a map of transaction IDs as keys mapping to a
// hex string of a raw transaction. If sending a raw transaction // CreatedTx structure. If sending a raw transaction succeeds, the
// succeeds, the tx is added to this map and checked again after each // tx is added to this map and checked again after each new block.
// new block. If the new block contains a tx, it is removed from // If the new block contains a tx, it is removed from this map.
// this map. Otherwise, btcwallet will resend the tx to btcd.
var UnminedTxs = struct { var UnminedTxs = struct {
sync.Mutex sync.Mutex
m map[TXID]*CreatedTx m map[TXID]*CreatedTx
@ -179,6 +182,10 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr
return nil, err 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. // Add outputs to new tx.
for addr, amt := range pairs { for addr, amt := range pairs {
addr160, _, err := btcutil.DecodeAddress(addr) 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) txout := btcwire.NewTxOut(int64(amt), pkScript)
msgtx.AddTxOut(txout) 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 // 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, Subscript: pkScript,
} }
copy(changeUtxo.AddrHash[:], changeAddrHash) 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. // 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) buf := new(bytes.Buffer)
msgtx.BtcEncode(buf, btcwire.ProtocolVersion) 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
} }

View file

@ -17,6 +17,7 @@
package main package main
import ( import (
"bytes"
"code.google.com/p/go.net/websocket" "code.google.com/p/go.net/websocket"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
@ -25,6 +26,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
@ -527,15 +529,47 @@ func NtfnTxMined(n btcws.Notification) {
log.Errorf("%v handler: unexpected type", n.Id()) log.Errorf("%v handler: unexpected type", n.Id())
return return
} }
txid, err := btcwire.NewShaHashFromStr(tmn.TxID) txid, err := btcwire.NewShaHashFromStr(tmn.TxID)
if err != nil { if err != nil {
log.Errorf("%v handler: invalid hash string", n.Id()) log.Errorf("%v handler: invalid hash string", n.Id())
return 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. // Remove mined transaction from pool.
UnminedTxs.Lock() UnminedTxs.Lock()
delete(UnminedTxs.m, TXID(txid[:])) delete(UnminedTxs.m, TXID(*txid))
UnminedTxs.Unlock() UnminedTxs.Unlock()
} }

667
tx/tx.go
View file

@ -22,14 +22,31 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/conformal/btcutil"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"io" "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. // Byte headers prepending received and sent serialized transactions.
const ( const (
RecvTxHeader byte = iota recvTxHeader byte = iota
SendTxHeader sendTxHeader
)
// File format versions.
const (
utxoFileVersion uint32 = 0
txFileVersion uint32 = 0
) )
// UtxoStore is a type used for holding all Utxo structures for all // 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. // TxStore is a slice holding RecvTx and SendTx pointers.
type TxStore []interface{} 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 // RecvTx is a type storing information about a transaction that was
// received by an address in a wallet. // received by an address in a wallet.
type RecvTx struct { type RecvTx struct {
TxHash btcwire.ShaHash TxID btcwire.ShaHash
TimeReceived int64
BlockHeight int32
BlockHash btcwire.ShaHash BlockHash btcwire.ShaHash
Height int32 BlockIndex int32
Amt uint64 // Measured in Satoshis BlockTime int64
SenderAddr [ripemd160.Size]byte Amount int64 // Measured in Satoshis
ReceiverAddr [ripemd160.Size]byte 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 // SendTx is a type storing information about a transaction that was
// sent by an address in a wallet. // sent by an address in a wallet.
type SendTx struct { type SendTx struct {
TxHash btcwire.ShaHash TxID btcwire.ShaHash
BlockHash btcwire.ShaHash Time int64
Height int32 BlockHeight int32
Fee uint64 // Measured in Satoshis BlockHash btcwire.ShaHash
SenderAddr [ripemd160.Size]byte BlockIndex int32
ReceiverAddrs []struct { BlockTime int64
Addr [ripemd160.Size]byte Fee int64 // Measured in Satoshis
Amt uint64 // Measured in Satoshis Receivers Pairs
}
} }
// We want to use binaryRead and binaryWrite instead of binary.Read // 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 // 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 // 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. // 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 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 { for {
// Read Utxo // Read Utxo
utxo := new(Utxo) utxo := new(Utxo)
read, err = utxo.ReadFrom(r) n, err := utxo.ReadFrom(r)
if err != nil { if err != nil {
if read == 0 && err == io.EOF { if n == 0 && err == io.EOF {
return n, nil err = nil
} }
return n + read, err return read + n, err
} }
n += read read += n
*u = append(*u, utxo) *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 // WriteTo satisifies the io.WriterTo interface. Each Utxo is written
// to w, prepended by a single byte header to distinguish between // to w, prepended by a single byte header to distinguish between
// confirmed and unconfirmed outputs. // 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 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 { for _, utxo := range *u {
// Write Utxo // Write Utxo
written, err = utxo.WriteTo(w) n, err := utxo.WriteTo(w)
if err != nil { 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 // 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 // ReadFrom satisifies the io.ReaderFrom interface. A Utxo is read
// from r with the format: // from r with the format:
// //
// [AddrHash (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (4 bytes), BlockHash (32 bytes)] // AddrHash (20 bytes)
// // Out (36 bytes)
// Each field is read little endian. // 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) { func (u *Utxo) ReadFrom(r io.Reader) (n int64, err error) {
datas := []interface{}{ datas := []interface{}{
&u.AddrHash, &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 // WriteTo satisifies the io.WriterTo interface. A Utxo is written to
// w in the format: // w in the format:
// //
// [AddrHash (20 bytes), Out (36 bytes), Subscript (varies), Amt (8 bytes), Height (4 bytes), BlockHash (32 bytes)] // AddrHash (20 bytes)
// // Out (36 bytes)
// Each field is written little endian. // 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) { func (u *Utxo) WriteTo(w io.Writer) (n int64, err error) {
datas := []interface{}{ datas := []interface{}{
&u.AddrHash, &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 // ReadFrom satisifies the io.ReaderFrom interface. A PkScript is read
// from r with the format: // from r with the format:
// //
// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)] // Length (4 byte, little endian)
// // ScriptBytes (Length bytes)
// Length is read little endian.
func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) { func (s *PkScript) ReadFrom(r io.Reader) (n int64, err error) {
var scriptlen uint32 var scriptlen uint32
var read int64 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 // WriteTo satisifies the io.WriterTo interface. A PkScript is written
// to w in the format: // to w in the format:
// //
// [Length (4 byte unsigned integer), ScriptBytes (Length bytes)] // Length (4 byte, little endian)
// // ScriptBytes (Length bytes)
// Length is written little endian.
func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) { func (s *PkScript) WriteTo(w io.Writer) (n int64, err error) {
var written int64 var written int64
written, err = binaryWrite(w, binary.LittleEndian, uint32(len(*s))) 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 // ReadFrom satisifies the io.ReaderFrom interface. A TxStore is read
// in from r with the format: // in from r with the format:
// //
// [[TxHeader (1 byte), Tx (varies in size)]...] // Version (4 bytes, little endian)
func (txs *TxStore) ReadFrom(r io.Reader) (n int64, err error) { // [(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{}{} store := []interface{}{}
defer func() { defer func() {
*txs = store *txs = store
}() }()
var read int64
for { for {
// Read header // Read header
var header byte var header byte
read, err = binaryRead(r, binary.LittleEndian, &header) n, err := binaryRead(r, binary.LittleEndian, &header)
if err != nil { if err != nil {
// io.EOF is not an error here. // io.EOF is not an error here.
if err == io.EOF { 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 var tx io.ReaderFrom
switch header { switch header {
case RecvTxHeader: case recvTxHeader:
tx = new(RecvTx) tx = new(RecvTx)
case SendTxHeader:
case sendTxHeader:
tx = new(SendTx) tx = new(SendTx)
default: default:
return n, fmt.Errorf("unknown Tx header") return n, fmt.Errorf("unknown Tx header")
} }
// Read tx // Read tx
read, err = tx.ReadFrom(r) n, err = tx.ReadFrom(r)
if err != nil { if err != nil {
return n + read, err return read + n, err
} }
n += read read += n
store = append(store, tx) 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 // WriteTo satisifies the io.WriterTo interface. A TxStore is written
// to w in the format: // to w in the format:
// //
// [[TxHeader (1 byte), Tx (varies in size)]...] // Version (4 bytes, little endian)
func (txs *TxStore) WriteTo(w io.Writer) (n int64, err error) { // [(TxHeader (1 byte), Tx (varies in size))...]
store := ([]interface{})(*txs) func (txs *TxStore) WriteTo(w io.Writer) (int64, error) {
var written int64 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 { for _, tx := range store {
switch tx.(type) { switch tx.(type) {
case *RecvTx: case *RecvTx:
written, err = binaryWrite(w, binary.LittleEndian, RecvTxHeader) n, err := binaryWrite(w, binary.LittleEndian, recvTxHeader)
if err != nil { if err != nil {
return n + written, err return written + n, err
} }
n += written written += n
case *SendTx: case *SendTx:
written, err = binaryWrite(w, binary.LittleEndian, SendTxHeader) n, err := binaryWrite(w, binary.LittleEndian, sendTxHeader)
if err != nil { if err != nil {
return n + written, err return written + n, err
} }
n += written written += n
default: default:
return n, fmt.Errorf("unknown type in TxStore") return written, fmt.Errorf("unknown type in TxStore")
} }
wt := tx.(io.WriterTo) wt := tx.(io.WriterTo)
written, err = wt.WriteTo(w) n, err := wt.WriteTo(w)
if err != nil { 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 // 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-- { for i := len(s) - 1; i >= 0; i-- {
var txheight int32 var txBlockHeight int32
var txhash *btcwire.ShaHash var txBlockHash *btcwire.ShaHash
switch s[i].(type) { switch tx := s[i].(type) {
case *RecvTx: case *RecvTx:
tx := s[i].(*RecvTx) if height > tx.BlockHeight {
if height > tx.Height {
break break
} }
txheight = tx.Height txBlockHeight = tx.BlockHeight
txhash = &tx.BlockHash txBlockHash = &tx.BlockHash
case *SendTx: case *SendTx:
tx := s[i].(*SendTx) if height > tx.BlockHeight {
if height > tx.Height {
break break
} }
txheight = tx.Height txBlockHeight = tx.BlockHeight
txhash = &tx.BlockHash txBlockHash = &tx.BlockHash
} }
if height == txheight && *hash == *txhash { if height == txBlockHeight && *hash == *txBlockHash {
endlen = i 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 // ReadFrom satisifies the io.ReaderFrom interface. A RecTx is read
// in from r with the format: // in from r with the format:
// //
// [TxHash (32 bytes), BlockHash (32 bytes), Height (4 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] // TxID (32 bytes)
// // TimeReceived (8 bytes, little endian)
// Each field is read 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) { func (tx *RecvTx) ReadFrom(r io.Reader) (n int64, err error) {
datas := []interface{}{ datas := []interface{}{
&tx.TxHash, &tx.TxID,
&tx.TimeReceived,
&tx.BlockHeight,
&tx.BlockHash, &tx.BlockHash,
&tx.Height, &tx.BlockIndex,
&tx.Amt, &tx.BlockTime,
&tx.SenderAddr, &tx.Amount,
&tx.ReceiverAddr, &tx.ReceiverHash,
} }
var read int64 var read int64
for _, data := range datas { 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 { if err != nil {
return n + read, err 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 // WriteTo satisifies the io.WriterTo interface. A RecvTx is written to
// w in the format: // w in the format:
// //
// [TxHash (32 bytes), BlockHash (32 bytes), Height (4 bytes), Amt (8 bytes), SenderAddr (20 bytes), ReceiverAddr (20 bytes)] // TxID (32 bytes)
// // TimeReceived (8 bytes, little endian)
// Each field is written 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) { func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) {
datas := []interface{}{ datas := []interface{}{
&tx.TxHash, &tx.TxID,
&tx.TimeReceived,
&tx.BlockHeight,
&tx.BlockHash, &tx.BlockHash,
&tx.Height, &tx.BlockIndex,
&tx.Amt, &tx.BlockTime,
&tx.SenderAddr, &tx.Amount,
&tx.ReceiverAddr, &tx.ReceiverHash,
} }
var written int64 var written int64
for _, data := range datas { 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 { if err != nil {
return n + written, err return n + written, err
} }
@ -547,94 +833,161 @@ func (tx *RecvTx) WriteTo(w io.Writer) (n int64, err error) {
return n, nil 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 // ReadFrom satisifies the io.WriterTo interface. A SendTx is read
// from r with the format: // 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)]...] // TxID (32 bytes)
// // Time (8 bytes, little endian)
// Each field is read 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) { 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 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 { 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 { if err != nil {
return n + read, err return n + read, err
} }
n += read 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 return n, nil
} }
// WriteTo satisifies the io.WriterTo interface. A SendTx is written to // WriteTo satisifies the io.WriterTo interface. A SendTx is written to
// w in the format: // 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)]...] // TxID (32 bytes)
// // Time (8 bytes, little endian)
// Each field is written 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) { 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 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 { 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 { if err != nil {
return n + written, err return n + written, err
} }
n += written 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 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
}

View file

@ -19,7 +19,6 @@ package tx
import ( import (
"bytes" "bytes"
"code.google.com/p/go.crypto/ripemd160" "code.google.com/p/go.crypto/ripemd160"
"encoding/binary"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"io" "io"
@ -29,55 +28,53 @@ import (
var ( var (
recvtx = &RecvTx{ 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, 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, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 30, 31,
}, },
Amt: 69, BlockHash: [btcwire.HashSize]byte{
SenderAddr: [ripemd160.Size]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, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 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{ 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, 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, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 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, 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 { BlockHeight: 69,
Addr [ripemd160.Size]byte BlockTime: 54321,
Amt uint64 BlockIndex: 3,
}{ Receivers: []Pair{
struct { Pair{
Addr [ripemd160.Size]byte PubkeyHash: []byte{
Amt uint64
}{
Amt: 69,
Addr: [ripemd160.Size]byte{
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 38, 39, 34, 35, 36, 37, 38, 39,
}, },
Amount: 69,
}, },
struct { Pair{
Addr [ripemd160.Size]byte PubkeyHash: []byte{
Amt uint64
}{
Amt: 96,
Addr: [ripemd160.Size]byte{
40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
54, 55, 56, 57, 58, 59, 54, 55, 56, 57, 58, 59,
}, },
Amount: 96,
}, },
}, },
} }
@ -145,24 +142,36 @@ func TestUtxoStoreWriteRead(t *testing.T) {
utxo.Subscript = []byte{} utxo.Subscript = []byte{}
utxo.Amt = uint64(i + 3) utxo.Amt = uint64(i + 3)
utxo.Height = int32(i + 4) 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) *store1 = append(*store1, utxo)
} }
bufWriter := &bytes.Buffer{} bufWriter := &bytes.Buffer{}
n, err := store1.WriteTo(bufWriter) nWritten, err := store1.WriteTo(bufWriter)
if err != nil { if err != nil {
t.Error(err) 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() 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) store2 := new(UtxoStore)
n, err = store2.ReadFrom(bytes.NewBuffer(storeBytes)) nRead, err := store2.ReadFrom(bufReader)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if int(n) != len(storeBytes) { if nWritten != nRead {
t.Error("Incorrect number of bytes read.") t.Errorf("Bytes written (%v) does not match bytes read (%v).", nWritten, nRead)
} }
if !reflect.DeepEqual(store1, store2) { if !reflect.DeepEqual(store1, store2) {
@ -170,15 +179,15 @@ func TestUtxoStoreWriteRead(t *testing.T) {
t.Error("Stores do not match.") t.Error("Stores do not match.")
} }
truncatedLen := 100 truncatedLen := 101
truncatedReadBuf := bytes.NewBuffer(storeBytes[:truncatedLen]) truncatedReadBuf := bytes.NewBuffer(storeBytes[:truncatedLen])
store3 := new(UtxoStore) store3 := new(UtxoStore)
n, err = store3.ReadFrom(truncatedReadBuf) n, err := store3.ReadFrom(truncatedReadBuf)
if err != io.EOF { if err != io.EOF {
t.Errorf("Expected err = io.EOF reading from truncated buffer, got: %v", err) t.Errorf("Expected err = io.EOF reading from truncated buffer, got: %v", err)
} }
if int(n) != truncatedLen { 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) t.Error(err)
return return
} }
if int(n) != binary.Size(recvtx) {
t.Error("Writing Tx: Size Mismatch")
return
}
txBytes := bufWriter.Bytes() txBytes := bufWriter.Bytes()
tx := new(RecvTx) tx := new(RecvTx)
@ -201,10 +206,6 @@ func TestRecvTxWriteRead(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if int(n) != binary.Size(tx) {
t.Error("Reading Tx: Size Mismatch")
return
}
if !reflect.DeepEqual(recvtx, tx) { if !reflect.DeepEqual(recvtx, tx) {
t.Error("Txs do not match.") t.Error("Txs do not match.")
@ -240,7 +241,8 @@ func TestSendTxWriteRead(t *testing.T) {
return return
} }
if n1 != n2 { 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 return
} }