Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions SYSCOIN_5_API_UPDATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
- **Purpose**: Returns filtered list of assets matching search criteria
- **Response**: Paginated list of assets with basic information

#### Send transaction, optional params
- **Endpoint**: `POST` or `GET` `/api/v2/sendtx/`; explorer `POST` sendtx
- **Purpose**: Optional `maxfeerate` / `maxburnamount` for Syscoin Core `sendrawtransaction` (JSON body starting with `{` and `hex` field, else raw hex; `GET` query string; optional form fields) — **Syscoin** only; the defaults are set to **0.10** and **150** SYS
- **Response**: Same as before (`result` = txid)

### 3. Enhanced Transaction Responses

#### SPT Transaction Support
Expand Down
10 changes: 10 additions & 0 deletions bchain/coins/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@ func (c *blockChainWithMetrics) SendRawTransaction(tx string) (v string, err err
return c.b.SendRawTransaction(tx)
}

func (c *blockChainWithMetrics) SendRawTransactionWithOpts(p bchain.SendRawTransactionParams) (string, error) {
o, ok := c.b.(bchain.SendRawTransactionOpts)
if !ok {
return "", fmt.Errorf("maxfeerate and maxburnamount are supported only on Syscoin")
}
var err error
defer func(s time.Time) { c.observeRPCLatency("SendRawTransactionWithOpts", s, err) }(time.Now())
return o.SendRawTransactionWithOpts(p)
}

