diff --git a/api/types.go b/api/types.go index e9725775..4fd1e851 100644 --- a/api/types.go +++ b/api/types.go @@ -301,12 +301,13 @@ func (a Utxos) Less(i, j int) bool { // BalanceHistory contains info about one point in time of balance history type BalanceHistory struct { - Time uint32 `json:"time"` - Txs uint32 `json:"txs"` - ReceivedSat *Amount `json:"received"` - SentSat *Amount `json:"sent"` - FiatRates map[string]float64 `json:"rates,omitempty"` - Txid string `json:"txid,omitempty"` + Time uint32 `json:"time"` + Txs uint32 `json:"txs"` + ReceivedSat *Amount `json:"received"` + SentSat *Amount `json:"sent"` + SentToSelfSat *Amount `json:"sentToSelf"` + FiatRates map[string]float64 `json:"rates,omitempty"` + Txid string `json:"txid,omitempty"` } // BalanceHistories is array of BalanceHistory @@ -328,8 +329,9 @@ func (a BalanceHistories) SortAndAggregate(groupByTime uint32) BalanceHistories bhs := make(BalanceHistories, 0) if len(a) > 0 { bha := BalanceHistory{ - SentSat: &Amount{}, - ReceivedSat: &Amount{}, + ReceivedSat: &Amount{}, + SentSat: &Amount{}, + SentToSelfSat: &Amount{}, } sort.Sort(a) for i := range a { @@ -342,17 +344,19 @@ func (a BalanceHistories) SortAndAggregate(groupByTime uint32) BalanceHistories bhs = append(bhs, bha) } bha = BalanceHistory{ - Time: time, - SentSat: &Amount{}, - ReceivedSat: &Amount{}, + Time: time, + ReceivedSat: &Amount{}, + SentSat: &Amount{}, + SentToSelfSat: &Amount{}, } } if bha.Txid != bh.Txid { bha.Txs += bh.Txs bha.Txid = bh.Txid } - (*big.Int)(bha.SentSat).Add((*big.Int)(bha.SentSat), (*big.Int)(bh.SentSat)) (*big.Int)(bha.ReceivedSat).Add((*big.Int)(bha.ReceivedSat), (*big.Int)(bh.ReceivedSat)) + (*big.Int)(bha.SentSat).Add((*big.Int)(bha.SentSat), (*big.Int)(bh.SentSat)) + (*big.Int)(bha.SentToSelfSat).Add((*big.Int)(bha.SentToSelfSat), (*big.Int)(bh.SentToSelfSat)) } if bha.Txs > 0 { bha.Txid = "" diff --git a/api/types_test.go b/api/types_test.go index b8d24c3e..26265f61 100644 --- a/api/types_test.go +++ b/api/types_test.go @@ -67,20 +67,22 @@ func TestBalanceHistories_SortAndAggregate(t *testing.T) { name: "one", a: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(1)), - SentSat: (*Amount)(big.NewInt(2)), - Time: 1521514812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(1)), + SentSat: (*Amount)(big.NewInt(2)), + SentToSelfSat: (*Amount)(big.NewInt(1)), + Time: 1521514812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, }, groupByTime: 3600, want: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(1)), - SentSat: (*Amount)(big.NewInt(2)), - Time: 1521514800, - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(1)), + SentSat: (*Amount)(big.NewInt(2)), + SentToSelfSat: (*Amount)(big.NewInt(1)), + Time: 1521514800, + Txs: 1, }, }, }, @@ -88,67 +90,76 @@ func TestBalanceHistories_SortAndAggregate(t *testing.T) { name: "aggregate", a: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(1)), - SentSat: (*Amount)(big.NewInt(2)), - Time: 1521504812, - Txid: "0011223344556677889900112233445566778899001122334455667788990011", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(1)), + SentSat: (*Amount)(big.NewInt(2)), + SentToSelfSat: (*Amount)(big.NewInt(0)), + Time: 1521504812, + Txid: "0011223344556677889900112233445566778899001122334455667788990011", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(3)), - SentSat: (*Amount)(big.NewInt(4)), - Time: 1521504812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(3)), + SentSat: (*Amount)(big.NewInt(4)), + SentToSelfSat: (*Amount)(big.NewInt(2)), + Time: 1521504812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(5)), - SentSat: (*Amount)(big.NewInt(6)), - Time: 1521514812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(5)), + SentSat: (*Amount)(big.NewInt(6)), + SentToSelfSat: (*Amount)(big.NewInt(3)), + Time: 1521514812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(7)), - SentSat: (*Amount)(big.NewInt(8)), - Time: 1521504812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(7)), + SentSat: (*Amount)(big.NewInt(8)), + SentToSelfSat: (*Amount)(big.NewInt(3)), + Time: 1521504812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(9)), - SentSat: (*Amount)(big.NewInt(10)), - Time: 1521534812, - Txid: "0011223344556677889900112233445566778899001122334455667788990011", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(9)), + SentSat: (*Amount)(big.NewInt(10)), + SentToSelfSat: (*Amount)(big.NewInt(5)), + Time: 1521534812, + Txid: "0011223344556677889900112233445566778899001122334455667788990011", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(11)), - SentSat: (*Amount)(big.NewInt(12)), - Time: 1521534812, - Txid: "1122334455667788990011223344556677889900112233445566778899001100", - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(11)), + SentSat: (*Amount)(big.NewInt(12)), + SentToSelfSat: (*Amount)(big.NewInt(6)), + Time: 1521534812, + Txid: "1122334455667788990011223344556677889900112233445566778899001100", + Txs: 1, }, }, groupByTime: 3600, want: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(11)), - SentSat: (*Amount)(big.NewInt(14)), - Time: 1521504000, - Txs: 2, + ReceivedSat: (*Amount)(big.NewInt(11)), + SentSat: (*Amount)(big.NewInt(14)), + SentToSelfSat: (*Amount)(big.NewInt(5)), + Time: 1521504000, + Txs: 2, }, { - ReceivedSat: (*Amount)(big.NewInt(5)), - SentSat: (*Amount)(big.NewInt(6)), - Time: 1521514800, - Txs: 1, + ReceivedSat: (*Amount)(big.NewInt(5)), + SentSat: (*Amount)(big.NewInt(6)), + SentToSelfSat: (*Amount)(big.NewInt(3)), + Time: 1521514800, + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(20)), - SentSat: (*Amount)(big.NewInt(22)), - Time: 1521532800, - Txs: 2, + ReceivedSat: (*Amount)(big.NewInt(20)), + SentSat: (*Amount)(big.NewInt(22)), + SentToSelfSat: (*Amount)(big.NewInt(11)), + Time: 1521532800, + Txs: 2, }, }, }, diff --git a/api/worker.go b/api/worker.go index 95c66b35..4f593bfc 100644 --- a/api/worker.go +++ b/api/worker.go @@ -821,7 +821,7 @@ func (w *Worker) balanceHistoryHeightsFromTo(fromTimestamp, toTimestamp int64) ( return fromUnix, fromHeight, toUnix, toHeight } -func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid string, fromUnix, toUnix uint32) (*BalanceHistory, error) { +func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid string, fromUnix, toUnix uint32, selfAddrDesc map[string]struct{}) (*BalanceHistory, error) { var time uint32 var err error var ta *db.TxAddresses @@ -854,17 +854,30 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s return nil, nil } bh := BalanceHistory{ - Time: time, - Txs: 1, - SentSat: &Amount{}, - ReceivedSat: &Amount{}, - Txid: txid, + Time: time, + Txs: 1, + ReceivedSat: &Amount{}, + SentSat: &Amount{}, + SentToSelfSat: &Amount{}, + Txid: txid, } + countSentToSelf := false if w.chainType == bchain.ChainBitcoinType { + // detect if this input is the first of selfAddrDesc + // to not to count sentToSelf multiple times if counting multiple xpub addresses + ownInputIndex := -1 for i := range ta.Inputs { tai := &ta.Inputs[i] + if _, found := selfAddrDesc[string(tai.AddrDesc)]; found { + if ownInputIndex < 0 { + ownInputIndex = i + } + } if bytes.Equal(addrDesc, tai.AddrDesc) { (*big.Int)(bh.SentSat).Add((*big.Int)(bh.SentSat), &tai.ValueSat) + if ownInputIndex == i { + countSentToSelf = true + } } } for i := range ta.Outputs { @@ -872,6 +885,11 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s if bytes.Equal(addrDesc, tao.AddrDesc) { (*big.Int)(bh.ReceivedSat).Add((*big.Int)(bh.ReceivedSat), &tao.ValueSat) } + if countSentToSelf { + if _, found := selfAddrDesc[string(tao.AddrDesc)]; found { + (*big.Int)(bh.SentToSelfSat).Add((*big.Int)(bh.SentToSelfSat), &tao.ValueSat) + } + } } } else if w.chainType == bchain.ChainEthereumType { var value big.Int @@ -889,6 +907,9 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s if bytes.Equal(addrDesc, txAddrDesc) { (*big.Int)(bh.ReceivedSat).Add((*big.Int)(bh.ReceivedSat), &value) } + if _, found := selfAddrDesc[string(txAddrDesc)]; found { + countSentToSelf = true + } } } } @@ -903,6 +924,11 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s // add sent amount only for OK transactions, however fees always if ethTxData.Status == 1 { (*big.Int)(bh.SentSat).Add((*big.Int)(bh.SentSat), &value) + if countSentToSelf { + if _, found := selfAddrDesc[string(txAddrDesc)]; found { + (*big.Int)(bh.SentToSelfSat).Add((*big.Int)(bh.SentToSelfSat), &value) + } + } } var feesSat big.Int // mempool txs do not have fees yet @@ -963,8 +989,9 @@ func (w *Worker) GetBalanceHistory(address string, fromTimestamp, toTimestamp in if err != nil { return nil, err } + selfAddrDesc := map[string]struct{}{string(addrDesc): {}} for txi := len(txs) - 1; txi >= 0; txi-- { - bh, err := w.balanceHistoryForTxid(addrDesc, txs[txi], fromUnix, toUnix) + bh, err := w.balanceHistoryForTxid(addrDesc, txs[txi], fromUnix, toUnix, selfAddrDesc) if err != nil { return nil, err } diff --git a/api/xpub.go b/api/xpub.go index 245e1149..8b1cdc58 100644 --- a/api/xpub.go +++ b/api/xpub.go @@ -606,12 +606,18 @@ func (w *Worker) GetXpubBalanceHistory(xpub string, fromTimestamp, toTimestamp i if err != nil { return nil, err } + selfAddrDesc := make(map[string]struct{}) + for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + selfAddrDesc[string(da[i].addrDesc)] = struct{}{} + } + } for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { for i := range da { ad := &da[i] txids := ad.txids for txi := len(txids) - 1; txi >= 0; txi-- { - bh, err := w.balanceHistoryForTxid(ad.addrDesc, txids[txi].txid, fromUnix, toUnix) + bh, err := w.balanceHistoryForTxid(ad.addrDesc, txids[txi].txid, fromUnix, toUnix, selfAddrDesc) if err != nil { return nil, err } diff --git a/docs/api.md b/docs/api.md index dd9a0b9f..4e788cb5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -674,6 +674,7 @@ Example response (fiatcurrency not specified): "txs": 5, "received": "5000000", "sent": "0", + "sentToSelf":"100000", "rates": { "usd": 7855.9, "eur": 6838.13, @@ -685,6 +686,7 @@ Example response (fiatcurrency not specified): "txs": 1, "received": "0", "sent": "5000000", + "sentToSelf":"0", "rates": { "usd": 8283.11, "eur": 7464.45, @@ -703,6 +705,7 @@ Example response (fiatcurrency=usd): "txs": 5, "received": "5000000", "sent": "0", + "sentToSelf":"0", "rates": { "usd": 7855.9 } @@ -712,6 +715,7 @@ Example response (fiatcurrency=usd): "txs": 1, "received": "0", "sent": "5000000", + "sentToSelf":"0", "rates": { "usd": 8283.11 } @@ -728,6 +732,7 @@ Example response (fiatcurrency=usd&groupBy=172800): "txs": 6, "received": "5000000", "sent": "5000000", + "sentToSelf":"0", "rates": { "usd": 7734.45 } @@ -735,6 +740,8 @@ Example response (fiatcurrency=usd&groupBy=172800): ] ``` +The value of `sentToSelf` is the amount sent from the same address to the same address or within addresses of xpub. + ### Websocket API Websocket interface is provided at `/websocket/`. The interface can be explored using Blockbook Websocket Test Page found at `/test-websocket.html`. diff --git a/server/public_test.go b/server/public_test.go index 378fcfba..fee5b252 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -776,7 +776,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","sentToSelf":"0","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -785,7 +785,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","sentToSelf":"9000","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -794,7 +794,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","rates":{"eur":1301}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","rates":{"eur":1303}}]`, + `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","sentToSelf":"0","rates":{"eur":1301}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","sentToSelf":"9000","rates":{"eur":1303}}]`, }, }, { @@ -803,7 +803,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","rates":{"eur":1301,"usd":2001}}]`, + `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}}]`, }, }, { @@ -812,7 +812,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -821,7 +821,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}}]`, + `[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}}]`, }, }, { @@ -830,7 +830,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"usd":2001}}]`, + `[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"usd":2001}}]`, }, }, { @@ -839,7 +839,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -1370,7 +1370,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "descriptor": "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz", }, }, - want: `{"id":"32","data":[{"time":1521514800,"txs":1,"received":"24690","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","rates":{"eur":1303,"usd":2003}}]}`, + want: `{"id":"32","data":[{"time":1521514800,"txs":1,"received":"24690","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","sentToSelf":"0","rates":{"eur":1303,"usd":2003}}]}`, }, { name: "websocket getBalanceHistory xpub", @@ -1380,7 +1380,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "descriptor": dbtestdata.Xpub, }, }, - want: `{"id":"33","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","rates":{"eur":1303,"usd":2003}}]}`, + want: `{"id":"33","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]}`, }, { name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[usd]", @@ -1393,7 +1393,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"usd"}, }, }, - want: `{"id":"34","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"usd":2001}}]}`, + want: `{"id":"34","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"usd":2001}}]}`, }, { name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[usd, eur, incorrect]", @@ -1406,7 +1406,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"usd", "eur", "incorrect"}, }, }, - want: `{"id":"35","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"incorrect":-1,"usd":2001}}]}`, + want: `{"id":"35","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"incorrect":-1,"usd":2001}}]}`, }, { name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[]", @@ -1419,7 +1419,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{}, }, }, - want: `{"id":"36","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}}]}`, + want: `{"id":"36","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}}]}`, }, }