Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ type Tx struct {
Direction string `json:"direction,omitempty" ts_doc:"Transaction direction relative to the queried account, when returned by account summary endpoints."`
AddressValueInSat *Amount `json:"addressValueIn,omitempty" ts_doc:"Total input value belonging to the queried account, when returned by account summary endpoints."`
AddressValueOutSat *Amount `json:"addressValueOut,omitempty" ts_doc:"Total output value belonging to the queried account, when returned by account summary endpoints."`
AccountOwnsAllInputs *bool `json:"accountOwnsAllInputs,omitempty" ts_doc:"Whether every input has a known positive native value and belongs to the queried account. Present only in compact account summaries."`
AccountAssetTransfers []TokenTransfer `json:"accountAssetTransfers,omitempty" ts_doc:"Syscoin SPT transfers summarized relative to the queried account."`
// SYSCOIN: SPT transaction type and optional decoded memo.
TokenType *bchain.TokenType `json:"tokenType,omitempty" ts_doc:"Syscoin SPT transaction type."`
Expand Down
5 changes: 5 additions & 0 deletions api/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1599,8 +1599,12 @@ func (w *Worker) summarizeTxForAccount(tx *Tx, addrDescs map[string]struct{}) *T
}
var valueIn, valueOut big.Int
assetTransfers := make(map[string]*accountAssetTransferSummary)
accountOwnsAllInputs := len(tx.Vin) > 0
for i := range tx.Vin {
vin := &tx.Vin[i]
if len(vin.AddrDesc) == 0 || vin.ValueSat == nil || (*big.Int)(vin.ValueSat).Sign() <= 0 || !addrDescInSet(vin.AddrDesc, addrDescs) {
accountOwnsAllInputs = false
}
if !addrDescInSet(vin.AddrDesc, addrDescs) {
continue
}
Expand Down Expand Up @@ -1641,6 +1645,7 @@ func (w *Worker) summarizeTxForAccount(tx *Tx, addrDescs map[string]struct{}) *T
Direction: direction,
AddressValueInSat: (*Amount)(&valueIn),
AddressValueOutSat: (*Amount)(&valueOut),
AccountOwnsAllInputs: &accountOwnsAllInputs,
AccountAssetTransfers: accountAssetTransfers,
TokenType: tx.TokenType,
Memo: tx.Memo,
Expand Down
132 changes: 132 additions & 0 deletions api/worker_txsummary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//go:build unittest

package api

import (
"encoding/json"
"math/big"
"testing"

"github.com/trezor/blockbook/bchain"
)

func TestSummarizeTxForAccountInputOwnership(t *testing.T) {
owned := bchain.AddressDescriptor{1}
ownedChange := bchain.AddressDescriptor{2}
foreign := bchain.AddressDescriptor{3}
accounts := map[string]struct{}{string(owned): {}, string(ownedChange): {}}
amount := func(value int64) *Amount { return (*Amount)(big.NewInt(value)) }
tests := []struct {
name string
vin []Vin
want bool
}{
{"one owned input", []Vin{{AddrDesc: owned, ValueSat: amount(100000226)}}, true},
{"multiple owned inputs", []Vin{{AddrDesc: owned, ValueSat: amount(60000000)}, {AddrDesc: ownedChange, ValueSat: amount(40000226)}}, true},
{"mixed ownership", []Vin{{AddrDesc: owned, ValueSat: amount(60000000)}, {AddrDesc: foreign, ValueSat: amount(40000226)}}, false},
{"only foreign inputs", []Vin{{AddrDesc: foreign, ValueSat: amount(100000226)}}, false},
{"zero owned input", []Vin{{AddrDesc: owned, ValueSat: amount(0)}}, false},
{"foreign zero input alongside owned funding", []Vin{{AddrDesc: owned, ValueSat: amount(100000226)}, {AddrDesc: foreign, ValueSat: amount(0)}}, false},
{"unknown value", []Vin{{AddrDesc: owned}}, false},
{"negative value", []Vin{{AddrDesc: owned, ValueSat: amount(-1)}}, false},
{"missing descriptor", []Vin{{ValueSat: amount(100000226)}}, false},
{"empty inputs", []Vin{}, false},
{"nil inputs", nil, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tx := &Tx{Txid: "tx", Vin: test.vin, Vout: []Vout{}, FeesSat: amount(226)}
summary := (&Worker{}).summarizeTxForAccount(tx, accounts)
if summary.AccountOwnsAllInputs == nil || *summary.AccountOwnsAllInputs != test.want {
t.Fatalf("accountOwnsAllInputs = %v, want explicit %v", summary.AccountOwnsAllInputs, test.want)
}
if tx.AccountOwnsAllInputs != nil {
t.Fatal("summary changed the materialized transaction")
}
if summary.Vin == nil || len(summary.Vin) != 0 || summary.Vout == nil || len(summary.Vout) != 0 {
t.Fatal("summary must retain empty input/output arrays")
}
})
}
if summary := (&Worker{}).summarizeTxForAccount(nil, accounts); summary != nil {
t.Fatal("nil transaction must still produce nil summary")
}
for _, descriptors := range []map[string]struct{}{nil, {}, {"": {}}} {
tx := &Tx{Vin: []Vin{{AddrDesc: owned, ValueSat: amount(100000226)}}}
summary := (&Worker{}).summarizeTxForAccount(tx, descriptors)
if summary.AccountOwnsAllInputs == nil || *summary.AccountOwnsAllInputs {
t.Fatal("missing account descriptors must not establish ownership")
}
}
missingDescriptor := &Tx{Vin: []Vin{{ValueSat: amount(100000226)}}}
summary := (&Worker{}).summarizeTxForAccount(missingDescriptor, map[string]struct{}{"": {}})
if summary.AccountOwnsAllInputs == nil || *summary.AccountOwnsAllInputs {
t.Fatal("an empty descriptor must not match an empty account descriptor")
}
}

