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/wire v1.5.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/dustin/go-humanize v1.0.1
|
||||
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/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM=
|
||||
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/go.mod h1:THsO8aBSwWBq6ZsIG25cNqbkNb+EEASXzLhFvODVc0s=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user