Add v3tool. (#366)
This commit adds a new executable - v3tool - a developer testing tool which hits endpoints of the vspd API.
This commit is contained in:
parent
0c06145d67
commit
b5ac64891e
21
cmd/v3tool/README.md
Normal file
21
cmd/v3tool/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# v3tool
|
||||||
|
|
||||||
|
v3tool is a simple client for manual testing of vspd.
|
||||||
|
It is a developer tool, not suitable for end users or production use.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. An instance of dcrwallet which owns at least one immature or live ticket.
|
||||||
|
1. An instance of vspd to test.
|
||||||
|
|
||||||
|
## What v3tool does
|
||||||
|
|
||||||
|
1. Retrieve the pubkey from vspd.
|
||||||
|
1. Retrieve the list of owned immature/live tickets from dcrwallet.
|
||||||
|
1. For each ticket:
|
||||||
|
1. Use dcrwallet to find the tx hex, voting privkey and commitment address of the ticket.
|
||||||
|
1. Get a fee address and amount from vspd to register this ticket.
|
||||||
|
1. Create the fee tx and send it to vspd.
|
||||||
|
1. Get the ticket status.
|
||||||
|
1. Change vote choices on the ticket.
|
||||||
|
1. Get the ticket status again.
|
||||||
165
cmd/v3tool/dcrwallet.go
Normal file
165
cmd/v3tool/dcrwallet.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
wallettypes "decred.org/dcrwallet/v2/rpc/jsonrpc/types"
|
||||||
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
||||||
|
"github.com/decred/dcrd/chaincfg/v3"
|
||||||
|
"github.com/decred/dcrd/dcrutil/v4"
|
||||||
|
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
|
||||||
|
"github.com/decred/dcrd/txscript/v4/stdaddr"
|
||||||
|
"github.com/decred/dcrd/txscript/v4/stdscript"
|
||||||
|
"github.com/decred/dcrd/wire"
|
||||||
|
"github.com/jrick/wsrpc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dcrwallet struct {
|
||||||
|
*wsrpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWalletRPC(rpcURL, rpcUser, rpcPass string) (*dcrwallet, error) {
|
||||||
|
tlsOpt := wsrpc.WithTLSConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})
|
||||||
|
authOpt := wsrpc.WithBasicAuth(rpcUser, rpcPass)
|
||||||
|
rpc, err := wsrpc.Dial(context.TODO(), rpcURL, tlsOpt, authOpt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dcrwallet{rpc}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dcrwallet) createFeeTx(feeAddress string, fee int64) (string, error) {
|
||||||
|
amounts := make(map[string]float64)
|
||||||
|
amounts[feeAddress] = dcrutil.Amount(fee).ToCoin()
|
||||||
|
|
||||||
|
var msgtxstr string
|
||||||
|
err := w.Call(context.TODO(), "createrawtransaction", &msgtxstr, nil, amounts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
zero := int32(0)
|
||||||
|
opt := wallettypes.FundRawTransactionOptions{
|
||||||
|
ConfTarget: &zero,
|
||||||
|
}
|
||||||
|
var fundTx wallettypes.FundRawTransactionResult
|
||||||
|
err = w.Call(context.TODO(), "fundrawtransaction", &fundTx, msgtxstr, "default", &opt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := wire.NewMsgTx()
|
||||||
|
err = tx.Deserialize(hex.NewDecoder(strings.NewReader(fundTx.Hex)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions := make([]dcrdtypes.TransactionInput, 0)
|
||||||
|
|
||||||
|
for _, v := range tx.TxIn {
|
||||||
|
transactions = append(transactions, dcrdtypes.TransactionInput{
|
||||||
|
Txid: v.PreviousOutPoint.Hash.String(),
|
||||||
|
Vout: v.PreviousOutPoint.Index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var locked bool
|
||||||
|
unlock := false
|
||||||
|
err = w.Call(context.TODO(), "lockunspent", &locked, unlock, transactions)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !locked {
|
||||||
|
return "", errors.New("unspent output not locked")
|
||||||
|
}
|
||||||
|
|
||||||
|
var signedTx wallettypes.SignRawTransactionResult
|
||||||
|
err = w.Call(context.TODO(), "signrawtransaction", &signedTx, fundTx.Hex)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !signedTx.Complete {
|
||||||
|
return "", fmt.Errorf("not all signed")
|
||||||
|
}
|
||||||
|
return signedTx.Hex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dcrwallet) SignMessage(ctx context.Context, msg string, commitmentAddr stdaddr.Address) ([]byte, error) {
|
||||||
|
var signature string
|
||||||
|
err := w.Call(context.TODO(), "signmessage", &signature, commitmentAddr.String(), msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.DecodeString(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dcrwallet) dumpPrivKey(addr stdaddr.Address) (string, error) {
|
||||||
|
var privKeyStr string
|
||||||
|
err := w.Call(context.TODO(), "dumpprivkey", &privKeyStr, addr.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return privKeyStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dcrwallet) getTickets() (*wallettypes.GetTicketsResult, error) {
|
||||||
|
var tickets wallettypes.GetTicketsResult
|
||||||
|
includeImmature := true
|
||||||
|
err := w.Call(context.TODO(), "gettickets", &tickets, includeImmature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tickets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTicketDetails returns the ticket hex, privkey for voting, and the
|
||||||
|
// commitment address.
|
||||||
|
func (w *dcrwallet) getTicketDetails(ticketHash string) (string, string, stdaddr.Address, error) {
|
||||||
|
var getTransactionResult wallettypes.GetTransactionResult
|
||||||
|
err := w.Call(context.TODO(), "gettransaction", &getTransactionResult, ticketHash, false)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("gettransaction: %v\n", err)
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msgTx := wire.NewMsgTx()
|
||||||
|
if err = msgTx.Deserialize(hex.NewDecoder(strings.NewReader(getTransactionResult.Hex))); err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
if len(msgTx.TxOut) < 2 {
|
||||||
|
return "", "", nil, errors.New("msgTx.TxOut < 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptVersion = 0
|
||||||
|
scriptType, submissionAddr := stdscript.ExtractAddrs(scriptVersion,
|
||||||
|
msgTx.TxOut[0].PkScript, chaincfg.TestNet3Params())
|
||||||
|
if scriptType == stdscript.STNonStandard {
|
||||||
|
return "", "", nil, fmt.Errorf("invalid script version %d", scriptVersion)
|
||||||
|
}
|
||||||
|
if len(submissionAddr) != 1 {
|
||||||
|
return "", "", nil, errors.New("submissionAddr != 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript,
|
||||||
|
chaincfg.TestNet3Params())
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privKeyStr, err := w.dumpPrivKey(submissionAddr[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return getTransactionResult.Hex, privKeyStr, addr, nil
|
||||||
|
}
|
||||||
208
cmd/v3tool/main.go
Normal file
208
cmd/v3tool/main.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/decred/slog"
|
||||||
|
"github.com/decred/vspd/client"
|
||||||
|
"github.com/decred/vspd/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vspdURL = "http://localhost:8800"
|
||||||
|
// dcrwallet RPC.
|
||||||
|
rpcURL = "wss://localhost:19110/ws"
|
||||||
|
rpcUser = "user"
|
||||||
|
rpcPass = "pass"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVspPubKey(url string) ([]byte, error) {
|
||||||
|
resp, err := http.Get(url + "/api/v3/vspinfo")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var j types.VspInfoResponse
|
||||||
|
err = json.Unmarshal(b, &j)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.ValidateServerSignature(resp, b, j.PubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.PubKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() int {
|
||||||
|
log := slog.NewBackend(os.Stdout).Logger("")
|
||||||
|
log.SetLevel(slog.LevelTrace)
|
||||||
|
|
||||||
|
walletRPC, err := newWalletRPC(rpcURL, rpcUser, rpcPass)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer walletRPC.Close()
|
||||||
|
|
||||||
|
log.Infof("vpsd url: %s", vspdURL)
|
||||||
|
|
||||||
|
pubKey, err := getVspPubKey(vspdURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("vspd pubkey: %x", pubKey)
|
||||||
|
|
||||||
|
vClient := client.Client{
|
||||||
|
URL: vspdURL,
|
||||||
|
PubKey: pubKey,
|
||||||
|
Sign: walletRPC.SignMessage,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of tickets
|
||||||
|
tickets, err := walletRPC.getTickets()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tickets.Hashes) == 0 {
|
||||||
|
log.Errorf("wallet owns no tickets")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("wallet returned %d ticket(s):", len(tickets.Hashes))
|
||||||
|
for _, tkt := range tickets.Hashes {
|
||||||
|
log.Infof(" %s", tkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(tickets.Hashes); i++ {
|
||||||
|
ticketHash := tickets.Hashes[i]
|
||||||
|
hex, privKeyStr, commitmentAddr, err := walletRPC.getTicketDetails(ticketHash)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("")
|
||||||
|
log.Infof("Processing ticket %d of %d:", i+1, len(tickets.Hashes))
|
||||||
|
log.Infof(" Hash: %s", ticketHash)
|
||||||
|
log.Infof(" privKeyStr: %s", privKeyStr)
|
||||||
|
log.Infof(" commitmentAddr: %s", commitmentAddr)
|
||||||
|
log.Infof("")
|
||||||
|
|
||||||
|
feeAddrReq := types.FeeAddressRequest{
|
||||||
|
TicketHex: hex,
|
||||||
|
// Hack for ParentHex, can't be bothered to get the real one. It doesn't
|
||||||
|
// make a difference when testing locally anyway.
|
||||||
|
ParentHex: hex,
|
||||||
|
TicketHash: ticketHash,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
feeAddrResp, err := vClient.FeeAddress(context.TODO(), feeAddrReq, commitmentAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getFeeAddress error: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("feeAddress: %v", feeAddrResp.FeeAddress)
|
||||||
|
log.Infof("privKeyStr: %v", privKeyStr)
|
||||||
|
|
||||||
|
feeTx, err := walletRPC.createFeeTx(feeAddrResp.FeeAddress, feeAddrResp.FeeAmount)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("createFeeTx error: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
voteChoices := map[string]string{"autorevocations": "no"}
|
||||||
|
tspend := map[string]string{
|
||||||
|
"6c78690fa2fa31803df0376897725704e9dc19ecbdf80061e79b69de93ca1360": "no",
|
||||||
|
"abb86660dda1f1b66544bab24a823a22e9213ada48649f0d913623f49e17dacb": "yes",
|
||||||
|
}
|
||||||
|
treasury := map[string]string{
|
||||||
|
"03f6e7041f1cf51ee10e0a01cd2b0385ce3cd9debaabb2296f7e9dee9329da946c": "no",
|
||||||
|
"0319a37405cb4d1691971847d7719cfce70857c0f6e97d7c9174a3998cf0ab86dd": "yes",
|
||||||
|
}
|
||||||
|
|
||||||
|
payFeeReq := types.PayFeeRequest{
|
||||||
|
FeeTx: feeTx,
|
||||||
|
VotingKey: privKeyStr,
|
||||||
|
TicketHash: ticketHash,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
VoteChoices: voteChoices,
|
||||||
|
TSpendPolicy: tspend,
|
||||||
|
TreasuryPolicy: treasury,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = vClient.PayFee(context.TODO(), payFeeReq, commitmentAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("payFee error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketStatusReq := types.TicketStatusRequest{
|
||||||
|
TicketHash: ticketHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = vClient.TicketStatus(context.TODO(), ticketStatusReq, commitmentAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getTicketStatus error: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
voteChoices["autorevocations"] = "yes"
|
||||||
|
|
||||||
|
// Sleep to ensure a new timestamp. vspd will reject old/reused timestamps.
|
||||||
|
time.Sleep(1001 * time.Millisecond)
|
||||||
|
|
||||||
|
voteChoiceReq := types.SetVoteChoicesRequest{
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
TicketHash: ticketHash,
|
||||||
|
VoteChoices: voteChoices,
|
||||||
|
TSpendPolicy: tspend,
|
||||||
|
TreasuryPolicy: treasury,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = vClient.SetVoteChoices(context.TODO(), voteChoiceReq, commitmentAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("setVoteChoices error: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = vClient.TicketStatus(context.TODO(), ticketStatusReq, commitmentAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getTicketStatus error: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
os.Exit(run())
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
@ -15,6 +15,7 @@ require (
|
|||||||
github.com/decred/dcrd/txscript/v4 v4.0.0
|
github.com/decred/dcrd/txscript/v4 v4.0.0
|
||||||
github.com/decred/dcrd/wire v1.5.0
|
github.com/decred/dcrd/wire v1.5.0
|
||||||
github.com/decred/slog v1.2.0
|
github.com/decred/slog v1.2.0
|
||||||
|
github.com/decred/vspd/client v1.1.0
|
||||||
github.com/decred/vspd/types v1.1.0
|
github.com/decred/vspd/types v1.1.0
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gin-gonic/gin v1.8.2
|
github.com/gin-gonic/gin v1.8.2
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -69,6 +69,8 @@ github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQ
|
|||||||
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
|
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
|
||||||
github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM=
|
github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM=
|
||||||
github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
|
github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
|
||||||
|
github.com/decred/vspd/client v1.1.0 h1:vyMjbT9jGuH96yNsv2msx5E/DP7xk3ePIU7XWhu1Des=
|
||||||
|
github.com/decred/vspd/client v1.1.0/go.mod h1:+gn860mKciikYxQrPm+hSVMEG8CQ7EWf/vN9pZa3Baw=
|
||||||
github.com/decred/vspd/types v1.1.0 h1:hTeqQwgRUN2FGIbuCIdyzBejKV+jxKrmEIcLKxpsB1g=
|
github.com/decred/vspd/types v1.1.0 h1:hTeqQwgRUN2FGIbuCIdyzBejKV+jxKrmEIcLKxpsB1g=
|
||||||
github.com/decred/vspd/types v1.1.0/go.mod h1:THsO8aBSwWBq6ZsIG25cNqbkNb+EEASXzLhFvODVc0s=
|
github.com/decred/vspd/types v1.1.0/go.mod h1:THsO8aBSwWBq6ZsIG25cNqbkNb+EEASXzLhFvODVc0s=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user