func TestSummarizeTxForAccountOwnershipPreservesAmounts(t *testing.T) {
owned := bchain.AddressDescriptor{1}
foreign := bchain.AddressDescriptor{2}
accounts := map[string]struct{}{string(owned): {}}
amount := func(value int64) *Amount { return (*Amount)(big.NewInt(value)) }
for _, self := range []bool{false, true} {
name := "external spend"
accountOut := int64(30000000)
outputs := []Vout{{AddrDesc: owned, ValueSat: amount(accountOut)}, {AddrDesc: foreign, ValueSat: amount(70000000)}}
if self {
name = "self spend"
accountOut = 100000000
outputs = []Vout{{AddrDesc: owned, ValueSat: amount(accountOut)}}
}
t.Run(name, func(t *testing.T) {
tx := &Tx{
Txid: "tx", Vin: []Vin{{AddrDesc: owned, ValueSat: amount(100000226)}}, Vout: outputs,
ValueInSat: amount(100000226), ValueOutSat: amount(100000000), FeesSat: amount(226),
Confirmations: 1, Blockheight: 100, Blocktime: 1700000000,
}
summary := (&Worker{}).summarizeTxForAccount(tx, accounts)
if summary.AccountOwnsAllInputs == nil || !*summary.AccountOwnsAllInputs {
t.Fatal("expected explicit complete input ownership")
}
if summary.AddressValueInSat.String() != "100000226" || summary.ValueInSat.String() != "100000226" || summary.AddressValueOutSat.AsInt64() != accountOut || summary.ValueOutSat.AsInt64() != accountOut {
t.Fatal("account totals and legacy aliases changed")
}
if summary.FeesSat != tx.FeesSat || summary.Confirmations != tx.Confirmations || summary.Blockheight != tx.Blockheight || summary.Blocktime != tx.Blocktime {
t.Fatal("fees or confirmation metadata changed")
}
if tx.ValueOutSat.String() != "100000000" || tx.AccountOwnsAllInputs != nil {
t.Fatal("materialized transaction was modified")
}
})
}
}

func TestTxAccountOwnsAllInputsJSON(t *testing.T) {
for _, test := range []struct {
name string
value *bool
want interface{}
}{
{"not a summary", nil, nil},
{"owned summary", boolPointer(true), true},
{"unproven summary", boolPointer(false), false},
} {
t.Run(test.name, func(t *testing.T) {
encoded, err := json.Marshal(&Tx{AccountOwnsAllInputs: test.value})
if err != nil {
t.Fatal(err)
}
var fields map[string]interface{}
if err := json.Unmarshal(encoded, &fields); err != nil {
t.Fatal(err)
}
value, present := fields["accountOwnsAllInputs"]
if present != (test.value != nil) || value != test.want {
t.Fatalf("accountOwnsAllInputs = %v (present %v), want %v", value, present, test.want)
}
})
}
}

func boolPointer(value bool) *bool { return &value }
2 changes: 2 additions & 0 deletions blockbook-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ export interface Tx {
addressValueIn?: string;
/** Total output value belonging to the queried account, when returned by account summary endpoints. */
addressValueOut?: string;
/** Whether every input has a known positive native value and belongs to the queried account. Present only in compact account summaries. */
accountOwnsAllInputs?: boolean;
/** Syscoin SPT transfers summarized relative to the queried account. */
accountAssetTransfers?: TokenTransfer[];
/** Syscoin SPT transaction type. */
Expand Down
3 changes: 3 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2137,6 +2137,9 @@ components:
allOf:
- $ref: "#/components/schemas/AmountString"
description: Total output value belonging to the queried account, when returned by account summary endpoints.
accountOwnsAllInputs:
type: boolean
description: Whether every input has a known positive native value and belongs to the queried account. Present only in compact account summaries.
accountAssetTransfers:
type: array
description: Syscoin SPT transfers summarized relative to the queried account.
Expand Down
Loading