From 8bf5837b26b81b4b7d8faf3959b59fe19de18b81 Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Wed, 14 Nov 2018 14:46:42 +0100 Subject: [PATCH] Implement address utxo api call #83 --- api/types.go | 10 +++++ api/worker.go | 100 +++++++++++++++++++++++++++++++++++++++--- server/public.go | 11 +++++ server/public_test.go | 9 ++++ 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/api/types.go b/api/types.go index 8a535f24..c925be62 100644 --- a/api/types.go +++ b/api/types.go @@ -111,6 +111,16 @@ type Address struct { Txids []string `json:"transactions,omitempty"` } +// AddressUtxo holds information about address and its transactions +type AddressUtxo struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + Amount string `json:"amount"` + AmountSat big.Int `json:"satoshis"` + Height int `json:"height,omitempty"` + Confirmations int `json:"confirmations"` +} + // Blocks is list of blocks with paging information type Blocks struct { Paging diff --git a/api/worker.go b/api/worker.go index f7572058..d4f92f1e 100644 --- a/api/worker.go +++ b/api/worker.go @@ -45,7 +45,7 @@ func (w *Worker) getAddressesFromVout(vout *bchain.Vout) (bchain.AddressDescript } // setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output -// there is not an index, it must be found using addresses -> txaddresses -> tx +// there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) error { err := w.db.GetAddrDescTransactions(vout.ScriptPubKey.AddrDesc, height, ^uint32(0), func(t string, index uint32, isOutput bool) error { if isOutput == false { @@ -102,12 +102,13 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool) (*Tx, error) { if err != nil { return nil, NewAPIError(fmt.Sprintf("Tx not found, %v", err), true) } - ta, err := w.db.GetTxAddresses(txid) - if err != nil { - return nil, errors.Annotatef(err, "GetTxAddresses %v", txid) - } + var ta *db.TxAddresses var blockhash string if bchainTx.Confirmations > 0 { + ta, err = w.db.GetTxAddresses(txid) + if err != nil { + return nil, errors.Annotatef(err, "GetTxAddresses %v", txid) + } blockhash, err = w.db.GetBlockHash(height) if err != nil { return nil, errors.Annotatef(err, "GetBlockHash %v", height) @@ -476,6 +477,95 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids b return r, nil } +// GetAddressUtxo returns unspent outputs for given address +func (w *Worker) GetAddressUtxo(address string) ([]AddressUtxo, error) { + start := time.Now() + addrDesc, err := w.chainParser.GetAddrDescFromAddress(address) + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Invalid address, %v", err), true) + } + var r []AddressUtxo + // get utxo from mempool + txm, err := w.getAddressTxids(addrDesc, true) + if err != nil { + return nil, errors.Annotatef(err, "getAddressTxids %v true", address) + } + for _, txid := range txm { + bchainTx, _, err := w.txCache.GetTransaction(txid) + // mempool transaction may fail + if err != nil { + glog.Error("GetTransaction in mempool ", txid, ": ", err) + } else { + for i := range bchainTx.Vout { + bchainVout := &bchainTx.Vout[i] + vad, err := w.chainParser.GetAddrDescFromVout(bchainVout) + if err == nil && bytes.Equal(addrDesc, vad) { + r = append(r, AddressUtxo{ + Txid: bchainTx.Txid, + Vout: uint32(i), + AmountSat: bchainVout.ValueSat, + Amount: w.chainParser.AmountToDecimalString(&bchainVout.ValueSat), + }) + } + } + } + } + // get utxo from index + ba, err := w.db.GetAddrDescBalance(addrDesc) + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) + } + // ba can be nil if the address is only in mempool! + if ba != nil && ba.BalanceSat.Uint64() > 0 { + type outpoint struct { + txid string + vout uint32 + } + txids := make([]outpoint, 0) + err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, vout uint32, isOutput bool) error { + if isOutput { + txids = append(txids, outpoint{txid, vout}) + } + return nil + }) + var lastTxid string + var ta *db.TxAddresses + total := ba.BalanceSat + b, _, err := w.db.GetBestBlock() + bestheight := int(b) + for i := len(txids) - 1; i >= 0 && total.Int64() > 0; i-- { + o := txids[i] + if lastTxid != o.txid { + ta, err = w.db.GetTxAddresses(o.txid) + if err != nil { + return nil, err + } + lastTxid = o.txid + } + if ta == nil { + glog.Warning("DB inconsistency: tx ", o.txid, ": not found in txAddresses") + } else { + if len(ta.Outputs) <= int(o.vout) { + glog.Warning("DB inconsistency: txAddresses ", o.txid, " does not have enough outputs") + } else { + v := ta.Outputs[o.vout].ValueSat + r = append(r, AddressUtxo{ + Txid: o.txid, + Vout: o.vout, + AmountSat: v, + Amount: w.chainParser.AmountToDecimalString(&v), + Height: int(ta.Height), + Confirmations: bestheight - int(ta.Height) + 1, + }) + total.Sub(&total, &v) + } + } + } + } + glog.Info("GetAddressUtxo ", address, ", ", len(r), " utxos, finished in ", time.Since(start)) + return r, nil +} + // GetBlocks returns BlockInfo for blocks on given page func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) { start := time.Now() diff --git a/server/public.go b/server/public.go index e581d81f..85814e75 100644 --- a/server/public.go +++ b/server/public.go @@ -129,6 +129,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx)) serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific)) serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress)) + serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo)) serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock)) serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx)) serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee)) @@ -685,6 +686,16 @@ func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) { return address, err } +func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) { + var utxo []api.AddressUtxo + var err error + s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() + if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:]) + } + return utxo, err +} + func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) { var block *api.Block var err error diff --git a/server/public_test.go b/server/public_test.go index 6e9b4bc8..f2f2a4ba 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -408,6 +408,15 @@ func httpTests(t *testing.T, ts *httptest.Server) { `{"page":1,"totalPages":1,"itemsOnPage":1000,"addrStr":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"12345.67890123","totalSent":"12345.67890123","unconfirmedBalance":"0","unconfirmedTxApperances":0,"txApperances":2,"transactions":["7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"]}`, }, }, + { + name: "apiAddressUtxo", + r: newGetRequest(ts.URL + "/api/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":1,"amount":"9172.83951061","satoshis":917283951061,"height":225494,"confirmations":1}]`, + }, + }, { name: "apiSendTx", r: newGetRequest(ts.URL + "/api/sendtx/1234567890"),