diff --git a/db/db.go b/db/db.go index d546cd5..27e703b 100644 --- a/db/db.go +++ b/db/db.go @@ -406,6 +406,69 @@ func Iter(db *grocksdb.DB, opts *IterOptions) <-chan *prefixes.PrefixRowKV { return ch } +func (db *ReadOnlyDBColumnFamily) selectFrom(prefix []byte, startKey, stopKey prefixes.BaseKey) ([]*IterOptions, error) { + handle, err := db.EnsureHandle(prefixes.HashXHistory) + if err != nil { + return nil, err + } + // Prefix and handle + options := NewIterateOptions().WithPrefix(prefix).WithCfHandle(handle) + // Start and stop bounds + options = options.WithStart(startKey.PackKey()).WithStop(stopKey.PackKey()).WithIncludeStop(true) + // Don't include the key + options = options.WithIncludeKey(true).WithIncludeValue(true) + return []*IterOptions{options}, nil +} + +func iterate(db *grocksdb.DB, opts []*IterOptions) <-chan []*prefixes.PrefixRowKV { + out := make(chan []*prefixes.PrefixRowKV) + routine := func() { + for _, o := range opts { + for kv := range IterCF(db, o) { + row := make([]*prefixes.PrefixRowKV, 0, 1) + row = append(row, kv) + out <- row + } + } + close(out) + } + go routine() + return out +} + +func innerJoin(db *grocksdb.DB, in <-chan []*prefixes.PrefixRowKV, selectFn func([]*prefixes.PrefixRowKV) ([]*IterOptions, error)) <-chan []*prefixes.PrefixRowKV { + out := make(chan []*prefixes.PrefixRowKV) + routine := func() { + for kvs := range in { + selected, err := selectFn(kvs) + if err != nil { + out <- []*prefixes.PrefixRowKV{{Error: err}} + close(out) + return + } + for kv := range iterate(db, selected) { + row := make([]*prefixes.PrefixRowKV, 0, len(kvs)+1) + row = append(row, kvs...) + row = append(row, kv...) + out <- row + } + } + close(out) + return + } + go routine() + return out +} + +func checkForError(kvs []*prefixes.PrefixRowKV) error { + for _, kv := range kvs { + if kv.Error != nil { + return kv.Error + } + } + return nil +} + // // GetDB functions that open and return a db // diff --git a/db/db_get.go b/db/db_get.go index d561908..ea9fc66 100644 --- a/db/db_get.go +++ b/db/db_get.go @@ -6,8 +6,10 @@ import ( "encoding/hex" "fmt" "log" + "math" "github.com/lbryio/herald.go/db/prefixes" + "github.com/lbryio/lbcd/chaincfg/chainhash" "github.com/linxGnu/grocksdb" ) @@ -82,6 +84,187 @@ func (db *ReadOnlyDBColumnFamily) GetHeader(height uint32) ([]byte, error) { return rawValue, nil } +func (db *ReadOnlyDBColumnFamily) GetHeaders(height uint32, count uint32) ([][112]byte, error) { + handle, err := db.EnsureHandle(prefixes.Header) + if err != nil { + return nil, err + } + + startKeyRaw := prefixes.NewHeaderKey(0).PackKey() + endKeyRaw := prefixes.NewHeaderKey(height + count).PackKey() + options := NewIterateOptions().WithPrefix([]byte{prefixes.Header}).WithCfHandle(handle) + options = options.WithIncludeKey(false).WithIncludeValue(true) //.WithIncludeStop(true) + options = options.WithStart(startKeyRaw).WithStop(endKeyRaw) + + result := make([][112]byte, 0, count) + for kv := range IterCF(db.DB, options) { + h := [112]byte{} + copy(h[:], kv.Value.(*prefixes.BlockHeaderValue).Header[:112]) + result = append(result, h) + } + + return result, nil +} + +func (db *ReadOnlyDBColumnFamily) GetBalance(hashX []byte) (uint64, uint64, error) { + handle, err := db.EnsureHandle(prefixes.UTXO) + if err != nil { + return 0, 0, err + } + + startKey := prefixes.UTXOKey{ + Prefix: []byte{prefixes.UTXO}, + HashX: hashX, + TxNum: 0, + Nout: 0, + } + endKey := prefixes.UTXOKey{ + Prefix: []byte{prefixes.UTXO}, + HashX: hashX, + TxNum: math.MaxUint32, + Nout: math.MaxUint16, + } + + startKeyRaw := startKey.PackKey() + endKeyRaw := endKey.PackKey() + // Prefix and handle + options := NewIterateOptions().WithPrefix([]byte{prefixes.UTXO}).WithCfHandle(handle) + // Start and stop bounds + options = options.WithStart(startKeyRaw).WithStop(endKeyRaw).WithIncludeStop(true) + // Don't include the key + options = options.WithIncludeKey(false).WithIncludeValue(true) + + ch := IterCF(db.DB, options) + var confirmed uint64 = 0 + var unconfirmed uint64 = 0 // TODO + for kv := range ch { + confirmed += kv.Value.(*prefixes.UTXOValue).Amount + } + + return confirmed, unconfirmed, nil +} + +type TXOInfo struct { + TxHash *chainhash.Hash + TxPos uint16 + Height uint32 + Value uint64 +} + +func (db *ReadOnlyDBColumnFamily) GetUnspent(hashX []byte) ([]TXOInfo, error) { + startKey := &prefixes.UTXOKey{ + Prefix: []byte{prefixes.UTXO}, + HashX: hashX, + TxNum: 0, + Nout: 0, + } + endKey := &prefixes.UTXOKey{ + Prefix: []byte{prefixes.UTXO}, + HashX: hashX, + TxNum: math.MaxUint32, + Nout: math.MaxUint16, + } + selectedUTXO, err := db.selectFrom([]byte{prefixes.UTXO}, startKey, endKey) + if err != nil { + return nil, err + } + + selectTxHashByTxNum := func(in []*prefixes.PrefixRowKV) ([]*IterOptions, error) { + historyKey := in[0].Key.(*prefixes.UTXOKey) + out := make([]*IterOptions, 0, 100) + startKey := &prefixes.TxHashKey{ + Prefix: []byte{prefixes.TxHash}, + TxNum: historyKey.TxNum, + } + endKey := &prefixes.TxHashKey{ + Prefix: []byte{prefixes.TxHash}, + TxNum: historyKey.TxNum, + } + selectedTxHash, err := db.selectFrom([]byte{prefixes.TxHash}, startKey, endKey) + if err != nil { + return nil, err + } + out = append(out, selectedTxHash...) + return out, nil + } + + results := make([]TXOInfo, 0, 1000) + for kvs := range innerJoin(db.DB, iterate(db.DB, selectedUTXO), selectTxHashByTxNum) { + if err := checkForError(kvs); err != nil { + return results, err + } + utxoKey := kvs[0].Key.(*prefixes.UTXOKey) + utxoValue := kvs[0].Value.(*prefixes.UTXOValue) + txhashValue := kvs[1].Value.(*prefixes.TxHashValue) + results = append(results, + TXOInfo{ + TxHash: txhashValue.TxHash, + TxPos: utxoKey.Nout, + Height: 0, // TODO + Value: utxoValue.Amount, + }, + ) + } + return results, nil +} + +type TxInfo struct { + TxHash *chainhash.Hash + Height uint32 +} + +func (db *ReadOnlyDBColumnFamily) GetHistory(hashX []byte) ([]TxInfo, error) { + startKey := &prefixes.HashXHistoryKey{ + Prefix: []byte{prefixes.HashXHistory}, + HashX: hashX, + Height: 0, + } + endKey := &prefixes.HashXHistoryKey{ + Prefix: []byte{prefixes.UTXO}, + HashX: hashX, + Height: math.MaxUint32, + } + selectedHistory, err := db.selectFrom([]byte{prefixes.HashXHistory}, startKey, endKey) + if err != nil { + return nil, err + } + + selectTxHashByTxNums := func(in []*prefixes.PrefixRowKV) ([]*IterOptions, error) { + historyValue := in[0].Value.(*prefixes.HashXHistoryValue) + out := make([]*IterOptions, 0, 100) + for _, txnum := range historyValue.TxNums { + startKey := &prefixes.TxHashKey{ + Prefix: []byte{prefixes.TxHash}, + TxNum: txnum, + } + endKey := &prefixes.TxHashKey{ + Prefix: []byte{prefixes.TxHash}, + TxNum: txnum, + } + selectedTxHash, err := db.selectFrom([]byte{prefixes.TxHash}, startKey, endKey) + if err != nil { + return nil, err + } + out = append(out, selectedTxHash...) + } + return out, nil + } + + results := make([]TxInfo, 0, 1000) + for kvs := range innerJoin(db.DB, iterate(db.DB, selectedHistory), selectTxHashByTxNums) { + if err := checkForError(kvs); err != nil { + return results, err + } + historyKey := kvs[0].Key.(*prefixes.HashXHistoryKey) + txHashValue := kvs[1].Value.(*prefixes.TxHashValue) + results = append(results, TxInfo{ + TxHash: txHashValue.TxHash, + Height: historyKey.Height, + }) + } + return results, nil +} + // GetStreamsAndChannelRepostedByChannelHashes returns a map of streams and channel hashes that are reposted by the given channel hashes. func (db *ReadOnlyDBColumnFamily) GetStreamsAndChannelRepostedByChannelHashes(reposterChannelHashes [][]byte) (map[string][]byte, map[string][]byte, error) { handle, err := db.EnsureHandle(prefixes.ChannelToClaim) diff --git a/db/prefixes/prefixes.go b/db/prefixes/prefixes.go index 0c9f67b..92d8f54 100644 --- a/db/prefixes/prefixes.go +++ b/db/prefixes/prefixes.go @@ -125,6 +125,7 @@ type PrefixRowKV struct { Value BaseValue RawKey []byte RawValue []byte + Error error } type BaseKey interface { diff --git a/go.mod b/go.mod index 5e6cfcf..d938900 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-restruct/restruct v1.2.0-alpha github.com/gorilla/mux v1.7.3 github.com/gorilla/rpc v1.2.0 + github.com/lbryio/lbcutil v1.0.202 github.com/lbryio/lbry.go/v3 v3.0.1-beta github.com/linxGnu/grocksdb v1.6.42 github.com/olivere/elastic/v7 v7.0.24 @@ -23,15 +24,17 @@ require ( gopkg.in/karalabe/cookiejar.v1 v1.0.0-20141109175019-e1490cae028c ) -require golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect +require ( +) require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/lbryio/lbcd v0.22.201-beta-rc1 + github.com/lbryio/lbcd v0.22.201-beta-rc4 github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -39,6 +42,7 @@ require ( github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/stretchr/testify v1.7.0 // indirect + golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect diff --git a/go.sum b/go.sum index c6d18ed..7593b5b 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -359,8 +360,12 @@ github.com/lbryio/lbcd v0.22.100-beta-rc5/go.mod h1:9PbFSlHYX7WlnDQwcTxHVf1W35VA github.com/lbryio/lbcd v0.22.200-beta/go.mod h1:kNuzGWf808ipTGB0y0WogzsGv5BVM4Qv85Z+JYwC9FA= github.com/lbryio/lbcd v0.22.201-beta-rc1 h1:FmzzApVj2RBXloLM2w9tLvN2xyTZjeyh+QC7GIw/wwo= github.com/lbryio/lbcd v0.22.201-beta-rc1/go.mod h1:kNuzGWf808ipTGB0y0WogzsGv5BVM4Qv85Z+JYwC9FA= +github.com/lbryio/lbcd v0.22.201-beta-rc4 h1:Xh751Bh/GWRcP5bI6NJ2+zueo2otTcTWapFvFbryP5c= +github.com/lbryio/lbcd v0.22.201-beta-rc4/go.mod h1:Jgo48JDINhdOgHHR83J70Q6G42x3WAo9DI//QogcL+E= github.com/lbryio/lbcutil v1.0.201/go.mod h1:gDHc/b+Rdz3J7+VB8e5/Bl9roVf8Q5/8FQCyuK9dXD0= github.com/lbryio/lbcutil v1.0.202-rc3/go.mod h1:LGPtVBBzh4cFXfLFb8ginlFcbA2QwumLNFd0yk/as2o= +github.com/lbryio/lbcutil v1.0.202 h1:L0aRMs2bdCUAicD8Xe4NmUEvevDDea3qkIpCSACnftI= +github.com/lbryio/lbcutil v1.0.202/go.mod h1:LGPtVBBzh4cFXfLFb8ginlFcbA2QwumLNFd0yk/as2o= github.com/lbryio/lbry.go/v2 v2.7.1/go.mod h1:sUhhSKqPNkiwgBqvBzJIqfLLzGH8hkDGrrO/HcaXzFc= github.com/lbryio/lbry.go/v3 v3.0.1-beta h1:oIpQ5czhtdVSoWZCiOHE9SrqnNsahyCnMhXvXsd2IiM= github.com/lbryio/lbry.go/v3 v3.0.1-beta/go.mod h1:v03OVXSBGNZNDfGoAVyjQV/ZOzBGQyTnWs3jpkssxGM= @@ -370,6 +375,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linxGnu/grocksdb v1.6.42 h1:nJLoXFuzwBwQQQrXTUgRGRz1QRm7y8pR6CNV/gwrbqs= github.com/linxGnu/grocksdb v1.6.42/go.mod h1:JcMMDBFaDNhRXFYcYXmgQwb/RarSld1PulTI7UzE+w0= +github.com/linxGnu/grocksdb v1.7.0 h1:UyFDykX0CUfxDN10cqlFho/rwt9K6KoDaLXL9Ej5z9g= +github.com/linxGnu/grocksdb v1.7.0/go.mod h1:JcMMDBFaDNhRXFYcYXmgQwb/RarSld1PulTI7UzE+w0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5/go.mod h1:H0aPCWffGOaDcjkw1iB7W9DVLp6GXmfcJY/7YZCWPA4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= diff --git a/server/blockchain.go b/server/blockchain.go new file mode 100644 index 0000000..1331d51 --- /dev/null +++ b/server/blockchain.go @@ -0,0 +1,291 @@ +package server + +import ( + "bytes" + "compress/zlib" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "errors" + + "github.com/lbryio/lbcd/chaincfg" + "github.com/lbryio/lbcd/txscript" + "github.com/lbryio/lbcutil" + "golang.org/x/exp/constraints" +) + +type RpcReq interface { +} +type RpcResp interface { +} +type RpcHandler interface { + Handle() (RpcResp, error) +} + +const CHUNK_SIZE = 96 +const MAX_CHUNK_SIZE = 40960 +const HEADER_SIZE = 112 +const HASHX_SIZE = 11 + +func min[Ord constraints.Ordered](x, y Ord) Ord { + if x < y { + return x + } + return y +} + +type blockGetChunkReq uint32 +type blockGetChunkResp string + +// 'blockchain.block.get_chunk' +func (req *blockGetChunkReq) Handle(s *Server) (*blockGetChunkResp, error) { + index := uint32(*req) + db_headers, err := s.DB.GetHeaders(index*CHUNK_SIZE, CHUNK_SIZE) + if err != nil { + return nil, err + } + raw := make([]byte, 0, HEADER_SIZE*len(db_headers)) + for _, h := range db_headers { + raw = append(raw, h[:]...) + } + headers := blockGetChunkResp(hex.EncodeToString(raw)) + return &headers, err +} + +type blockGetHeaderReq uint32 +type blockGetHeaderResp struct { + Version uint32 `json:"version"` + PrevBlockHash string `json:"prev_block_hash"` + MerkleRoot string `json:"merkle_root"` + ClaimTrieRoot string `json:"claim_trie_root"` + Timestamp uint32 `json:"timestamp"` + Bits uint32 `json:"bits"` + Nonce uint32 `json:"nonce"` + BlockHeight uint32 `json:"block_height"` +} + +// 'blockchain.block.get_header' +func (req *blockGetHeaderReq) Handle(s *Server) (*blockGetHeaderResp, error) { + height := uint32(*req) + headers, err := s.DB.GetHeaders(height, 1) + if err != nil { + return nil, err + } + if len(headers) < 1 { + return nil, errors.New("not found") + } + decode := func(header *[HEADER_SIZE]byte, height uint32) *blockGetHeaderResp { + return &blockGetHeaderResp{ + Version: binary.LittleEndian.Uint32(header[0:]), + PrevBlockHash: hex.EncodeToString(header[4:46]), + MerkleRoot: hex.EncodeToString(header[36:68]), + ClaimTrieRoot: hex.EncodeToString(header[68:100]), + Timestamp: binary.LittleEndian.Uint32(header[100:]), + Bits: binary.LittleEndian.Uint32(header[104:]), + Nonce: binary.LittleEndian.Uint32(header[108:]), + BlockHeight: height, + } + } + return decode(&headers[0], height), nil +} + +type blockHeadersReq struct { + StartHeight uint32 `json:"start_height"` + Count uint32 `json:"count"` + CpHeight uint32 `json:"cp_height"` + B64 bool `json:"b64"` +} + +type blockHeadersResp struct { + Base64 string `json:"base64,omitempty"` + Hex string `json:"hex,omitempty"` + Count uint32 `json:"count"` + Max uint32 `json:"max"` + Branch string `json:"branch,omitempty"` + Root string `json:"root,omitempty"` +} + +// 'blockchain.block.headers' +func (req *blockHeadersReq) Handle(s *Server) (*blockHeadersResp, error) { + count := min(req.Count, MAX_CHUNK_SIZE) + db_headers, err := s.DB.GetHeaders(req.StartHeight, count) + if err != nil { + return nil, err + } + count = uint32(len(db_headers)) + raw := make([]byte, 0, HEADER_SIZE*count) + for _, h := range db_headers { + raw = append(raw, h[:]...) + } + result := &blockHeadersResp{ + Count: count, + Max: MAX_CHUNK_SIZE, + } + if req.B64 { + zipped := bytes.Buffer{} + w := zlib.NewWriter(&zipped) + w.Write(raw) + w.Close() + result.Base64 = base64.StdEncoding.EncodeToString(zipped.Bytes()) + } else { + result.Hex = hex.EncodeToString(raw) + } + if count > 0 && req.CpHeight > 0 { + // TODO + //last_height := height + count - 1 + } + return result, err +} + +func hashXScript(script []byte, coin *chaincfg.Params) []byte { + if _, err := txscript.ExtractClaimScript(script); err == nil { + baseScript := txscript.StripClaimScriptPrefix(script) + if class, addrs, _, err := txscript.ExtractPkScriptAddrs(baseScript, coin); err == nil { + switch class { + case txscript.PubKeyHashTy, txscript.ScriptHashTy, txscript.PubKeyTy: + script, _ := txscript.PayToAddrScript(addrs[0]) + return hashXScript(script, coin) + } + } + } + sum := sha256.Sum256(script) + return sum[:HASHX_SIZE] +} + +type addressGetBalanceReq struct { + Address string +} +type addressGetBalanceResp struct { + Confirmed uint64 + Unconfirmed uint64 +} + +// 'blockchain.address.get_balance' +func (req *addressGetBalanceReq) Handle(s *Server) (*addressGetBalanceResp, error) { + address, err := lbcutil.DecodeAddress(req.Address, s.Coin) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + hashX := hashXScript(script, s.Coin) + confirmed, unconfirmed, err := s.DB.GetBalance(hashX) + if err != nil { + return nil, err + } + return &addressGetBalanceResp{confirmed, unconfirmed}, err +} + +type addressGetHistoryReq struct { + Address string +} +type TxInfo struct { + TxHash string + Height uint32 +} +type TxInfoFee struct { + TxInfo + Fee uint64 +} +type addressGetHistoryResp struct { + Confirmed []TxInfo + Unconfirmed []TxInfoFee +} + +// 'blockchain.address.get_history' +func (req *addressGetHistoryReq) Handle(s *Server) (*addressGetHistoryResp, error) { + address, err := lbcutil.DecodeAddress(req.Address, s.Coin) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + hashX := hashXScript(script, s.Coin) + dbTXs, err := s.DB.GetHistory(hashX) + confirmed := make([]TxInfo, 0, len(dbTXs)) + for _, tx := range dbTXs { + confirmed = append(confirmed, + TxInfo{ + TxHash: hex.EncodeToString(tx.TxHash[:]), + Height: tx.Height, + }) + } + result := &addressGetHistoryResp{ + Confirmed: confirmed, + Unconfirmed: []TxInfoFee{}, // TODO + } + return result, nil +} + +type addressGetMempoolReq struct { + Address string +} +type addressGetMempoolResp []TxInfoFee + +// 'blockchain.address.get_mempool' +func (req *addressGetMempoolReq) Handle(s *Server) (*addressGetMempoolResp, error) { + address, err := lbcutil.DecodeAddress(req.Address, s.Coin) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + // TODO... + hashX := hashXScript(script, s.Coin) + dbTXs, err := s.DB.GetHistory(hashX) + confirmed := make([]TxInfo, 0, len(dbTXs)) + for _, tx := range dbTXs { + confirmed = append(confirmed, + TxInfo{ + TxHash: hex.EncodeToString(tx.TxHash[:]), + Height: tx.Height, + }) + } + unconfirmed := make([]TxInfoFee, 0, 100) + result := addressGetMempoolResp(unconfirmed) + return &result, nil +} + +type addressListUnspentReq struct { + Address string +} +type TXOInfo struct { + TxHash string + TxPos uint16 + Height uint32 + Value uint64 +} +type addressListUnspentResp []TXOInfo + +// 'blockchain.address.listunspent' +func (req *addressListUnspentReq) Handle(s *Server) (*addressListUnspentResp, error) { + address, err := lbcutil.DecodeAddress(req.Address, s.Coin) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + hashX := hashXScript(script, s.Coin) + dbTXOs, err := s.DB.GetUnspent(hashX) + unspent := make([]TXOInfo, 0, len(dbTXOs)) + for _, txo := range dbTXOs { + unspent = append(unspent, + TXOInfo{ + TxHash: hex.EncodeToString(txo.TxHash[:]), + TxPos: txo.TxPos, + Height: txo.Height, + Value: txo.Value, + }) + } + result := addressListUnspentResp(unspent) + return &result, nil +} diff --git a/server/server.go b/server/server.go index 5aba88a..880f533 100644 --- a/server/server.go +++ b/server/server.go @@ -22,6 +22,7 @@ import ( "github.com/lbryio/herald.go/internal/metrics" "github.com/lbryio/herald.go/meta" pb "github.com/lbryio/herald.go/protobuf/go" + "github.com/lbryio/lbcd/chaincfg" "github.com/olivere/elastic/v7" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -38,6 +39,7 @@ type Server struct { DB *db.ReadOnlyDBColumnFamily EsClient *elastic.Client QueryCache *ttlcache.Cache + Coin *chaincfg.Params S256 *hash.Hash LastRefreshCheck time.Time RefreshDelta time.Duration @@ -260,6 +262,7 @@ func MakeHubServer(ctx context.Context, args *Args) *Server { EsClient: client, QueryCache: cache, S256: &s256, + Coin: &chaincfg.MainNetParams, LastRefreshCheck: time.Now(), RefreshDelta: refreshDelta, NumESRefreshes: 0,