func (c *blockChainWithMetrics) GetMempoolEntry(txid string) (v *bchain.MempoolEntry, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetMempoolEntry", s, err) }(time.Now())
return c.b.GetMempoolEntry(txid)
Expand Down
33 changes: 27 additions & 6 deletions bchain/coins/btc/bitcoinrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,8 @@ type ResEstimateFee struct {
// sendrawtransaction

type CmdSendRawTransaction struct {
Method string `json:"method"`
Params []string `json:"params"`
Method string `json:"method"`
Params []interface{} `json:"params"`
}

type ResSendRawTransaction struct {
Expand Down Expand Up @@ -887,13 +887,29 @@ func (b *BitcoinRPC) EstimateFee(blocks int) (big.Int, error) {
return r, nil
}

// SendRawTransaction sends raw transaction
func (b *BitcoinRPC) SendRawTransaction(tx string) (string, error) {
func nonEmptyPtr(s *string) bool {
return s != nil && *s != ""
}

// SendRawTransactionWithParams calls sendrawtransaction with optional Core-compatible
// maxfeerate / maxburnamount. Package-level function so types embedding BitcoinRPC do not
// promote it as SendRawTransactionOpts (only SyscoinRPC implements that interface).
func SendRawTransactionWithParams(b *BitcoinRPC, p bchain.SendRawTransactionParams) (string, error) {
glog.V(1).Info("rpc: sendrawtransaction")

params := []interface{}{p.Hex}
if nonEmptyPtr(p.MaxFeeRate) {
params = append(params, *p.MaxFeeRate)
}
if nonEmptyPtr(p.MaxBurnAmount) {
if !nonEmptyPtr(p.MaxFeeRate) {
return "", errors.New("maxfeerate is required when maxburnamount is set")
}
params = append(params, *p.MaxBurnAmount)
}

res := ResSendRawTransaction{}
req := CmdSendRawTransaction{Method: "sendrawtransaction"}
req.Params = []string{tx}
req := CmdSendRawTransaction{Method: "sendrawtransaction", Params: params}
err := b.Call(&req, &res)

if err != nil {
Expand All @@ -905,6 +921,11 @@ func (b *BitcoinRPC) SendRawTransaction(tx string) (string, error) {
return res.Result, nil
}

// SendRawTransaction sends raw transaction
func (b *BitcoinRPC) SendRawTransaction(tx string) (string, error) {
return SendRawTransactionWithParams(b, bchain.SendRawTransactionParams{Hex: tx})
}

// GetMempoolEntry returns mempool data for given transaction
func (b *BitcoinRPC) GetMempoolEntry(txid string) (*bchain.MempoolEntry, error) {
glog.V(1).Info("rpc: getmempoolentry")
Expand Down
52 changes: 52 additions & 0 deletions bchain/coins/sys/syscoinparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,58 @@ func TestMain(m *testing.M) {
os.Exit(c)
}

func TestSyscoinSendRawParams(t *testing.T) {
customFeeRate := "0.25"
customBurnAmount := "42"
empty := ""

tests := []struct {
name string
params bchain.SendRawTransactionParams
wantFeeRate string
wantBurnAmount string
}{
{
name: "defaults both optional params",
params: bchain.SendRawTransactionParams{Hex: "abc"},
wantFeeRate: defaultSyscoinMaxFeeRate,
wantBurnAmount: defaultSyscoinMaxBurnAmount,
},
{
name: "preserves explicit params",
params: bchain.SendRawTransactionParams{
Hex: "abc",
MaxFeeRate: &customFeeRate,
MaxBurnAmount: &customBurnAmount,
},
wantFeeRate: customFeeRate,
wantBurnAmount: customBurnAmount,
},
{
name: "defaults empty strings",
params: bchain.SendRawTransactionParams{
Hex: "abc",
MaxFeeRate: &empty,
MaxBurnAmount: &empty,
},
wantFeeRate: defaultSyscoinMaxFeeRate,
wantBurnAmount: defaultSyscoinMaxBurnAmount,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := syscoinSendRawParams(tt.params)
if got.MaxFeeRate == nil || *got.MaxFeeRate != tt.wantFeeRate {
t.Errorf("MaxFeeRate = %v, want %v", got.MaxFeeRate, tt.wantFeeRate)
}
if got.MaxBurnAmount == nil || *got.MaxBurnAmount != tt.wantBurnAmount {
t.Errorf("MaxBurnAmount = %v, want %v", got.MaxBurnAmount, tt.wantBurnAmount)
}
})
}
}

func Test_GetAddrDescFromAddress_Mainnet(t *testing.T) {
type args struct {
address string
Expand Down
36 changes: 35 additions & 1 deletion bchain/coins/sys/syscoinrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,38 @@ func (b *SyscoinRPC) GetSPVProof(hash string) (string, error) {
// It could be optimized for mempool, i.e. without block time and confirmations
func (b *SyscoinRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {
return b.GetTransaction(txid)
}
}

// Syscoin Core sendrawtransaction default maxfeerate is 0.10.
// Syscoin Core sendrawtransaction default maxburnamount is 0.0.
// Governance Proposal needs maxburnamount of 150.
// This change allows sending governance proposals without explicitly setting both parameters.
const (
defaultSyscoinMaxFeeRate = "0.10"
defaultSyscoinMaxBurnAmount = "150"
)

func syscoinSendRawParams(p bchain.SendRawTransactionParams) bchain.SendRawTransactionParams {
if p.MaxFeeRate == nil || *p.MaxFeeRate == "" {
s := defaultSyscoinMaxFeeRate
p.MaxFeeRate = &s
}
if p.MaxBurnAmount == nil || *p.MaxBurnAmount == "" {
s := defaultSyscoinMaxBurnAmount
p.MaxBurnAmount = &s
}
return p
}

// Override BitcoinRPC to apply Syscoin default maxfeerate / maxburnamount.
func (b *SyscoinRPC) SendRawTransaction(tx string) (string, error) {
return b.SendRawTransactionWithOpts(bchain.SendRawTransactionParams{Hex: tx})
}

// Forwards maxfeerate / maxburnamount to Syscoin Core sendrawtransaction.
func (b *SyscoinRPC) SendRawTransactionWithOpts(p bchain.SendRawTransactionParams) (string, error) {
p = syscoinSendRawParams(p)
return btc.SendRawTransactionWithParams(b.BitcoinRPC, p)
}

var _ bchain.SendRawTransactionOpts = (*SyscoinRPC)(nil)
15 changes: 15 additions & 0 deletions bchain/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,21 @@ const (
AddressBalanceDetailUTXOIndexed = 2
)

// SendRawTransactionParams carries optional arguments for Syscoin Core sendrawtransaction
// (maxfeerate, maxburnamount). Used with SendRawTransactionOpts.
// The implementation (see syscoinrpc.go) takes care of the proper optional argument settings.
type SendRawTransactionParams struct {
Hex string
MaxFeeRate *string
MaxBurnAmount *string
}

// SendRawTransactionOpts is implemented by chains whose node supports
// sendrawtransaction with maxfeerate / maxburnamount (Syscoin).
type SendRawTransactionOpts interface {
SendRawTransactionWithOpts(p SendRawTransactionParams) (string, error)
}

// BlockChain defines common interface to block chain daemon
type BlockChain interface {
// life-cycle methods
Expand Down
68 changes: 61 additions & 7 deletions server/public.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package server

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"io/ioutil"
Expand Down Expand Up @@ -991,9 +993,16 @@ func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (t
if err != nil {
return sendTransactionTpl, data, err
}
hex := r.FormValue("hex")
hex := strings.TrimSpace(r.FormValue("hex"))
if len(hex) > 0 {
res, err := s.chain.SendRawTransaction(hex)
p := bchain.SendRawTransactionParams{Hex: hex}
if v := strings.TrimSpace(r.FormValue("maxfeerate")); v != "" {
p.MaxFeeRate = &v
}
if v := strings.TrimSpace(r.FormValue("maxburnamount")); v != "" {
p.MaxBurnAmount = &v
}
res, err := s.submitSendRawTx(p)
if err != nil {
data.SendTxHex = hex
data.Error = &api.APIError{Text: err.Error(), Public: true}
Expand Down Expand Up @@ -1381,24 +1390,69 @@ type resultSendTransaction struct {
Result string `json:"result"`
}

func optionalNonEmptyQueryString(r *http.Request, key string) *string {
v := strings.TrimSpace(r.URL.Query().Get(key))
if v == "" {
return nil
}
return &v
}

func sendTxOptsRequested(p bchain.SendRawTransactionParams) bool {
return nonEmptyOptionalStr(p.MaxFeeRate) || nonEmptyOptionalStr(p.MaxBurnAmount)
}

func nonEmptyOptionalStr(s *string) bool {
return s != nil && *s != ""
}

func (s *PublicServer) submitSendRawTx(p bchain.SendRawTransactionParams) (string, error) {
if sendTxOptsRequested(p) {
o, ok := s.chain.(bchain.SendRawTransactionOpts)
if !ok {
return "", errors.New("maxfeerate and maxburnamount are supported only on Syscoin")
}
return o.SendRawTransactionWithOpts(p)
}
return s.chain.SendRawTransaction(p.Hex)
}

func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
var err error
var res resultSendTransaction
var hex string
var p bchain.SendRawTransactionParams
s.metrics.ExplorerViews.With(common.Labels{"action": "api-sendtx"}).Inc()
if r.Method == http.MethodPost {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, api.NewAPIError("Missing tx blob", true)
}
hex = string(data)
data = bytes.TrimSpace(data)
if len(data) > 0 && data[0] == '{' {
var body struct {
Hex string `json:"hex"`
MaxFeeRate *string `json:"maxfeerate,omitempty"`
MaxBurnAmount *string `json:"maxburnamount,omitempty"`
}
if err := json.Unmarshal(data, &body); err != nil {
return nil, api.NewAPIError("Invalid JSON body", true)
}
p.Hex = strings.TrimSpace(body.Hex)
p.MaxFeeRate = body.MaxFeeRate
p.MaxBurnAmount = body.MaxBurnAmount
} else {
p.Hex = string(data)
}
} else {
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
hex = r.URL.Path[i+1:]
p.Hex = r.URL.Path[i+1:]
}
p.MaxFeeRate = optionalNonEmptyQueryString(r, "maxfeerate")
p.MaxBurnAmount = optionalNonEmptyQueryString(r, "maxburnamount")
}
if len(hex) > 0 {
res.Result, err = s.chain.SendRawTransaction(hex)
p.Hex = strings.TrimSpace(p.Hex)
if len(p.Hex) > 0 {
res.Result, err = s.submitSendRawTx(p)
if err != nil {
return nil, api.NewAPIError(err.Error(), true)
}
Expand Down
18 changes: 18 additions & 0 deletions server/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,24 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
`{"result":"9876"}`,
},
},
{
name: "apiSendTx POST JSON",
r: newPostRequest(ts.URL+"/api/v2/sendtx/", `{"hex":"123456"}`),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"result":"9876"}`,
},
},
{
name: "apiSendTx POST JSON maxburn unsupported",
r: newPostRequest(ts.URL+"/api/v2/sendtx/", `{"hex":"123456","maxburnamount":"1"}`),
status: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: []string{
`{"error":"maxfeerate and maxburnamount are supported only on Syscoin"}`,
},
},
{
name: "apiSendTx POST empty",
r: newPostRequest(ts.URL+"/api/v2/sendtx", ""),
Expand Down