diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 405fd867..6a99b2d3 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -298,6 +298,8 @@ type GetBlockTemplateResult struct { // Block proposal from BIP 0023. Capabilities []string `json:"capabilities,omitempty"` RejectReasion string `json:"reject-reason,omitempty"` + + ClaimTrieHash string `json:"claimtrie"` } // GetMempoolEntryResult models the data returned from the getmempoolentry's @@ -430,6 +432,9 @@ type ScriptPubKeyResult struct { Hex string `json:"hex,omitempty"` ReqSigs int32 `json:"reqSigs,omitempty"` Type string `json:"type"` + SubType string `json:"subtype"` + IsClaim bool `json:"isclaim"` + IsSupport bool `json:"issupport"` Addresses []string `json:"addresses,omitempty"` } @@ -588,6 +593,8 @@ func (v *Vin) MarshalJSON() ([]byte, error) { type PrevOut struct { Addresses []string `json:"addresses,omitempty"` Value float64 `json:"value"` + IsClaim bool `json:"isclaim"` + IsSupport bool `json:"issupport"` } // VinPrevOut is like Vin except it includes PrevOut. It is used by searchrawtransaction diff --git a/btcjson/chainsvrresults_test.go b/btcjson/chainsvrresults_test.go index 72dcd8d7..af47ccab 100644 --- a/btcjson/chainsvrresults_test.go +++ b/btcjson/chainsvrresults_test.go @@ -70,7 +70,7 @@ func TestChainSvrCustomResults(t *testing.T) { }, Sequence: 4294967295, }, - expected: `{"txid":"123","vout":1,"scriptSig":{"asm":"0","hex":"00"},"prevOut":{"addresses":["addr1"],"value":0},"sequence":4294967295}`, + expected: `{"txid":"123","vout":1,"scriptSig":{"asm":"0","hex":"00"},"prevOut":{"addresses":["addr1"],"value":0,"isclaim":false,"issupport":false},"sequence":4294967295}`, }, } diff --git a/btcjson/claimcmds.go b/btcjson/claimcmds.go new file mode 100644 index 00000000..cb98fbf8 --- /dev/null +++ b/btcjson/claimcmds.go @@ -0,0 +1,95 @@ +package btcjson + +func init() { + // No special flags for commands in this file. + flags := UsageFlag(0) + + MustRegisterCmd("getchangesinblock", (*GetChangesInBlockCmd)(nil), flags) + MustRegisterCmd("getclaimsforname", (*GetClaimsForNameCmd)(nil), flags) + MustRegisterCmd("getclaimsfornamebyid", (*GetClaimsForNameByIDCmd)(nil), flags) + MustRegisterCmd("getclaimsfornamebybid", (*GetClaimsForNameByBidCmd)(nil), flags) + MustRegisterCmd("getclaimsfornamebyseq", (*GetClaimsForNameBySeqCmd)(nil), flags) + MustRegisterCmd("normalize", (*GetNormalizedCmd)(nil), flags) +} + +// optional inputs are required to be pointers, but they support things like `jsonrpcdefault:"false"` +// optional inputs have to be at the bottom of the struct +// optional outputs require ",omitempty" +// traditional bitcoin fields are all lowercase + +type GetChangesInBlockCmd struct { + HashOrHeight *string `json:"hashorheight" jsonrpcdefault:""` +} + +type GetChangesInBlockResult struct { + Hash string `json:"hash"` + Height int32 `json:"height"` + Names []string `json:"names"` +} + +type GetClaimsForNameCmd struct { + Name string `json:"name"` + HashOrHeight *string `json:"hashorheight" jsonrpcdefault:""` + IncludeValues *bool `json:"includevalues" jsonrpcdefault:"false"` +} + +type GetClaimsForNameByIDCmd struct { + Name string `json:"name"` + PartialClaimIDs []string `json:"partialclaimids"` + HashOrHeight *string `json:"hashorheight" jsonrpcdefault:""` + IncludeValues *bool `json:"includevalues" jsonrpcdefault:"false"` +} + +type GetClaimsForNameByBidCmd struct { + Name string `json:"name"` + Bids []int32 `json:"bids"` + HashOrHeight *string `json:"hashorheight" jsonrpcdefault:""` + IncludeValues *bool `json:"includevalues" jsonrpcdefault:"false"` +} + +type GetClaimsForNameBySeqCmd struct { + Name string `json:"name"` + Sequences []int32 `json:"sequences" jsonrpcusage:"[sequence,...]"` + HashOrHeight *string `json:"hashorheight" jsonrpcdefault:""` + IncludeValues *bool `json:"includevalues" jsonrpcdefault:"false"` +} + +type GetClaimsForNameResult struct { + Hash string `json:"hash"` + Height int32 `json:"height"` + NormalizedName string `json:"normalizedname"` + Claims []ClaimResult `json:"claims"` + // UnclaimedSupports []SupportResult `json:"unclaimedSupports"` how would this work with other constraints? +} + +type SupportResult struct { + TXID string `json:"txid"` + N uint32 `json:"n"` + Height int32 `json:"height"` + ValidAtHeight int32 `json:"validatheight"` + Amount int64 `json:"amount"` + Address string `json:"address,omitempty"` + Value string `json:"value,omitempty"` +} + +type ClaimResult struct { + ClaimID string `json:"claimid"` + TXID string `json:"txid"` + N uint32 `json:"n"` + Bid int32 `json:"bid"` + Sequence int32 `json:"sequence"` + Height int32 `json:"height"` + ValidAtHeight int32 `json:"validatheight"` + EffectiveAmount int64 `json:"effectiveamount"` + Supports []SupportResult `json:"supports,omitempty"` + Address string `json:"address,omitempty"` + Value string `json:"value,omitempty"` +} + +type GetNormalizedCmd struct { + Name string `json:"name"` +} + +type GetNormalizedResult struct { + NormalizedName string `json:"normalizedname"` +} diff --git a/btcjson/jsonrpc.go b/btcjson/jsonrpc.go index 553a7bc3..e94653da 100644 --- a/btcjson/jsonrpc.go +++ b/btcjson/jsonrpc.go @@ -226,8 +226,12 @@ func NewResponse(rpcVersion RPCVersion, id interface{}, marshalledResult []byte, // JSON-RPC client. func MarshalResponse(rpcVersion RPCVersion, id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) { if !rpcVersion.IsValid() { - str := fmt.Sprintf("rpcversion '%s' is invalid", rpcVersion) - return nil, makeError(ErrInvalidType, str) + if rpcVersion == "" { + rpcVersion = RpcVersion1 + } else { + str := fmt.Sprintf("rpcversion '%s' is unsupported", rpcVersion) + return nil, makeError(ErrInvalidType, str) + } } marshalledResult, err := json.Marshal(result) diff --git a/rpcclaimtrie.go b/rpcclaimtrie.go new file mode 100644 index 00000000..0677637b --- /dev/null +++ b/rpcclaimtrie.go @@ -0,0 +1,342 @@ +package main + +import ( + "bytes" + "encoding/hex" + "strconv" + "strings" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/claimtrie/node" + "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +var claimtrieHandlers = map[string]commandHandler{ + "getchangesinblock": handleGetChangesInBlock, + "getclaimsforname": handleGetClaimsForName, + "getclaimsfornamebyid": handleGetClaimsForNameByID, + "getclaimsfornamebybid": handleGetClaimsForNameByBid, + "getclaimsfornamebyseq": handleGetClaimsForNameBySeq, + "normalize": handleGetNormalized, +} + +func handleGetChangesInBlock(s *rpcServer, cmd interface{}, _ <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.GetChangesInBlockCmd) + hash, height, err := parseHashOrHeight(s, c.HashOrHeight) + if err != nil { + return nil, err + } + + names, err := s.cfg.Chain.GetNamesChangedInBlock(height) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Message: " + err.Error(), + } + } + + return btcjson.GetChangesInBlockResult{ + Hash: hash, + Height: height, + Names: names, + }, nil +} + +func parseHashOrHeight(s *rpcServer, hashOrHeight *string) (string, int32, error) { + if hashOrHeight == nil || len(*hashOrHeight) == 0 { + + if !s.cfg.Chain.IsCurrent() { + return "", 0, &btcjson.RPCError{ + Code: btcjson.ErrRPCClientInInitialDownload, + Message: "Unable to query the chain tip during initial download", + } + } + + // just give them the latest block if a specific one wasn't requested + best := s.cfg.Chain.BestSnapshot() + return best.Hash.String(), best.Height, nil + } + + ht, err := strconv.ParseInt(*hashOrHeight, 10, 32) + if err == nil && len(*hashOrHeight) < 32 { + hs, err := s.cfg.Chain.BlockHashByHeight(int32(ht)) + if err != nil { + return "", 0, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Unable to locate a block at height " + *hashOrHeight + ": " + err.Error(), + } + } + return hs.String(), int32(ht), nil + } + + hs, err := chainhash.NewHashFromStr(*hashOrHeight) + if err != nil { + return "", 0, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Unable to parse a height or hash from " + *hashOrHeight + ": " + err.Error(), + } + } + h, err := s.cfg.Chain.BlockHeightByHash(hs) + if err != nil { + return hs.String(), h, &btcjson.RPCError{ + Code: btcjson.ErrRPCBlockNotFound, + Message: "Unable to find a block with hash " + hs.String() + ": " + err.Error(), + } + } + return hs.String(), h, nil +} + +func handleGetClaimsForName(s *rpcServer, cmd interface{}, _ <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.GetClaimsForNameCmd) + hash, height, err := parseHashOrHeight(s, c.HashOrHeight) + if err != nil { + return nil, err + } + + name, n, err := s.cfg.Chain.GetClaimsForName(height, c.Name) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Message: " + err.Error(), + } + } + + var results []btcjson.ClaimResult + for i := range n.Claims { + cr, err := toClaimResult(s, int32(i), n, c.IncludeValues) + if err != nil { + return nil, err + } + results = append(results, cr) + } + + return btcjson.GetClaimsForNameResult{ + Hash: hash, + Height: height, + NormalizedName: name, + Claims: results, + }, nil +} + +func handleGetClaimsForNameByID(s *rpcServer, cmd interface{}, _ <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.GetClaimsForNameByIDCmd) + hash, height, err := parseHashOrHeight(s, c.HashOrHeight) + if err != nil { + return nil, err + } + + name, n, err := s.cfg.Chain.GetClaimsForName(height, c.Name) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Message: " + err.Error(), + } + } + + var results []btcjson.ClaimResult + for i := 0; i < len(n.Claims); i++ { + for _, id := range c.PartialClaimIDs { + if strings.HasPrefix(n.Claims[i].ClaimID.String(), id) { + cr, err := toClaimResult(s, int32(i), n, c.IncludeValues) + if err != nil { + return nil, err + } + results = append(results, cr) + break + } + } + } + + return btcjson.GetClaimsForNameResult{ + Hash: hash, + Height: height, + NormalizedName: name, + Claims: results, + }, nil +} + +func handleGetClaimsForNameByBid(s *rpcServer, cmd interface{}, _ <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.GetClaimsForNameByBidCmd) + hash, height, err := parseHashOrHeight(s, c.HashOrHeight) + if err != nil { + return nil, err + } + + name, n, err := s.cfg.Chain.GetClaimsForName(height, c.Name) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Message: " + err.Error(), + } + } + + var results []btcjson.ClaimResult + for _, b := range c.Bids { // claims are already sorted in bid order + if b >= 0 && int(b) < len(n.Claims) { + cr, err := toClaimResult(s, b, n, c.IncludeValues) + if err != nil { + return nil, err + } + results = append(results, cr) + } + } + + return btcjson.GetClaimsForNameResult{ + Hash: hash, + Height: height, + NormalizedName: name, + Claims: results, + }, nil +} + +func handleGetClaimsForNameBySeq(s *rpcServer, cmd interface{}, _ <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.GetClaimsForNameBySeqCmd) + hash, height, err := parseHashOrHeight(s, c.HashOrHeight) + if err != nil { + return nil, err + } + + name, n, err := s.cfg.Chain.GetClaimsForName(height, c.Name) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Message: " + err.Error(), + } + } + + sm := map[int32]bool{} + for _, seq := range c.Sequences { + sm[seq] = true + } + + var results []btcjson.ClaimResult + for i := 0; i < len(n.Claims); i++ { + if sm[n.Claims[i].Sequence] { + cr, err := toClaimResult(s, int32(i), n, c.IncludeValues) + if err != nil { + return nil, err + } + results = append(results, cr) + } + } + + return btcjson.GetClaimsForNameResult{ + Hash: hash, + Height: height, + NormalizedName: name, + Claims: results, + }, nil +} + +func toClaimResult(s *rpcServer, i int32, node *node.Node, includeValues *bool) (btcjson.ClaimResult, error) { + claim := node.Claims[i] + address, value, err := lookupValue(s, claim.OutPoint, includeValues) + supports, err := toSupportResults(s, i, node, includeValues) + return btcjson.ClaimResult{ + ClaimID: claim.ClaimID.String(), + Height: claim.AcceptedAt, + ValidAtHeight: claim.ActiveAt, + TXID: claim.OutPoint.Hash.String(), + N: claim.OutPoint.Index, + Bid: i, // assuming sorted by bid + EffectiveAmount: claim.Amount + node.SupportSums[claim.ClaimID.Key()], + Sequence: claim.Sequence, + Supports: supports, + Address: address, + Value: value, + }, err +} + +func toSupportResults(s *rpcServer, i int32, n *node.Node, includeValues *bool) ([]btcjson.SupportResult, error) { + var results []btcjson.SupportResult + c := n.Claims[i] + for _, sup := range n.Supports { + if sup.Status == node.Activated && c.ClaimID == sup.ClaimID { + address, value, err := lookupValue(s, sup.OutPoint, includeValues) + if err != nil { + return results, err + } + results = append(results, btcjson.SupportResult{ + TXID: sup.OutPoint.Hash.String(), + N: sup.OutPoint.Index, + Height: sup.AcceptedAt, + ValidAtHeight: sup.ActiveAt, + Amount: sup.Amount, + Value: value, + Address: address, + }) + } + } + return results, nil +} + +func lookupValue(s *rpcServer, outpoint wire.OutPoint, includeValues *bool) (string, string, error) { + if includeValues == nil || !*includeValues { + return "", "", nil + } + // TODO: maybe use addrIndex if the txIndex is not available + + if s.cfg.TxIndex == nil { + return "", "", &btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "The transaction index must be " + + "enabled to query the blockchain " + + "(specify --txindex)", + } + } + + txHash := &outpoint.Hash + blockRegion, err := s.cfg.TxIndex.TxBlockRegion(txHash) + if err != nil { + context := "Failed to retrieve transaction location" + return "", "", internalRPCError(err.Error(), context) + } + if blockRegion == nil { + return "", "", rpcNoTxInfoError(txHash) + } + + // Load the raw transaction bytes from the database. + var txBytes []byte + err = s.cfg.DB.View(func(dbTx database.Tx) error { + var err error + txBytes, err = dbTx.FetchBlockRegion(blockRegion) + return err + }) + if err != nil { + return "", "", rpcNoTxInfoError(txHash) + } + + // Deserialize the transaction + var msgTx wire.MsgTx + err = msgTx.Deserialize(bytes.NewReader(txBytes)) + if err != nil { + context := "Failed to deserialize transaction" + return "", "", internalRPCError(err.Error(), context) + } + + txo := msgTx.TxOut[outpoint.Index] + cs, err := txscript.DecodeClaimScript(txo.PkScript) + if err != nil { + context := "Failed to decode the claim script" + return "", "", internalRPCError(err.Error(), context) + } + + _, addresses, _, _ := txscript.ExtractPkScriptAddrs(txo.PkScript[cs.Size():], s.cfg.ChainParams) + return addresses[0].EncodeAddress(), hex.EncodeToString(cs.Value()), nil +} + +func handleGetNormalized(_ *rpcServer, cmd interface{}, _ <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.GetNormalizedCmd) + r := btcjson.GetNormalizedResult{ + NormalizedName: string(node.Normalize([]byte(c.Name))), + } + return r, nil +}