Split voting wallet and fee wallet clients (#45)
This commit is contained in:
parent
033ac95c33
commit
869b68fad5
58
README.md
58
README.md
@ -4,17 +4,54 @@
|
||||
[](http://copyfree.org)
|
||||
[](https://goreportcard.com/report/github.com/jholdstock/dcrvsp)
|
||||
|
||||
## Design decisions
|
||||
## Overview
|
||||
|
||||
- [gin-gonic](https://github.com/gin-gonic/gin) webserver for both front-end and API.
|
||||
User purchases a ticket, doesnt need any special conditions, indistinguishable
|
||||
from solo ticket. User can then choose to use a VSP on a per-ticket basis. Once
|
||||
the ticket is mined, and ideally before it has matured, the user sends the
|
||||
ticket details + fee to a VSP, and the VSP will take the fee and vote in return.
|
||||
|
||||
## Advantages
|
||||
|
||||
### For Administrators
|
||||
|
||||
- bbolt db.
|
||||
- No stakepoold.
|
||||
- Client accountability.
|
||||
|
||||
### For Users
|
||||
|
||||
- No redeem script to back up.
|
||||
- No registration required. No email.
|
||||
- Multiple VSPs on a single ticket.
|
||||
- Voting preferences per ticket.
|
||||
- Server accountability.
|
||||
- No address reuse.
|
||||
- VSP fees are paid "out of band", rather than being included in the ticket
|
||||
itself. This makes solo tickets and VSP tickets indistinguishable from
|
||||
eachother, enabling VSP users to purchase tickets in the same anonymity set
|
||||
as solo stakers.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- [gin-gonic](https://github.com/gin-gonic/gin) webserver.
|
||||
- Success responses use HTTP status 200 and a JSON encoded body.
|
||||
- Error responses use either HTTP status 500 or 400, and a JSON encoded error in the body (eg. `{"error":"Description"}')
|
||||
- Error responses use either HTTP status 500 or 400, and a JSON encoded error
|
||||
in the body (eg. `{"error":"Description"}')
|
||||
- [bbolt](https://github.com/etcd-io/bbolt) k/v database.
|
||||
- Tickets are stored in a single bucket, using ticket hash as the key and a
|
||||
json encoded representation of the ticket as the value.
|
||||
- [wsrpc](https://github.com/jrick/wsrpc) for dcrwallet comms.
|
||||
- [wsrpc](https://github.com/jrick/wsrpc) for RPC communication between dcrvsp
|
||||
and dcrwallet.
|
||||
|
||||
## MVP features
|
||||
## Architecture
|
||||
|
||||
- Single server running dcrvsp, dcrwallet and dcrd. dcrd requires txindex so
|
||||
`getrawtransaction` can be used.
|
||||
- Multiple remote "Voting servers", each running dcrwallet and dcrd. dcrwallet
|
||||
on these servers should be constantly unlocked and have voting enabled.
|
||||
|
||||
## MVP Features
|
||||
|
||||
- When dcrvsp is started for the first time, it generates a ed25519 keypair and
|
||||
stores it in the database. This key is used to sign all API responses, and the
|
||||
@ -29,11 +66,12 @@
|
||||
- Pay fee (`POST /payFee`)
|
||||
- Ticket status (`GET /ticketstatus`)
|
||||
- Set voting preferences (`POST /setvotechoices`)
|
||||
- A minimal, static, web front-end providing pool stats and basic connection instructions.
|
||||
- A minimal, static, web front-end providing pool stats and basic connection
|
||||
instructions.
|
||||
- Fees have an expiry period. If the fee is not paid within this period, the
|
||||
client must request a new fee. This enables the VSP to alter its fee rate.
|
||||
|
||||
## Future features
|
||||
## Future Features
|
||||
|
||||
- Write database backups to disk periodically.
|
||||
- Backup over http.
|
||||
@ -41,9 +79,11 @@
|
||||
- Accountability for both client and server changes to voting preferences.
|
||||
- Consistency checking across connected wallets.
|
||||
|
||||
## Notes
|
||||
## Backup and Recovery
|
||||
|
||||
- dcrd must have transaction index enabled so `getrawtransaction` can be used.
|
||||
- Regular backups of bbolt database.
|
||||
- Restore requires manual repair of fee wallet. Import xpub into account "fees",
|
||||
and rescan with a very large gap limit.
|
||||
|
||||
## Issue Tracker
|
||||
|
||||
|
||||
78
config.go
78
config.go
@ -24,7 +24,8 @@ var (
|
||||
defaultHomeDir = dcrutil.AppDataDir("dcrvsp", false)
|
||||
defaultConfigFilename = "dcrvsp.conf"
|
||||
defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename)
|
||||
defaultWalletHost = "127.0.0.1"
|
||||
defaultFeeWalletHost = "127.0.0.1"
|
||||
defaultVotingWalletHost = "127.0.0.1"
|
||||
defaultWebServerDebug = false
|
||||
)
|
||||
|
||||
@ -37,15 +38,20 @@ type config struct {
|
||||
VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."`
|
||||
HomeDir string `long:"homedir" ini-name:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."`
|
||||
ConfigFile string `long:"configfile" ini-name:"configfile" no-ini:"true" description:"Path to configuration file."`
|
||||
WalletHost string `long:"wallethost" ini-name:"wallethost" description:"The ip:port to establish a JSON-RPC connection with dcrwallet."`
|
||||
WalletUser string `long:"walletuser" ini-name:"walletuser" description:"Username for dcrwallet RPC connections."`
|
||||
WalletPass string `long:"walletpass" ini-name:"walletpass" description:"Password for dcrwallet RPC connections."`
|
||||
WalletCert string `long:"walletcert" ini-name:"walletcert" description:"The dcrwallet RPC certificate file."`
|
||||
FeeWalletHost string `long:"feewallethost" ini-name:"feewallethost" description:"The ip:port to establish a JSON-RPC connection with fee dcrwallet."`
|
||||
FeeWalletUser string `long:"feewalletuser" ini-name:"feewalletuser" description:"Username for fee dcrwallet RPC connections."`
|
||||
FeeWalletPass string `long:"feewalletpass" ini-name:"feewalletpass" description:"Password for fee dcrwallet RPC connections."`
|
||||
FeeWalletCert string `long:"feewalletcert" ini-name:"feewalletcert" description:"The fee dcrwallet RPC certificate file."`
|
||||
VotingWalletHost string `long:"votingwallethost" ini-name:"votingwallethost" description:"The ip:port to establish a JSON-RPC connection with voting dcrwallet."`
|
||||
VotingWalletUser string `long:"votingwalletuser" ini-name:"votingwalletuser" description:"Username for voting dcrwallet RPC connections."`
|
||||
VotingWalletPass string `long:"votingwalletpass" ini-name:"votingwalletpass" description:"Password for voting dcrwallet RPC connections."`
|
||||
VotingWalletCert string `long:"votingwalletcert" ini-name:"votingwalletcert" description:"The voting dcrwallet RPC certificate file."`
|
||||
WebServerDebug bool `long:"webserverdebug" ini-name:"webserverdebug" description:"Enable web server debug mode (verbose logging to terminal and live-reloading templates)."`
|
||||
|
||||
dbPath string
|
||||
netParams *netParams
|
||||
dcrwCert []byte
|
||||
feeWalletCert []byte
|
||||
votingWalletCert []byte
|
||||
}
|
||||
|
||||
// fileExists reports whether the named file or directory exists.
|
||||
@ -142,7 +148,8 @@ func loadConfig() (*config, error) {
|
||||
VSPFee: defaultVSPFee,
|
||||
HomeDir: defaultHomeDir,
|
||||
ConfigFile: defaultConfigFile,
|
||||
WalletHost: defaultWalletHost,
|
||||
FeeWalletHost: defaultFeeWalletHost,
|
||||
VotingWalletHost: defaultVotingWalletHost,
|
||||
WebServerDebug: defaultWebServerDebug,
|
||||
}
|
||||
|
||||
@ -233,30 +240,53 @@ func loadConfig() (*config, error) {
|
||||
cfg.netParams = &simNetParams
|
||||
}
|
||||
|
||||
// Ensure the dcrwallet RPC username is set.
|
||||
if cfg.WalletUser == "" {
|
||||
return nil, errors.New("the walletuser option is not set")
|
||||
// Ensure the fee dcrwallet RPC username is set.
|
||||
if cfg.FeeWalletUser == "" {
|
||||
return nil, errors.New("the feewalletuser option is not set")
|
||||
}
|
||||
|
||||
// Ensure the dcrwallet RPC password is set.
|
||||
if cfg.WalletPass == "" {
|
||||
return nil, errors.New("the walletpass option is not set")
|
||||
// Ensure the fee dcrwallet RPC password is set.
|
||||
if cfg.FeeWalletPass == "" {
|
||||
return nil, errors.New("the feewalletpass option is not set")
|
||||
}
|
||||
|
||||
// Ensure the dcrwallet RPC cert path is set.
|
||||
if cfg.WalletCert == "" {
|
||||
return nil, errors.New("the walletcert option is not set")
|
||||
// Ensure the fee dcrwallet RPC cert path is set.
|
||||
if cfg.FeeWalletCert == "" {
|
||||
return nil, errors.New("the feewalletcert option is not set")
|
||||
}
|
||||
|
||||
// Load fee dcrwallet RPC certificate.
|
||||
cfg.FeeWalletCert = cleanAndExpandPath(cfg.FeeWalletCert)
|
||||
cfg.feeWalletCert, err = ioutil.ReadFile(cfg.FeeWalletCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read fee dcrwallet cert file: %v", err)
|
||||
}
|
||||
|
||||
// Ensure the voting dcrwallet RPC username is set.
|
||||
if cfg.VotingWalletUser == "" {
|
||||
return nil, errors.New("the votingwalletuser option is not set")
|
||||
}
|
||||
|
||||
// Ensure the voting dcrwallet RPC password is set.
|
||||
if cfg.VotingWalletPass == "" {
|
||||
return nil, errors.New("the votingwalletpass option is not set")
|
||||
}
|
||||
|
||||
// Ensure the voting dcrwallet RPC cert path is set.
|
||||
if cfg.VotingWalletCert == "" {
|
||||
return nil, errors.New("the votingwalletcert option is not set")
|
||||
}
|
||||
|
||||
// Load voting dcrwallet RPC certificate.
|
||||
cfg.VotingWalletCert = cleanAndExpandPath(cfg.VotingWalletCert)
|
||||
cfg.votingWalletCert, err = ioutil.ReadFile(cfg.VotingWalletCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read voting dcrwallet cert file: %v", err)
|
||||
}
|
||||
|
||||
// Add default port for the active network if there is no port specified.
|
||||
cfg.WalletHost = normalizeAddress(cfg.WalletHost, cfg.netParams.WalletRPCServerPort)
|
||||
|
||||
// Load dcrwallet RPC certificate.
|
||||
cfg.WalletCert = cleanAndExpandPath(cfg.WalletCert)
|
||||
cfg.dcrwCert, err = ioutil.ReadFile(cfg.WalletCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read dcrwallet cert file: %v", err)
|
||||
}
|
||||
cfg.FeeWalletHost = normalizeAddress(cfg.FeeWalletHost, cfg.netParams.WalletRPCServerPort)
|
||||
cfg.VotingWalletHost = normalizeAddress(cfg.VotingWalletHost, cfg.netParams.WalletRPCServerPort)
|
||||
|
||||
// Create the data directory.
|
||||
dataDir := filepath.Join(cfg.HomeDir, "data", cfg.netParams.Name)
|
||||
|
||||
54
main.go
54
main.go
@ -11,7 +11,6 @@ import (
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
"github.com/jholdstock/dcrvsp/webapi"
|
||||
"github.com/jrick/wsrpc/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -59,11 +58,36 @@ func run(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create dcrwallet RPC client.
|
||||
walletRPC := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, cfg.WalletHost, cfg.dcrwCert)
|
||||
walletClient, err := walletRPC()
|
||||
// Create RPC client for local dcrwallet instance (used for generating fee
|
||||
// addresses and broadcasting fee transactions).
|
||||
feeWalletConnect := rpc.Setup(ctx, &shutdownWg, cfg.FeeWalletUser, cfg.FeeWalletPass, cfg.FeeWalletHost, cfg.feeWalletCert)
|
||||
feeWalletConn, err := feeWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("dcrwallet RPC error: %v", err)
|
||||
log.Errorf("Fee wallet connection error: %v", err)
|
||||
requestShutdown()
|
||||
shutdownWg.Wait()
|
||||
return err
|
||||
}
|
||||
feeWalletClient, err := rpc.FeeWalletClient(ctx, feeWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet client error: %v", err)
|
||||
requestShutdown()
|
||||
shutdownWg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// Create RPC client for remote dcrwallet instance (used for voting).
|
||||
votingWalletConnect := rpc.Setup(ctx, &shutdownWg, cfg.VotingWalletUser, cfg.VotingWalletPass, cfg.VotingWalletHost, cfg.votingWalletCert)
|
||||
votingWalletConn, err := votingWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet connection error: %v", err)
|
||||
requestShutdown()
|
||||
shutdownWg.Wait()
|
||||
return err
|
||||
}
|
||||
_, err = rpc.VotingWalletClient(ctx, votingWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet client error: %v", err)
|
||||
requestShutdown()
|
||||
shutdownWg.Wait()
|
||||
return err
|
||||
@ -78,7 +102,7 @@ func run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Ensure the wallet account for collecting fees exists and matches config.
|
||||
err = setupFeeAccount(ctx, walletClient, cfg.FeeXPub)
|
||||
err = setupFeeAccount(ctx, feeWalletClient, cfg.FeeXPub)
|
||||
if err != nil {
|
||||
log.Errorf("Fee account error: %v", err)
|
||||
requestShutdown()
|
||||
@ -95,7 +119,7 @@ func run(ctx context.Context) error {
|
||||
FeeAccountName: feeAccountName,
|
||||
FeeAddressExpiration: defaultFeeAddressExpiration,
|
||||
}
|
||||
err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, walletRPC, cfg.WebServerDebug, apiCfg)
|
||||
err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, feeWalletConnect, votingWalletConnect, cfg.WebServerDebug, apiCfg)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialise webapi: %v", err)
|
||||
requestShutdown()
|
||||
@ -109,20 +133,18 @@ func run(ctx context.Context) error {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func setupFeeAccount(ctx context.Context, walletClient *wsrpc.Client, feeXpub string) error {
|
||||
func setupFeeAccount(ctx context.Context, walletClient *rpc.FeeWalletRPC, feeXpub string) error {
|
||||
// Check if account for fee collection already exists.
|
||||
var accounts map[string]float64
|
||||
err := walletClient.Call(ctx, "listaccounts", &accounts)
|
||||
accounts, err := walletClient.ListAccounts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dcrwallet RPC error: %v", err)
|
||||
return fmt.Errorf("ListAccounts error: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := accounts[feeAccountName]; ok {
|
||||
// Account already exists. Check xpub matches xpub from config.
|
||||
var existingXPub string
|
||||
err = walletClient.Call(ctx, "getmasterpubkey", &existingXPub, feeAccountName)
|
||||
existingXPub, err := walletClient.GetMasterPubKey(ctx, feeAccountName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dcrwallet RPC error: %v", err)
|
||||
return fmt.Errorf("GetMasterPubKey error: %v", err)
|
||||
}
|
||||
|
||||
if existingXPub != feeXpub {
|
||||
@ -133,8 +155,8 @@ func setupFeeAccount(ctx context.Context, walletClient *wsrpc.Client, feeXpub st
|
||||
|
||||
} else {
|
||||
// Account does not exist. Create it using xpub from config.
|
||||
if err = walletClient.Call(ctx, "importxpub", nil, feeAccountName, feeXpub); err != nil {
|
||||
log.Errorf("Failed to import xpub: %v", err)
|
||||
if err = walletClient.ImportXPub(ctx, feeAccountName, feeXpub); err != nil {
|
||||
log.Errorf("ImportXPub error: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Debugf("Created new wallet account %q to collect fees", feeAccountName)
|
||||
|
||||
@ -4,25 +4,29 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
"github.com/jrick/wsrpc/v2"
|
||||
)
|
||||
|
||||
type Client func() (*wsrpc.Client, error)
|
||||
// Caller provides a client interface to perform JSON-RPC remote procedure calls.
|
||||
type Caller interface {
|
||||
// Call performs the remote procedure call defined by method and
|
||||
// waits for a response or a broken client connection.
|
||||
// Args provides positional parameters for the call.
|
||||
// Res must be a pointer to a struct, slice, or map type to unmarshal
|
||||
// a result (if any), or nil if no result is needed.
|
||||
Call(ctx context.Context, method string, res interface{}, args ...interface{}) error
|
||||
}
|
||||
|
||||
const (
|
||||
requiredWalletVersion = "8.1.0"
|
||||
)
|
||||
// Connect dials and returns a connected RPC client.
|
||||
type Connect func() (Caller, error)
|
||||
|
||||
// Setup accepts RPC connection details, creates an RPC client, and returns a
|
||||
// function which can be called to access the client. The returned function will
|
||||
// try to handle any client disconnects by attempting to reconnect, but will
|
||||
// return an error if a new connection cannot be established.
|
||||
func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte) Client {
|
||||
func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte) Connect {
|
||||
|
||||
// Create TLS options.
|
||||
pool := x509.NewCertPool()
|
||||
@ -33,6 +37,8 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
||||
// Create authentication options.
|
||||
authOpt := wsrpc.WithBasicAuth(user, pass)
|
||||
|
||||
fullAddr := "wss://" + addr + "/ws"
|
||||
|
||||
var mu sync.Mutex
|
||||
var c *wsrpc.Client
|
||||
|
||||
@ -59,7 +65,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
||||
shutdownWg.Done()
|
||||
}()
|
||||
|
||||
return func() (*wsrpc.Client, error) {
|
||||
return func() (Caller, error) {
|
||||
defer mu.Unlock()
|
||||
mu.Lock()
|
||||
|
||||
@ -73,45 +79,11 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
||||
}
|
||||
}
|
||||
|
||||
fullAddr := "wss://" + addr + "/ws"
|
||||
c, err := wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt)
|
||||
var err error
|
||||
c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("Dialed RPC websocket %v", addr)
|
||||
|
||||
// Verify dcrwallet at is at the required api version
|
||||
var verMap map[string]dcrdtypes.VersionResult
|
||||
err = c.Call(ctx, "version", &verMap)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("wallet %v version failed: %v",
|
||||
addr, err)
|
||||
}
|
||||
walletVersion, exists := verMap["dcrwalletjsonrpcapi"]
|
||||
if !exists {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("wallet %v version response "+
|
||||
"missing 'dcrwalletjsonrpcapi'", addr)
|
||||
}
|
||||
if walletVersion.VersionString != requiredWalletVersion {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("wallet %v is not at the "+
|
||||
"proper version: %s != %s", addr,
|
||||
walletVersion.VersionString, requiredWalletVersion)
|
||||
}
|
||||
|
||||
// Verify dcrwallet is voting
|
||||
var walletInfo wallettypes.WalletInfoResult
|
||||
err = c.Call(ctx, "walletinfo", &walletInfo)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
if !walletInfo.Voting || !walletInfo.Unlocked {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("wallet %s has voting disabled", addr)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
113
rpc/feewallet.go
Normal file
113
rpc/feewallet.go
Normal file
@ -0,0 +1,113 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
requiredFeeWalletVersion = "8.1.0"
|
||||
)
|
||||
|
||||
// FeeWalletRPC provides methods for calling dcrwallet JSON-RPCs without exposing the details
|
||||
// of JSON encoding.
|
||||
type FeeWalletRPC struct {
|
||||
Caller
|
||||
}
|
||||
|
||||
// FeeWalletClient creates a new WalletRPC client instance from a caller.
|
||||
func FeeWalletClient(ctx context.Context, c Caller) (*FeeWalletRPC, error) {
|
||||
|
||||
// Verify dcrwallet is at the required api version.
|
||||
var verMap map[string]dcrdtypes.VersionResult
|
||||
err := c.Call(ctx, "version", &verMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("version check failed: %v", err)
|
||||
}
|
||||
walletVersion, exists := verMap["dcrwalletjsonrpcapi"]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("version response missing 'dcrwalletjsonrpcapi'")
|
||||
}
|
||||
if walletVersion.VersionString != requiredFeeWalletVersion {
|
||||
return nil, fmt.Errorf("wrong dcrwallet RPC version: expected %s, got %s",
|
||||
walletVersion.VersionString, requiredFeeWalletVersion)
|
||||
}
|
||||
|
||||
// Verify dcrwallet is connected to dcrd (not SPV).
|
||||
var walletInfo wallettypes.WalletInfoResult
|
||||
err = c.Call(ctx, "walletinfo", &walletInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walletinfo check failed: %v", err)
|
||||
}
|
||||
if !walletInfo.DaemonConnected {
|
||||
return nil, fmt.Errorf("wallet is not connected to dcrd")
|
||||
}
|
||||
|
||||
// TODO: Ensure correct network.
|
||||
|
||||
return &FeeWalletRPC{c}, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) ImportXPub(ctx context.Context, account, xpub string) error {
|
||||
return c.Call(ctx, "importxpub", nil, account, xpub)
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) GetMasterPubKey(ctx context.Context, account string) (string, error) {
|
||||
var pubKey string
|
||||
err := c.Call(ctx, "getmasterpubkey", &pubKey, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) ListAccounts(ctx context.Context) (map[string]float64, error) {
|
||||
var accounts map[string]float64
|
||||
err := c.Call(ctx, "listaccounts", &accounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) GetNewAddress(ctx context.Context, account string) (string, error) {
|
||||
var newAddress string
|
||||
err := c.Call(ctx, "getnewaddress", &newAddress, account)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newAddress, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) GetBlockHeader(ctx context.Context, blockHash string) (*dcrdtypes.GetBlockHeaderVerboseResult, error) {
|
||||
verbose := true
|
||||
var blockHeader dcrdtypes.GetBlockHeaderVerboseResult
|
||||
err := c.Call(ctx, "getblockheader", &blockHeader, blockHash, verbose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &blockHeader, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) GetRawTransaction(ctx context.Context, txHash string) (*dcrdtypes.TxRawResult, error) {
|
||||
verbose := 1
|
||||
var resp dcrdtypes.TxRawResult
|
||||
err := c.Call(ctx, "getrawtransaction", &resp, txHash, verbose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *FeeWalletRPC) SendRawTransaction(ctx context.Context, txHex string) (string, error) {
|
||||
allowHighFees := false
|
||||
var txHash string
|
||||
err := c.Call(ctx, "sendrawtransaction", &txHash, txHex, allowHighFees)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return txHash, nil
|
||||
}
|
||||
73
rpc/votingwallet.go
Normal file
73
rpc/votingwallet.go
Normal file
@ -0,0 +1,73 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
requiredVotingWalletVersion = "8.1.0"
|
||||
)
|
||||
|
||||
// VotingWalletRPC provides methods for calling dcrwallet JSON-RPCs without exposing the details
|
||||
// of JSON encoding.
|
||||
type VotingWalletRPC struct {
|
||||
Caller
|
||||
}
|
||||
|
||||
// VotingWalletClient creates a new VotingWalletRPC client instance from a caller.
|
||||
func VotingWalletClient(ctx context.Context, c Caller) (*VotingWalletRPC, error) {
|
||||
|
||||
// Verify dcrwallet is at the required api version.
|
||||
var verMap map[string]dcrdtypes.VersionResult
|
||||
err := c.Call(ctx, "version", &verMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("version check failed: %v", err)
|
||||
}
|
||||
walletVersion, exists := verMap["dcrwalletjsonrpcapi"]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("version response missing 'dcrwalletjsonrpcapi'")
|
||||
}
|
||||
if walletVersion.VersionString != requiredVotingWalletVersion {
|
||||
return nil, fmt.Errorf("wrong dcrwallet RPC version: expected %s, got %s",
|
||||
walletVersion.VersionString, requiredVotingWalletVersion)
|
||||
}
|
||||
|
||||
// Verify dcrwallet is voting, unlocked, and is connected to dcrd (not SPV).
|
||||
var walletInfo wallettypes.WalletInfoResult
|
||||
err = c.Call(ctx, "walletinfo", &walletInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walletinfo check failed: %v", err)
|
||||
}
|
||||
if !walletInfo.Voting {
|
||||
return nil, fmt.Errorf("wallet has voting disabled")
|
||||
}
|
||||
if !walletInfo.Unlocked {
|
||||
return nil, fmt.Errorf("wallet is not unlocked")
|
||||
}
|
||||
if !walletInfo.DaemonConnected {
|
||||
return nil, fmt.Errorf("wallet is not connected to dcrd")
|
||||
}
|
||||
|
||||
// TODO: Ensure correct network.
|
||||
|
||||
return &VotingWalletRPC{c}, nil
|
||||
}
|
||||
|
||||
func (c *VotingWalletRPC) AddTransaction(ctx context.Context, blockHash, txHex string) error {
|
||||
return c.Call(ctx, "addtransaction", nil, blockHash, txHex)
|
||||
}
|
||||
|
||||
func (c *VotingWalletRPC) ImportPrivKey(ctx context.Context, votingWIF string) error {
|
||||
label := "imported"
|
||||
rescan := false
|
||||
scanFrom := 0
|
||||
return c.Call(ctx, "importprivkey", nil, votingWIF, label, rescan, scanFrom)
|
||||
}
|
||||
|
||||
func (c *VotingWalletRPC) SetVoteChoice(ctx context.Context, agenda, choice, ticketHash string) error {
|
||||
return c.Call(ctx, "setvotechoice", nil, agenda, choice, ticketHash)
|
||||
}
|
||||
@ -11,10 +11,10 @@ import (
|
||||
"github.com/decred/dcrd/blockchain/stake/v3"
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
"github.com/decred/dcrd/wire"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
// feeAddress is the handler for "POST /feeaddress"
|
||||
@ -82,17 +82,21 @@ func feeAddress(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
walletClient, err := walletRPC()
|
||||
fWalletConn, err := feeWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to dial dcrwallet RPC: %v", err)
|
||||
log.Errorf("Fee wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
fWalletClient, err := rpc.FeeWalletClient(ctx, fWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Fee wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var resp dcrdtypes.TxRawResult
|
||||
err = walletClient.Call(ctx, "getrawtransaction", &resp, txHash.String(), 1)
|
||||
resp, err := fWalletClient.GetRawTransaction(ctx, txHash.String())
|
||||
if err != nil {
|
||||
log.Warnf("Could not retrieve tx %s for %s: %v", txHash, c.ClientIP(), err)
|
||||
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
||||
@ -153,16 +157,15 @@ func feeAddress(c *gin.Context) {
|
||||
// get blockheight and sdiff which is required by
|
||||
// txrules.StakePoolTicketFee, and store them in the database
|
||||
// for processing by payfee
|
||||
var blockHeader dcrdtypes.GetBlockHeaderVerboseResult
|
||||
err = walletClient.Call(ctx, "getblockheader", &blockHeader, resp.BlockHash, true)
|
||||
blockHeader, err := fWalletClient.GetBlockHeader(ctx, resp.BlockHash)
|
||||
if err != nil {
|
||||
log.Errorf("GetBlockHeader error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
var newAddress string
|
||||
err = walletClient.Call(ctx, "getnewaddress", &newAddress, "fees")
|
||||
// TODO: Generate this within dcrvsp without an RPC call?
|
||||
newAddress, err := fWalletClient.GetNewAddress(ctx, cfg.FeeAccountName)
|
||||
if err != nil {
|
||||
log.Errorf("GetNewAddress error: %v", err)
|
||||
sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c)
|
||||
|
||||
@ -10,10 +10,10 @@ import (
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
||||
"github.com/decred/dcrd/txscript/v3"
|
||||
"github.com/decred/dcrd/wire"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
// payFee is the handler for "POST /payfee"
|
||||
@ -131,31 +131,49 @@ findAddress:
|
||||
return
|
||||
}
|
||||
|
||||
walletClient, err := walletRPC()
|
||||
fWalletConn, err := feeWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to dial dcrwallet RPC: %v", err)
|
||||
log.Errorf("Fee wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var rawTicket dcrdtypes.TxRawResult
|
||||
|
||||
err = walletClient.Call(ctx, "getrawtransaction", &rawTicket, ticketHash.String(), 1)
|
||||
fWalletClient, err := rpc.FeeWalletClient(ctx, fWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("GetRawTransaction failed: %v", err)
|
||||
log.Errorf("Fee wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
err = walletClient.Call(ctx, "addtransaction", nil, rawTicket.BlockHash, rawTicket.Hex)
|
||||
rawTicket, err := fWalletClient.GetRawTransaction(ctx, ticketHash.String())
|
||||
if err != nil {
|
||||
log.Warnf("Could not retrieve tx %s for %s: %v", ticketHash.String(), c.ClientIP(), err)
|
||||
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
vWalletConn, err := votingWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
vWalletClient, err := rpc.VotingWalletClient(ctx, vWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
err = vWalletClient.AddTransaction(ctx, rawTicket.BlockHash, rawTicket.Hex)
|
||||
if err != nil {
|
||||
log.Errorf("AddTransaction failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
err = walletClient.Call(ctx, "importprivkey", nil, votingWIF.String(), "imported", false, 0)
|
||||
err = vWalletClient.ImportPrivKey(ctx, votingWIF.String())
|
||||
if err != nil {
|
||||
log.Errorf("ImportPrivKey failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
@ -164,9 +182,9 @@ findAddress:
|
||||
|
||||
// Update vote choices on voting wallets.
|
||||
for agenda, choice := range voteChoices {
|
||||
err = walletClient.Call(ctx, "setvotechoice", nil, agenda, choice, ticket.Hash)
|
||||
err = vWalletClient.SetVoteChoice(ctx, agenda, choice, ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("setvotechoice failed: %v", err)
|
||||
log.Errorf("SetVoteChoice failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
@ -181,8 +199,7 @@ findAddress:
|
||||
return
|
||||
}
|
||||
|
||||
var sendTxHash string
|
||||
err = walletClient.Call(ctx, "sendrawtransaction", &sendTxHash, hex.EncodeToString(feeTxBuf.Bytes()), false)
|
||||
sendTxHash, err := fWalletClient.SendRawTransaction(ctx, hex.EncodeToString(feeTxBuf.Bytes()))
|
||||
if err != nil {
|
||||
log.Errorf("SendRawTransaction failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
// setVoteChoices is the handler for "POST /setvotechoices"
|
||||
@ -61,25 +62,33 @@ func setVoteChoices(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
walletClient, err := walletRPC()
|
||||
vWalletConn, err := votingWalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to dial dcrwallet RPC: %v", err)
|
||||
log.Errorf("Voting wallet connection error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
vWalletClient, err := rpc.VotingWalletClient(ctx, vWalletConn)
|
||||
if err != nil {
|
||||
log.Errorf("Voting wallet client error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Update vote choices on voting wallets.
|
||||
for agenda, choice := range voteChoices {
|
||||
err = walletClient.Call(ctx, "setvotechoice", nil, agenda, choice, ticket.Hash)
|
||||
err = vWalletClient.SetVoteChoice(ctx, agenda, choice, ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("setvotechoice failed: %v", err)
|
||||
log.Errorf("SetVoteChoice failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update database before updating wallets. DB is source of truth and
|
||||
// is less likely to error.
|
||||
err = db.UpdateVoteChoices(txHash.String(), voteChoices)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateVoteChoices error: %v", err)
|
||||
|
||||
@ -28,10 +28,11 @@ type Config struct {
|
||||
|
||||
var cfg Config
|
||||
var db *database.VspDatabase
|
||||
var walletRPC rpc.Client
|
||||
var feeWalletConnect rpc.Connect
|
||||
var votingWalletConnect rpc.Connect
|
||||
|
||||
func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup,
|
||||
listen string, vdb *database.VspDatabase, wRPC rpc.Client, debugMode bool, config Config) error {
|
||||
listen string, vdb *database.VspDatabase, fWalletConnect rpc.Connect, vWalletConnect rpc.Connect, debugMode bool, config Config) error {
|
||||
|
||||
// Create TCP listener.
|
||||
var listenConfig net.ListenConfig
|
||||
@ -79,7 +80,8 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
|
||||
|
||||
cfg = config
|
||||
db = vdb
|
||||
walletRPC = wRPC
|
||||
feeWalletConnect = fWalletConnect
|
||||
votingWalletConnect = vWalletConnect
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user