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