71e79c553e
to include full RPC name.
483 lines
13 KiB
Go
483 lines
13 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gorilla/rpc"
|
|
"github.com/lbryio/herald.go/db"
|
|
"github.com/lbryio/herald.go/internal"
|
|
"github.com/lbryio/lbcd/chaincfg"
|
|
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
|
"github.com/lbryio/lbcd/txscript"
|
|
"github.com/lbryio/lbcd/wire"
|
|
"github.com/lbryio/lbcutil"
|
|
"golang.org/x/exp/constraints"
|
|
)
|
|
|
|
type BlockchainCodec struct {
|
|
rpc.Codec
|
|
}
|
|
|
|
func (c *BlockchainCodec) NewRequest(r *http.Request) rpc.CodecRequest {
|
|
return &BlockchainCodecRequest{c.Codec.NewRequest(r)}
|
|
}
|
|
|
|
// BlockchainCodecRequest provides ability to rewrite the incoming
|
|
// request "method" field. For example:
|
|
// blockchain.block.get_header -> blockchain_block.Get_header
|
|
// blockchain.address.listunspent -> blockchain_address.Listunspent
|
|
// This makes the "method" string compatible with Gorilla/RPC
|
|
// requirements.
|
|
type BlockchainCodecRequest struct {
|
|
rpc.CodecRequest
|
|
}
|
|
|
|
func (cr *BlockchainCodecRequest) Method() (string, error) {
|
|
rawMethod, err := cr.CodecRequest.Method()
|
|
if err != nil {
|
|
return rawMethod, err
|
|
}
|
|
parts := strings.Split(rawMethod, ".")
|
|
if len(parts) < 2 {
|
|
return rawMethod, fmt.Errorf("blockchain rpc: service/method ill-formed: %q", rawMethod)
|
|
}
|
|
service := strings.Join(parts[0:len(parts)-1], "_")
|
|
method := parts[len(parts)-1]
|
|
if len(method) < 1 {
|
|
return rawMethod, fmt.Errorf("blockchain rpc: method ill-formed: %q", method)
|
|
}
|
|
method = strings.ToUpper(string(method[0])) + string(method[1:])
|
|
return service + "." + method, err
|
|
}
|
|
|
|
// BlockchainService methods handle "blockchain.block.*" RPCs
|
|
type BlockchainService struct {
|
|
DB *db.ReadOnlyDBColumnFamily
|
|
Chain *chaincfg.Params
|
|
}
|
|
|
|
// BlockchainAddressService methods handle "blockchain.address.*" RPCs
|
|
type BlockchainAddressService struct {
|
|
BlockchainService
|
|
}
|
|
|
|
// BlockchainScripthashService methods handle "blockchain.scripthash.*" RPCs
|
|
type BlockchainScripthashService struct {
|
|
BlockchainService
|
|
}
|
|
|
|
const CHUNK_SIZE = 96
|
|
const MAX_CHUNK_SIZE = 40960
|
|
const HEADER_SIZE = wire.MaxBlockHeaderPayload
|
|
const HASHX_LEN = 11
|
|
|
|
func min[Ord constraints.Ordered](x, y Ord) Ord {
|
|
if x < y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
type BlockGetServerHeightReq struct{}
|
|
type BlockGetServerHeightResp uint32
|
|
|
|
func (s *BlockchainService) Get_server_height(r *http.Request, req *BlockGetServerHeightReq, resp **BlockGetServerHeightResp) error {
|
|
if s.DB == nil || s.DB.LastState == nil {
|
|
return fmt.Errorf("unknown height")
|
|
}
|
|
result := BlockGetServerHeightResp(s.DB.LastState.Height)
|
|
*resp = &result
|
|
return nil
|
|
}
|
|
|
|
type BlockGetChunkReq uint32
|
|
type BlockGetChunkResp string
|
|
|
|
// 'blockchain.block.get_chunk'
|
|
func (s *BlockchainService) Get_chunk(r *http.Request, req *BlockGetChunkReq, resp **BlockGetChunkResp) error {
|
|
index := uint32(*req)
|
|
db_headers, err := s.DB.GetHeaders(index*CHUNK_SIZE, CHUNK_SIZE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw := make([]byte, 0, HEADER_SIZE*len(db_headers))
|
|
for _, h := range db_headers {
|
|
raw = append(raw, h[:]...)
|
|
}
|
|
headers := BlockGetChunkResp(hex.EncodeToString(raw))
|
|
*resp = &headers
|
|
return 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 (s *BlockchainService) Get_header(r *http.Request, req *BlockGetHeaderReq, resp **BlockGetHeaderResp) error {
|
|
height := uint32(*req)
|
|
headers, err := s.DB.GetHeaders(height, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(headers) < 1 {
|
|
return errors.New("not found")
|
|
}
|
|
decode := func(header *[HEADER_SIZE]byte, height uint32) *BlockGetHeaderResp {
|
|
var h1, h2, h3 chainhash.Hash
|
|
h1.SetBytes(header[4:36])
|
|
h2.SetBytes(header[36:68])
|
|
h3.SetBytes(header[68:100])
|
|
return &BlockGetHeaderResp{
|
|
Version: binary.LittleEndian.Uint32(header[0:]),
|
|
PrevBlockHash: h1.String(),
|
|
MerkleRoot: h2.String(),
|
|
ClaimTrieRoot: h3.String(),
|
|
Timestamp: binary.LittleEndian.Uint32(header[100:]),
|
|
Bits: binary.LittleEndian.Uint32(header[104:]),
|
|
Nonce: binary.LittleEndian.Uint32(header[108:]),
|
|
BlockHeight: height,
|
|
}
|
|
}
|
|
*resp = decode(&headers[0], height)
|
|
return err
|
|
}
|
|
|
|
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 (s *BlockchainService) Headers(r *http.Request, req *BlockHeadersReq, resp **BlockHeadersResp) error {
|
|
count := min(req.Count, MAX_CHUNK_SIZE)
|
|
db_headers, err := s.DB.GetHeaders(req.StartHeight, count)
|
|
if err != nil {
|
|
return 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
|
|
}
|
|
*resp = result
|
|
return err
|
|
}
|
|
|
|
func decodeScriptHash(scripthash string) ([]byte, error) {
|
|
sh, err := hex.DecodeString(scripthash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(sh) != chainhash.HashSize {
|
|
return nil, fmt.Errorf("invalid scripthash: %v (length %v)", scripthash, len(sh))
|
|
}
|
|
internal.ReverseBytesInPlace(sh)
|
|
return sh, nil
|
|
}
|
|
|
|
func hashX(scripthash []byte) []byte {
|
|
return scripthash[:HASHX_LEN]
|
|
}
|
|
|
|
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_LEN]
|
|
}
|
|
|
|
type AddressGetBalanceReq struct {
|
|
Address string `json:"address"`
|
|
}
|
|
type AddressGetBalanceResp struct {
|
|
Confirmed uint64 `json:"confirmed"`
|
|
Unconfirmed uint64 `json:"unconfirmed"`
|
|
}
|
|
|
|
// 'blockchain.address.get_balance'
|
|
func (s *BlockchainAddressService) Get_balance(r *http.Request, req *AddressGetBalanceReq, resp **AddressGetBalanceResp) error {
|
|
address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
script, err := txscript.PayToAddrScript(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashXScript(script, s.Chain)
|
|
confirmed, unconfirmed, err := s.DB.GetBalance(hashX)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*resp = &AddressGetBalanceResp{confirmed, unconfirmed}
|
|
return err
|
|
}
|
|
|
|
type scripthashGetBalanceReq struct {
|
|
ScriptHash string `json:"scripthash"`
|
|
}
|
|
type ScripthashGetBalanceResp struct {
|
|
Confirmed uint64 `json:"confirmed"`
|
|
Unconfirmed uint64 `json:"unconfirmed"`
|
|
}
|
|
|
|
// 'blockchain.scripthash.get_balance'
|
|
func (s *BlockchainScripthashService) Get_balance(r *http.Request, req *scripthashGetBalanceReq, resp **ScripthashGetBalanceResp) error {
|
|
scripthash, err := decodeScriptHash(req.ScriptHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashX(scripthash)
|
|
confirmed, unconfirmed, err := s.DB.GetBalance(hashX)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*resp = &ScripthashGetBalanceResp{confirmed, unconfirmed}
|
|
return err
|
|
}
|
|
|
|
type AddressGetHistoryReq struct {
|
|
Address string `json:"address"`
|
|
}
|
|
type TxInfo struct {
|
|
TxHash string `json:"tx_hash"`
|
|
Height uint32 `json:"height"`
|
|
}
|
|
type TxInfoFee struct {
|
|
TxInfo
|
|
Fee uint64 `json:"fee"`
|
|
}
|
|
type AddressGetHistoryResp struct {
|
|
Confirmed []TxInfo `json:"confirmed"`
|
|
Unconfirmed []TxInfoFee `json:"unconfirmed"`
|
|
}
|
|
|
|
// 'blockchain.address.get_history'
|
|
func (s *BlockchainAddressService) Get_history(r *http.Request, req *AddressGetHistoryReq, resp **AddressGetHistoryResp) error {
|
|
address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
script, err := txscript.PayToAddrScript(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashXScript(script, s.Chain)
|
|
dbTXs, err := s.DB.GetHistory(hashX)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
confirmed := make([]TxInfo, 0, len(dbTXs))
|
|
for _, tx := range dbTXs {
|
|
confirmed = append(confirmed,
|
|
TxInfo{
|
|
TxHash: tx.TxHash.String(),
|
|
Height: tx.Height,
|
|
})
|
|
}
|
|
result := &AddressGetHistoryResp{
|
|
Confirmed: confirmed,
|
|
Unconfirmed: []TxInfoFee{}, // TODO
|
|
}
|
|
*resp = result
|
|
return err
|
|
}
|
|
|
|
type ScripthashGetHistoryReq struct {
|
|
ScriptHash string `json:"scripthash"`
|
|
}
|
|
type ScripthashGetHistoryResp struct {
|
|
Confirmed []TxInfo `json:"confirmed"`
|
|
Unconfirmed []TxInfoFee `json:"unconfirmed"`
|
|
}
|
|
|
|
// 'blockchain.scripthash.get_history'
|
|
func (s *BlockchainScripthashService) Get_history(r *http.Request, req *ScripthashGetHistoryReq, resp **ScripthashGetHistoryResp) error {
|
|
scripthash, err := decodeScriptHash(req.ScriptHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashX(scripthash)
|
|
dbTXs, err := s.DB.GetHistory(hashX)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
confirmed := make([]TxInfo, 0, len(dbTXs))
|
|
for _, tx := range dbTXs {
|
|
confirmed = append(confirmed,
|
|
TxInfo{
|
|
TxHash: tx.TxHash.String(),
|
|
Height: tx.Height,
|
|
})
|
|
}
|
|
result := &ScripthashGetHistoryResp{
|
|
Confirmed: confirmed,
|
|
Unconfirmed: []TxInfoFee{}, // TODO
|
|
}
|
|
*resp = result
|
|
return err
|
|
}
|
|
|
|
type AddressGetMempoolReq struct {
|
|
Address string `json:"address"`
|
|
}
|
|
type AddressGetMempoolResp []TxInfoFee
|
|
|
|
// 'blockchain.address.get_mempool'
|
|
func (s *BlockchainAddressService) Get_mempool(r *http.Request, req *AddressGetMempoolReq, resp **AddressGetMempoolResp) error {
|
|
address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
script, err := txscript.PayToAddrScript(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashXScript(script, s.Chain)
|
|
// TODO...
|
|
internal.ReverseBytesInPlace(hashX)
|
|
unconfirmed := make([]TxInfoFee, 0, 100)
|
|
result := AddressGetMempoolResp(unconfirmed)
|
|
*resp = &result
|
|
return err
|
|
}
|
|
|
|
type ScripthashGetMempoolReq struct {
|
|
ScriptHash string `json:"scripthash"`
|
|
}
|
|
type ScripthashGetMempoolResp []TxInfoFee
|
|
|
|
// 'blockchain.scripthash.get_mempool'
|
|
func (s *BlockchainScripthashService) Get_mempool(r *http.Request, req *ScripthashGetMempoolReq, resp **ScripthashGetMempoolResp) error {
|
|
scripthash, err := decodeScriptHash(req.ScriptHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashX(scripthash)
|
|
// TODO...
|
|
internal.ReverseBytesInPlace(hashX)
|
|
unconfirmed := make([]TxInfoFee, 0, 100)
|
|
result := ScripthashGetMempoolResp(unconfirmed)
|
|
*resp = &result
|
|
return err
|
|
}
|
|
|
|
type AddressListUnspentReq struct {
|
|
Address string `json:"address"`
|
|
}
|
|
type TXOInfo struct {
|
|
TxHash string `json:"tx_hash"`
|
|
TxPos uint16 `json:"tx_pos"`
|
|
Height uint32 `json:"height"`
|
|
Value uint64 `json:"value"`
|
|
}
|
|
type AddressListUnspentResp []TXOInfo
|
|
|
|
// 'blockchain.address.listunspent'
|
|
func (s *BlockchainAddressService) Listunspent(r *http.Request, req *AddressListUnspentReq, resp **AddressListUnspentResp) error {
|
|
address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
script, err := txscript.PayToAddrScript(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashXScript(script, s.Chain)
|
|
dbTXOs, err := s.DB.GetUnspent(hashX)
|
|
unspent := make([]TXOInfo, 0, len(dbTXOs))
|
|
for _, txo := range dbTXOs {
|
|
unspent = append(unspent,
|
|
TXOInfo{
|
|
TxHash: txo.TxHash.String(),
|
|
TxPos: txo.TxPos,
|
|
Height: txo.Height,
|
|
Value: txo.Value,
|
|
})
|
|
}
|
|
result := AddressListUnspentResp(unspent)
|
|
*resp = &result
|
|
return err
|
|
}
|
|
|
|
type ScripthashListUnspentReq struct {
|
|
ScriptHash string `json:"scripthash"`
|
|
}
|
|
type ScripthashListUnspentResp []TXOInfo
|
|
|
|
// 'blockchain.scripthash.listunspent'
|
|
func (s *BlockchainScripthashService) Listunspent(r *http.Request, req *ScripthashListUnspentReq, resp **ScripthashListUnspentResp) error {
|
|
scripthash, err := decodeScriptHash(req.ScriptHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashX := hashX(scripthash)
|
|
dbTXOs, err := s.DB.GetUnspent(hashX)
|
|
unspent := make([]TXOInfo, 0, len(dbTXOs))
|
|
for _, txo := range dbTXOs {
|
|
unspent = append(unspent,
|
|
TXOInfo{
|
|
TxHash: txo.TxHash.String(),
|
|
TxPos: txo.TxPos,
|
|
Height: txo.Height,
|
|
Value: txo.Value,
|
|
})
|
|
}
|
|
result := ScripthashListUnspentResp(unspent)
|
|
*resp = &result
|
|
return err
|
|
}
|