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)
|
[](http://copyfree.org)
|
||||||
[](https://goreportcard.com/report/github.com/jholdstock/dcrvsp)
|
[](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.
|
- 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.
|
- [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
|
- Tickets are stored in a single bucket, using ticket hash as the key and a
|
||||||
json encoded representation of the ticket as the value.
|
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
|
- 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
|
stores it in the database. This key is used to sign all API responses, and the
|
||||||
@ -29,11 +66,12 @@
|
|||||||
- Pay fee (`POST /payFee`)
|
- Pay fee (`POST /payFee`)
|
||||||
- Ticket status (`GET /ticketstatus`)
|
- Ticket status (`GET /ticketstatus`)
|
||||||
- Set voting preferences (`POST /setvotechoices`)
|
- 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
|
- 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.
|
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.
|
- Write database backups to disk periodically.
|
||||||
- Backup over http.
|
- Backup over http.
|
||||||
@ -41,9 +79,11 @@
|
|||||||
- Accountability for both client and server changes to voting preferences.
|
- Accountability for both client and server changes to voting preferences.
|
||||||
- Consistency checking across connected wallets.
|
- 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
|
## Issue Tracker
|
||||||
|
|
||||||
|
|||||||
128
config.go
128
config.go
@ -17,35 +17,41 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultListen = ":3000"
|
defaultListen = ":3000"
|
||||||
defaultLogLevel = "debug"
|
defaultLogLevel = "debug"
|
||||||
defaultVSPFee = 0.01
|
defaultVSPFee = 0.01
|
||||||
defaultNetwork = "testnet"
|
defaultNetwork = "testnet"
|
||||||
defaultHomeDir = dcrutil.AppDataDir("dcrvsp", false)
|
defaultHomeDir = dcrutil.AppDataDir("dcrvsp", false)
|
||||||
defaultConfigFilename = "dcrvsp.conf"
|
defaultConfigFilename = "dcrvsp.conf"
|
||||||
defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename)
|
defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename)
|
||||||
defaultWalletHost = "127.0.0.1"
|
defaultFeeWalletHost = "127.0.0.1"
|
||||||
defaultWebServerDebug = false
|
defaultVotingWalletHost = "127.0.0.1"
|
||||||
|
defaultWebServerDebug = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// config defines the configuration options for the VSP.
|
// config defines the configuration options for the VSP.
|
||||||
type config struct {
|
type config struct {
|
||||||
Listen string `long:"listen" ini-name:"listen" description:"The ip:port to listen for API requests."`
|
Listen string `long:"listen" ini-name:"listen" description:"The ip:port to listen for API requests."`
|
||||||
LogLevel string `long:"loglevel" ini-name:"loglevel" description:"Logging level." choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"critical"`
|
LogLevel string `long:"loglevel" ini-name:"loglevel" description:"Logging level." choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"critical"`
|
||||||
Network string `long:"network" ini-name:"network" description:"Decred network to use." choice:"testnet" choice:"mainnet" choice:"simnet"`
|
Network string `long:"network" ini-name:"network" description:"Decred network to use." choice:"testnet" choice:"mainnet" choice:"simnet"`
|
||||||
FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."`
|
FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."`
|
||||||
VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."`
|
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."`
|
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."`
|
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."`
|
FeeWalletHost string `long:"feewallethost" ini-name:"feewallethost" description:"The ip:port to establish a JSON-RPC connection with fee dcrwallet."`
|
||||||
WalletUser string `long:"walletuser" ini-name:"walletuser" description:"Username for dcrwallet RPC connections."`
|
FeeWalletUser string `long:"feewalletuser" ini-name:"feewalletuser" description:"Username for fee dcrwallet RPC connections."`
|
||||||
WalletPass string `long:"walletpass" ini-name:"walletpass" description:"Password for dcrwallet RPC connections."`
|
FeeWalletPass string `long:"feewalletpass" ini-name:"feewalletpass" description:"Password for fee dcrwallet RPC connections."`
|
||||||
WalletCert string `long:"walletcert" ini-name:"walletcert" description:"The dcrwallet RPC certificate file."`
|
FeeWalletCert string `long:"feewalletcert" ini-name:"feewalletcert" description:"The fee 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)."`
|
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
|
dbPath string
|
||||||
netParams *netParams
|
netParams *netParams
|
||||||
dcrwCert []byte
|
feeWalletCert []byte
|
||||||
|
votingWalletCert []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileExists reports whether the named file or directory exists.
|
// fileExists reports whether the named file or directory exists.
|
||||||
@ -136,14 +142,15 @@ func loadConfig() (*config, error) {
|
|||||||
|
|
||||||
// Default config.
|
// Default config.
|
||||||
cfg := config{
|
cfg := config{
|
||||||
Listen: defaultListen,
|
Listen: defaultListen,
|
||||||
LogLevel: defaultLogLevel,
|
LogLevel: defaultLogLevel,
|
||||||
Network: defaultNetwork,
|
Network: defaultNetwork,
|
||||||
VSPFee: defaultVSPFee,
|
VSPFee: defaultVSPFee,
|
||||||
HomeDir: defaultHomeDir,
|
HomeDir: defaultHomeDir,
|
||||||
ConfigFile: defaultConfigFile,
|
ConfigFile: defaultConfigFile,
|
||||||
WalletHost: defaultWalletHost,
|
FeeWalletHost: defaultFeeWalletHost,
|
||||||
WebServerDebug: defaultWebServerDebug,
|
VotingWalletHost: defaultVotingWalletHost,
|
||||||
|
WebServerDebug: defaultWebServerDebug,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-parse the command line options to see if an alternative config
|
// Pre-parse the command line options to see if an alternative config
|
||||||
@ -233,30 +240,53 @@ func loadConfig() (*config, error) {
|
|||||||
cfg.netParams = &simNetParams
|
cfg.netParams = &simNetParams
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the dcrwallet RPC username is set.
|
// Ensure the fee dcrwallet RPC username is set.
|
||||||
if cfg.WalletUser == "" {
|
if cfg.FeeWalletUser == "" {
|
||||||
return nil, errors.New("the walletuser option is not set")
|
return nil, errors.New("the feewalletuser option is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the dcrwallet RPC password is set.
|
// Ensure the fee dcrwallet RPC password is set.
|
||||||
if cfg.WalletPass == "" {
|
if cfg.FeeWalletPass == "" {
|
||||||
return nil, errors.New("the walletpass option is not set")
|
return nil, errors.New("the feewalletpass option is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the dcrwallet RPC cert path is set.
|
// Ensure the fee dcrwallet RPC cert path is set.
|
||||||
if cfg.WalletCert == "" {
|
if cfg.FeeWalletCert == "" {
|
||||||
return nil, errors.New("the walletcert option is not set")
|
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.
|
// Add default port for the active network if there is no port specified.
|
||||||
cfg.WalletHost = normalizeAddress(cfg.WalletHost, cfg.netParams.WalletRPCServerPort)
|
cfg.FeeWalletHost = normalizeAddress(cfg.FeeWalletHost, cfg.netParams.WalletRPCServerPort)
|
||||||
|
cfg.VotingWalletHost = normalizeAddress(cfg.VotingWalletHost, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the data directory.
|
// Create the data directory.
|
||||||
dataDir := filepath.Join(cfg.HomeDir, "data", cfg.netParams.Name)
|
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/database"
|
||||||
"github.com/jholdstock/dcrvsp/rpc"
|
"github.com/jholdstock/dcrvsp/rpc"
|
||||||
"github.com/jholdstock/dcrvsp/webapi"
|
"github.com/jholdstock/dcrvsp/webapi"
|
||||||
"github.com/jrick/wsrpc/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -59,11 +58,36 @@ func run(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create dcrwallet RPC client.
|
// Create RPC client for local dcrwallet instance (used for generating fee
|
||||||
walletRPC := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, cfg.WalletHost, cfg.dcrwCert)
|
// addresses and broadcasting fee transactions).
|
||||||
walletClient, err := walletRPC()
|
feeWalletConnect := rpc.Setup(ctx, &shutdownWg, cfg.FeeWalletUser, cfg.FeeWalletPass, cfg.FeeWalletHost, cfg.feeWalletCert)
|
||||||
|
feeWalletConn, err := feeWalletConnect()
|
||||||
if err != nil {
|
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()
|
requestShutdown()
|
||||||
shutdownWg.Wait()
|
shutdownWg.Wait()
|
||||||
return err
|
return err
|
||||||
@ -78,7 +102,7 @@ func run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the wallet account for collecting fees exists and matches config.
|
// 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 {
|
if err != nil {
|
||||||
log.Errorf("Fee account error: %v", err)
|
log.Errorf("Fee account error: %v", err)
|
||||||
requestShutdown()
|
requestShutdown()
|
||||||
@ -95,7 +119,7 @@ func run(ctx context.Context) error {
|
|||||||
FeeAccountName: feeAccountName,
|
FeeAccountName: feeAccountName,
|
||||||
FeeAddressExpiration: defaultFeeAddressExpiration,
|
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 {
|
if err != nil {
|
||||||
log.Errorf("Failed to initialise webapi: %v", err)
|
log.Errorf("Failed to initialise webapi: %v", err)
|
||||||
requestShutdown()
|
requestShutdown()
|
||||||
@ -109,20 +133,18 @@ func run(ctx context.Context) error {
|
|||||||
return ctx.Err()
|
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.
|
// Check if account for fee collection already exists.
|
||||||
var accounts map[string]float64
|
accounts, err := walletClient.ListAccounts(ctx)
|
||||||
err := walletClient.Call(ctx, "listaccounts", &accounts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("dcrwallet RPC error: %v", err)
|
return fmt.Errorf("ListAccounts error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := accounts[feeAccountName]; ok {
|
if _, ok := accounts[feeAccountName]; ok {
|
||||||
// Account already exists. Check xpub matches xpub from config.
|
// Account already exists. Check xpub matches xpub from config.
|
||||||
var existingXPub string
|
existingXPub, err := walletClient.GetMasterPubKey(ctx, feeAccountName)
|
||||||
err = walletClient.Call(ctx, "getmasterpubkey", &existingXPub, feeAccountName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("dcrwallet RPC error: %v", err)
|
return fmt.Errorf("GetMasterPubKey error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingXPub != feeXpub {
|
if existingXPub != feeXpub {
|
||||||
@ -133,8 +155,8 @@ func setupFeeAccount(ctx context.Context, walletClient *wsrpc.Client, feeXpub st
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Account does not exist. Create it using xpub from config.
|
// Account does not exist. Create it using xpub from config.
|
||||||
if err = walletClient.Call(ctx, "importxpub", nil, feeAccountName, feeXpub); err != nil {
|
if err = walletClient.ImportXPub(ctx, feeAccountName, feeXpub); err != nil {
|
||||||
log.Errorf("Failed to import xpub: %v", err)
|
log.Errorf("ImportXPub error: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debugf("Created new wallet account %q to collect fees", feeAccountName)
|
log.Debugf("Created new wallet account %q to collect fees", feeAccountName)
|
||||||
|
|||||||
@ -4,25 +4,29 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types"
|
|
||||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
|
||||||
"github.com/jrick/wsrpc/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 (
|
// Connect dials and returns a connected RPC client.
|
||||||
requiredWalletVersion = "8.1.0"
|
type Connect func() (Caller, error)
|
||||||
)
|
|
||||||
|
|
||||||
// Setup accepts RPC connection details, creates an RPC client, and returns a
|
// 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
|
// 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
|
// try to handle any client disconnects by attempting to reconnect, but will
|
||||||
// return an error if a new connection cannot be established.
|
// 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.
|
// Create TLS options.
|
||||||
pool := x509.NewCertPool()
|
pool := x509.NewCertPool()
|
||||||
@ -33,6 +37,8 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
|||||||
// Create authentication options.
|
// Create authentication options.
|
||||||
authOpt := wsrpc.WithBasicAuth(user, pass)
|
authOpt := wsrpc.WithBasicAuth(user, pass)
|
||||||
|
|
||||||
|
fullAddr := "wss://" + addr + "/ws"
|
||||||
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var c *wsrpc.Client
|
var c *wsrpc.Client
|
||||||
|
|
||||||
@ -59,7 +65,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
|||||||
shutdownWg.Done()
|
shutdownWg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return func() (*wsrpc.Client, error) {
|
return func() (Caller, error) {
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
|
|
||||||
@ -73,45 +79,11 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fullAddr := "wss://" + addr + "/ws"
|
var err error
|
||||||
c, err := wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt)
|
c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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/blockchain/stake/v3"
|
||||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrutil/v3"
|
"github.com/decred/dcrd/dcrutil/v3"
|
||||||
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
|
|
||||||
"github.com/decred/dcrd/wire"
|
"github.com/decred/dcrd/wire"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jholdstock/dcrvsp/database"
|
"github.com/jholdstock/dcrvsp/database"
|
||||||
|
"github.com/jholdstock/dcrvsp/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// feeAddress is the handler for "POST /feeaddress"
|
// feeAddress is the handler for "POST /feeaddress"
|
||||||
@ -82,17 +82,21 @@ func feeAddress(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
walletClient, err := walletRPC()
|
fWalletConn, err := feeWalletConnect()
|
||||||
if err != nil {
|
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)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
resp, err := fWalletClient.GetRawTransaction(ctx, txHash.String())
|
||||||
|
|
||||||
var resp dcrdtypes.TxRawResult
|
|
||||||
err = walletClient.Call(ctx, "getrawtransaction", &resp, txHash.String(), 1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Could not retrieve tx %s for %s: %v", txHash, c.ClientIP(), err)
|
log.Warnf("Could not retrieve tx %s for %s: %v", txHash, c.ClientIP(), err)
|
||||||
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
||||||
@ -153,16 +157,15 @@ func feeAddress(c *gin.Context) {
|
|||||||
// get blockheight and sdiff which is required by
|
// get blockheight and sdiff which is required by
|
||||||
// txrules.StakePoolTicketFee, and store them in the database
|
// txrules.StakePoolTicketFee, and store them in the database
|
||||||
// for processing by payfee
|
// for processing by payfee
|
||||||
var blockHeader dcrdtypes.GetBlockHeaderVerboseResult
|
blockHeader, err := fWalletClient.GetBlockHeader(ctx, resp.BlockHash)
|
||||||
err = walletClient.Call(ctx, "getblockheader", &blockHeader, resp.BlockHash, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("GetBlockHeader error: %v", err)
|
log.Errorf("GetBlockHeader error: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var newAddress string
|
// TODO: Generate this within dcrvsp without an RPC call?
|
||||||
err = walletClient.Call(ctx, "getnewaddress", &newAddress, "fees")
|
newAddress, err := fWalletClient.GetNewAddress(ctx, cfg.FeeAccountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("GetNewAddress error: %v", err)
|
log.Errorf("GetNewAddress error: %v", err)
|
||||||
sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c)
|
sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c)
|
||||||
|
|||||||
@ -10,10 +10,10 @@ import (
|
|||||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrec"
|
"github.com/decred/dcrd/dcrec"
|
||||||
"github.com/decred/dcrd/dcrutil/v3"
|
"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/txscript/v3"
|
||||||
"github.com/decred/dcrd/wire"
|
"github.com/decred/dcrd/wire"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/jholdstock/dcrvsp/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// payFee is the handler for "POST /payfee"
|
// payFee is the handler for "POST /payfee"
|
||||||
@ -131,31 +131,49 @@ findAddress:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
walletClient, err := walletRPC()
|
fWalletConn, err := feeWalletConnect()
|
||||||
if err != nil {
|
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)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
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 {
|
if err != nil {
|
||||||
log.Errorf("GetRawTransaction failed: %v", err)
|
log.Errorf("Fee wallet client error: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
log.Errorf("AddTransaction failed: %v", err)
|
log.Errorf("AddTransaction failed: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = walletClient.Call(ctx, "importprivkey", nil, votingWIF.String(), "imported", false, 0)
|
err = vWalletClient.ImportPrivKey(ctx, votingWIF.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ImportPrivKey failed: %v", err)
|
log.Errorf("ImportPrivKey failed: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
@ -164,9 +182,9 @@ findAddress:
|
|||||||
|
|
||||||
// Update vote choices on voting wallets.
|
// Update vote choices on voting wallets.
|
||||||
for agenda, choice := range voteChoices {
|
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 {
|
if err != nil {
|
||||||
log.Errorf("setvotechoice failed: %v", err)
|
log.Errorf("SetVoteChoice failed: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -181,8 +199,7 @@ findAddress:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendTxHash string
|
sendTxHash, err := fWalletClient.SendRawTransaction(ctx, hex.EncodeToString(feeTxBuf.Bytes()))
|
||||||
err = walletClient.Call(ctx, "sendrawtransaction", &sendTxHash, hex.EncodeToString(feeTxBuf.Bytes()), false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("SendRawTransaction failed: %v", err)
|
log.Errorf("SendRawTransaction failed: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrutil/v3"
|
"github.com/decred/dcrd/dcrutil/v3"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/jholdstock/dcrvsp/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setVoteChoices is the handler for "POST /setvotechoices"
|
// setVoteChoices is the handler for "POST /setvotechoices"
|
||||||
@ -61,25 +62,33 @@ func setVoteChoices(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
walletClient, err := walletRPC()
|
vWalletConn, err := votingWalletConnect()
|
||||||
if err != nil {
|
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)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
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.
|
// Update vote choices on voting wallets.
|
||||||
for agenda, choice := range voteChoices {
|
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 {
|
if err != nil {
|
||||||
log.Errorf("setvotechoice failed: %v", err)
|
log.Errorf("SetVoteChoice failed: %v", err)
|
||||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Update database before updating wallets. DB is source of truth and
|
||||||
|
// is less likely to error.
|
||||||
err = db.UpdateVoteChoices(txHash.String(), voteChoices)
|
err = db.UpdateVoteChoices(txHash.String(), voteChoices)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("UpdateVoteChoices error: %v", err)
|
log.Errorf("UpdateVoteChoices error: %v", err)
|
||||||
|
|||||||
@ -28,10 +28,11 @@ type Config struct {
|
|||||||
|
|
||||||
var cfg Config
|
var cfg Config
|
||||||
var db *database.VspDatabase
|
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,
|
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.
|
// Create TCP listener.
|
||||||
var listenConfig net.ListenConfig
|
var listenConfig net.ListenConfig
|
||||||
@ -79,7 +80,8 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
|
|||||||
|
|
||||||
cfg = config
|
cfg = config
|
||||||
db = vdb
|
db = vdb
|
||||||
walletRPC = wRPC
|
feeWalletConnect = fWalletConnect
|
||||||
|
votingWalletConnect = vWalletConnect
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user