Files
atomic-swap/monero/wallet_client.go
2023-08-16 19:28:51 -05:00

841 lines
25 KiB
Go

// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
// Package monero provides client libraries for working with wallet files and interacting
// with a monero node. Management of monero-wallet-rpc daemon instances is fully
// encapsulated by these libraries.
package monero
import (
"bufio"
"context"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path"
"strings"
"syscall"
"time"
"github.com/MarinX/monerorpc"
monerodaemon "github.com/MarinX/monerorpc/daemon"
"github.com/MarinX/monerorpc/wallet"
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common"
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
)
const (
moneroWalletRPCLogPrefix = "[monero-wallet-rpc]: "
// MinSpendConfirmations is the number of confirmations required on transaction
// outputs before they can be spent again.
MinSpendConfirmations = 10
// SweepToSelfConfirmations is the number of confirmations that we wait for when
// sweeping funds from an A+B wallet to our primary wallet.
SweepToSelfConfirmations = 2
)
// WalletClient represents a monero-wallet-rpc client.
type WalletClient interface {
GetAccounts() (*wallet.GetAccountsResponse, error)
GetAddress(idx uint32) (*wallet.GetAddressResponse, error)
PrimaryAddress() *mcrypto.Address
GetBalance(idx uint32) (*wallet.GetBalanceResponse, error)
Transfer(
ctx context.Context,
to *mcrypto.Address,
accountIdx uint32,
amount *coins.PiconeroAmount,
numConfirmations uint64,
) (*wallet.Transfer, error)
SweepAll(
ctx context.Context,
to *mcrypto.Address,
accountIdx uint32,
numConfirmations uint64,
) ([]*wallet.Transfer, error)
CreateWalletConf(walletNamePrefix string) *WalletClientConf
WalletName() string
GetHeight() (uint64, error)
Endpoint() string // URL on which the wallet is accepting RPC requests
Close() // Close closes the client itself, including any open wallet
CloseAndRemoveWallet()
}
// WalletClientConf wraps the configuration fields needed to call NewWalletClient
type WalletClientConf struct {
Env common.Environment // Required
WalletFilePath string // Required, wallet created if it does not exist
WalletPassword string // Optional, password used to open wallet or when creating a new wallet
WalletPort uint // Optional, zero means OS picks a random port
MonerodNodes []*common.MoneroNode // Optional, defaulted from environment if nil
MoneroWalletRPCPath string // optional, path to monero-rpc-binary
LogPath string // optional, default is dir(WalletFilePath)/../monero-wallet-rpc.log
}
// Fill fills in the optional configuration values (Port, MonerodNodes, MoneroWalletRPCPath,
// and LogPath) if they are not set.
// Note: MonerodNodes is set to the first validated node.
func (conf *WalletClientConf) Fill() error {
if conf.WalletFilePath == "" {
panic("WalletFilePath is a required conf field") // should have been caught before we were invoked
}
var err error
if conf.MoneroWalletRPCPath == "" {
conf.MoneroWalletRPCPath, err = getMoneroWalletRPCBin()
if err != nil {
return err
}
}
if len(conf.MonerodNodes) == 0 {
conf.MonerodNodes = common.ConfigDefaultsForEnv(conf.Env).MoneroNodes
}
validatedNode, err := findWorkingNode(conf.Env, conf.MonerodNodes)
if err != nil {
return err
}
conf.MonerodNodes = []*common.MoneroNode{validatedNode}
if conf.LogPath == "" {
// default to the folder above the wallet
conf.LogPath = path.Join(path.Dir(path.Dir(conf.WalletFilePath)), "monero-wallet-rpc.log")
}
if conf.WalletPort == 0 {
conf.WalletPort, err = common.GetFreeTCPPort()
if err != nil {
return err
}
}
return nil
}
// waitForReceiptRequest wraps the input parameters for waitForReceipt
type waitForReceiptRequest struct {
Ctx context.Context
TxID string
NumConfirmations uint64
AccountIdx uint32
}
type walletClient struct {
wRPC wallet.Wallet // full monero-wallet-rpc API (larger than the WalletClient interface)
dRPC monerodaemon.Daemon // full monerod RPC API
endpoint string
walletAddr *mcrypto.Address
conf *WalletClientConf
rpcProcess *os.Process // monero-wallet-rpc process that we create
}
// NewWalletClient returns a WalletClient for a newly created monero-wallet-rpc process.
func NewWalletClient(conf *WalletClientConf) (WalletClient, error) {
if path.Dir(conf.WalletFilePath) == "." {
return nil, errors.New("wallet file cannot be in the current working directory")
}
err := conf.Fill()
if err != nil {
return nil, err
}
walletExists, err := common.FileExists(conf.WalletFilePath)
if err != nil {
return nil, err
}
isNewWallet := !walletExists
validatedNode := conf.MonerodNodes[0]
proc, err := createWalletRPCService(
conf.Env,
conf.MoneroWalletRPCPath,
conf.WalletPort,
path.Dir(conf.WalletFilePath),
conf.LogPath,
validatedNode)
if err != nil {
return nil, err
}
c := NewThinWalletClient(validatedNode.Host, validatedNode.Port, conf.WalletPort).(*walletClient)
c.rpcProcess = proc
walletName := path.Base(conf.WalletFilePath)
if isNewWallet {
if err = c.CreateWallet(walletName, conf.WalletPassword); err != nil {
c.Close()
return nil, err
}
log.Infof("New Monero wallet %s created", conf.WalletFilePath)
} else {
err = c.wRPC.OpenWallet(&wallet.OpenWalletRequest{
Filename: walletName,
Password: conf.WalletPassword,
})
if err != nil {
c.Close()
return nil, err
}
}
acctResp, err := c.GetAddress(0)
if err != nil {
c.Close()
return nil, err
}
c.walletAddr, err = mcrypto.NewAddress(acctResp.Address, conf.Env)
if err != nil {
c.Close()
return nil, err
}
c.conf = conf
return c, nil
}
// NewThinWalletClient returns a WalletClient for an existing monero-wallet-rpc process.
func NewThinWalletClient(monerodHost string, monerodPort uint, walletPort uint) WalletClient {
monerodEndpoint := fmt.Sprintf("http://%s:%d/json_rpc", monerodHost, monerodPort)
walletEndpoint := fmt.Sprintf("http://127.0.0.1:%d/json_rpc", walletPort)
return &walletClient{
dRPC: monerorpc.New(monerodEndpoint, nil).Daemon,
wRPC: monerorpc.New(walletEndpoint, nil).Wallet,
endpoint: walletEndpoint,
}
}
func (c *walletClient) WalletName() string {
return path.Base(c.conf.WalletFilePath)
}
func (c *walletClient) GetAccounts() (*wallet.GetAccountsResponse, error) {
return c.wRPC.GetAccounts(&wallet.GetAccountsRequest{})
}
func (c *walletClient) GetBalance(idx uint32) (*wallet.GetBalanceResponse, error) {
if err := c.refresh(); err != nil {
return nil, err
}
return c.wRPC.GetBalance(&wallet.GetBalanceRequest{
AccountIndex: idx,
})
}
// waitForReceipt waits for the passed monero transaction ID to receive numConfirmations
// and returns the transfer information. While this function will always wait for the
// transaction to leave the mem-pool even if zero confirmations are requested, it is the
// caller's responsibility to request enough confirmations that the returned transfer
// information will not be invalidated by a block reorg.
func (c *walletClient) waitForReceipt(req *waitForReceiptRequest) (*wallet.Transfer, error) {
height, err := c.GetHeight()
if err != nil {
return nil, err
}
var transfer *wallet.Transfer
for {
// Wallet is already refreshed here, due to GetHeight above and WaitForBlocks below
transferResp, err := c.wRPC.GetTransferByTxid(&wallet.GetTransferByTxidRequest{
TxID: req.TxID,
AccountIndex: req.AccountIdx,
})
if err != nil {
return nil, err
}
transfer = &transferResp.Transfer
log.Infof("Received %d of %d confirmations of XMR TXID=%s (height=%d)",
transfer.Confirmations,
req.NumConfirmations,
req.TxID,
height)
// wait for transaction be mined (height set) even if 0 confirmations requested
if transfer.Height > 0 && transfer.Confirmations >= req.NumConfirmations {
break
}
height, err = WaitForBlocks(req.Ctx, c, 1)
if err != nil {
return nil, err
}
}
return transfer, nil
}
func (c *walletClient) Transfer(
ctx context.Context,
to *mcrypto.Address,
accountIdx uint32,
amount *coins.PiconeroAmount,
numConfirmations uint64,
) (*wallet.Transfer, error) {
amt, err := amount.Uint64()
if err != nil {
return nil, err
}
amountStr := amount.AsMoneroString()
log.Infof("Transferring %s XMR to %s", amountStr, to)
reqResp, err := c.wRPC.Transfer(&wallet.TransferRequest{
Destinations: []wallet.Destination{{
Amount: amt,
Address: to.String(),
}},
AccountIndex: accountIdx,
})
if err != nil {
log.Warnf("Transfer of %s XMR failed: %s", amountStr, err)
return nil, fmt.Errorf("transfer failed: %w", err)
}
log.Infof("Transfer of %s XMR initiated, TXID=%s", amountStr, reqResp.TxHash)
transfer, err := c.waitForReceipt(&waitForReceiptRequest{
Ctx: ctx,
TxID: reqResp.TxHash,
NumConfirmations: numConfirmations,
AccountIdx: accountIdx,
})
if err != nil {
return nil, fmt.Errorf("monero TXID=%s receipt failure: %w", reqResp.TxHash, err)
}
log.Infof("Transfer TXID=%s succeeded with %d confirmations and fee %s XMR",
transfer.TxID,
transfer.Confirmations,
coins.FmtPiconeroAsXMR(transfer.Fee),
)
return transfer, nil
}
func (c *walletClient) SweepAll(
ctx context.Context,
to *mcrypto.Address,
accountIdx uint32,
numConfirmations uint64,
) ([]*wallet.Transfer, error) {
addrResp, err := c.GetAddress(accountIdx)
if err != nil {
return nil, fmt.Errorf("sweep operation failed to get address: %w", err)
}
from := addrResp.Address
balance, err := c.GetBalance(accountIdx)
if err != nil {
return nil, fmt.Errorf("sweep operation failed to get balance: %w", err)
}
log.Infof("Starting sweep of %s XMR from %s to %s", coins.FmtPiconeroAsXMR(balance.Balance), from, to)
if balance.Balance == 0 {
return nil, fmt.Errorf("sweep from %s failed, no balance to sweep", from)
}
if balance.BlocksToUnlock > 0 {
log.Infof("Sweep operation waiting %d blocks for balance to fully unlock", balance.BlocksToUnlock)
if _, err = WaitForBlocks(ctx, c, int(balance.BlocksToUnlock)); err != nil {
return nil, fmt.Errorf("sweep operation failed waiting to unlock balance: %w", err)
}
}
reqResp, err := c.wRPC.SweepAll(&wallet.SweepAllRequest{
AccountIndex: accountIdx,
Address: to.String(),
})
if err != nil {
return nil, fmt.Errorf("sweep_all from %s failed: %w", from, err)
}
log.Infof("Sweep transaction started, TX IDs: %s", strings.Join(reqResp.TxHashList, ", "))
var transfers []*wallet.Transfer
for _, txID := range reqResp.TxHashList {
receipt, err := c.waitForReceipt(&waitForReceiptRequest{
Ctx: ctx,
TxID: txID,
NumConfirmations: numConfirmations,
AccountIdx: accountIdx,
})
if err != nil {
return nil, fmt.Errorf("sweep of TXID=%s failed waiting for receipt: %w", txID, err)
}
log.Infof("Sweep transfer ID=%s of %s XMR (%s XMR fees) completed at height %d",
txID,
coins.FmtPiconeroAsXMR(receipt.Amount),
coins.FmtPiconeroAsXMR(receipt.Fee),
receipt.Height,
)
transfers = append(transfers, receipt)
}
return transfers, nil
}
func (c *walletClient) CreateWalletConf(walletNamePrefix string) *WalletClientConf {
walletName := fmt.Sprintf("%s-%s", walletNamePrefix, time.Now().Format(common.TimeFmtNSecs))
walletPath := path.Join(path.Dir(c.conf.WalletFilePath), walletName)
conf := &WalletClientConf{
Env: c.conf.Env,
WalletFilePath: walletPath,
WalletPassword: c.conf.WalletPassword,
WalletPort: 0,
MonerodNodes: c.conf.MonerodNodes,
MoneroWalletRPCPath: c.conf.MoneroWalletRPCPath,
LogPath: c.conf.LogPath,
}
return conf
}
func createWalletFromKeys(
conf *WalletClientConf,
walletRestoreHeight uint64,
privateSpendKey *mcrypto.PrivateSpendKey, // nil for a view-only wallet
privateViewKey *mcrypto.PrivateViewKey,
address *mcrypto.Address,
) (WalletClient, error) {
if conf.WalletPort == 0 { // swap wallets need randomized ports, so we expect this to be zero
var err error
conf.WalletPort, err = common.GetFreeTCPPort()
if err != nil {
return nil, err
}
}
// should be a one item list, we use the same node that the primary wallet is using
monerodNode := conf.MonerodNodes[0]
proc, err := createWalletRPCService(
conf.Env,
conf.MoneroWalletRPCPath,
conf.WalletPort,
path.Dir(conf.WalletFilePath),
conf.LogPath,
monerodNode,
)
if err != nil {
return nil, err
}
c := NewThinWalletClient(monerodNode.Host, monerodNode.Port, conf.WalletPort).(*walletClient)
c.rpcProcess = proc
c.conf = conf
err = c.generateFromKeys(
privateSpendKey, // nil for a view-only wallet
privateViewKey,
address,
walletRestoreHeight,
path.Base(conf.WalletFilePath),
conf.WalletPassword,
)
if err != nil {
c.Close()
return nil, err
}
acctResp, err := c.GetAddress(0)
if err != nil {
c.Close()
return nil, err
}
c.walletAddr, err = mcrypto.NewAddress(acctResp.Address, conf.Env)
if err != nil {
c.Close()
return nil, err
}
if !c.walletAddr.Equal(address) {
c.Close()
return nil, fmt.Errorf("provided address %s does not match monero-wallet-rpc computed address %s",
address, c.walletAddr)
}
bal, err := c.GetBalance(0)
if err != nil {
c.Close()
return nil, err
}
log.Infof("Created wallet %s, balance is %s XMR (%d blocks to unlock), address is %s",
c.WalletName(),
coins.FmtPiconeroAsXMR(bal.Balance),
bal.BlocksToUnlock,
c.PrimaryAddress(),
)
return c, nil
}
// CreateSpendWalletFromKeys creates a new monero-wallet-rpc process, wallet client and
// spend wallet for the passed private key pair (view key and spend key).
func CreateSpendWalletFromKeys(
conf *WalletClientConf,
privateKeyPair *mcrypto.PrivateKeyPair,
restoreHeight uint64,
) (WalletClient, error) {
privateViewKey := privateKeyPair.ViewKey()
privateSpendKey := privateKeyPair.SpendKey()
address := privateKeyPair.PublicKeyPair().Address(conf.Env)
return createWalletFromKeys(conf, restoreHeight, privateSpendKey, privateViewKey, address)
}
// CreateViewOnlyWalletFromKeys creates a new monero-wallet-rpc process, wallet client and
// view-only wallet for the passed private view key and address.
func CreateViewOnlyWalletFromKeys(
conf *WalletClientConf,
privateViewKey *mcrypto.PrivateViewKey,
address *mcrypto.Address,
restoreHeight uint64,
) (WalletClient, error) {
return createWalletFromKeys(conf, restoreHeight, nil, privateViewKey, address)
}
func (c *walletClient) generateFromKeys(
sk *mcrypto.PrivateSpendKey,
vk *mcrypto.PrivateViewKey,
address *mcrypto.Address,
restoreHeight uint64,
filename,
password string,
) error {
const (
successMessage = "Wallet has been generated successfully."
viewOnlySuccessMessage = "Watch-only wallet has been generated successfully."
)
spendKey := "" // not used when only generating a view key
if sk != nil {
spendKey = sk.Hex()
}
res, err := c.wRPC.GenerateFromKeys(&wallet.GenerateFromKeysRequest{
Filename: filename,
Address: address.String(),
RestoreHeight: restoreHeight,
Viewkey: vk.Hex(),
Spendkey: spendKey,
Password: password,
})
if err != nil {
return err
}
expectedMessage := successMessage
if spendKey == "" {
expectedMessage = viewOnlySuccessMessage
}
if res.Info != expectedMessage {
return fmt.Errorf("got unexpected Info string: %s", res.Info)
}
return nil
}
func (c *walletClient) GetAddress(idx uint32) (*wallet.GetAddressResponse, error) {
return c.wRPC.GetAddress(&wallet.GetAddressRequest{
AccountIndex: idx,
})
}
func (c *walletClient) refresh() error {
_, err := c.wRPC.Refresh(&wallet.RefreshRequest{})
return err
}
func (c *walletClient) CreateWallet(filename, password string) error {
return c.wRPC.CreateWallet(&wallet.CreateWalletRequest{
Filename: filename,
Password: password,
Language: "English",
})
}
func (c *walletClient) PrimaryAddress() *mcrypto.Address {
if c.walletAddr == nil {
// Initialised in constructor function, so this shouldn't ever happen
panic("primary wallet address was not initialised")
}
return c.walletAddr
}
func (c *walletClient) GetHeight() (uint64, error) {
if err := c.refresh(); err != nil {
return 0, err
}
res, err := c.wRPC.GetHeight()
if err != nil {
return 0, err
}
return res.Height, nil
}
// getChainHeight gets the blockchain height directly from the monero daemon instead
// of the wallet height.
func (c *walletClient) getChainHeight() (uint64, error) {
res, err := c.dRPC.GetBlockCount()
if err != nil {
return 0, err
}
return res.Count, nil
}
func (c *walletClient) Endpoint() string {
return c.endpoint
}
// Close kills the monero-wallet-rpc process closing the wallet. It is designed to only be
// called a single time from a single go process.
func (c *walletClient) Close() {
if c.rpcProcess == nil {
return // no monero-wallet-rpc instance was created
}
p := c.rpcProcess
err := c.wRPC.StopWallet()
if err != nil {
log.Warnf("StopWallet errored: %s", err)
err = p.Kill() // uses, SIG-TERM, which monero-wallet-rpc has a handler for
}
// If err is nil at this point, the process existed, and we block until the child
// process exits. (Note: kill does not error when signaling an exited, but non-reaped
// child.)
if err == nil {
_, _ = p.Wait()
}
}
// CloseAndRemoveWallet kills the monero-wallet-rpc process and removes the wallet files. This
// should never be called on the user's primary wallet. It is for temporary swap wallets only.
// Call this function at most once from a single go process.
func (c *walletClient) CloseAndRemoveWallet() {
c.Close()
// Just log any file removal errors, as there is nothing useful the caller can do
// with the errors
if err := os.Remove(c.conf.WalletFilePath); err != nil {
log.Errorf("Failed to remove wallet file %q: %s", c.conf.WalletFilePath, err)
}
if err := os.Remove(c.conf.WalletFilePath + ".keys"); err != nil {
log.Errorf("Failed to remove wallet keys file %q: %s", c.conf.WalletFilePath, err)
}
if err := os.Remove(c.conf.WalletFilePath + ".address.txt"); err != nil {
// .address.txt doesn't always exist, only log if it existed and we failed
if !errors.Is(err, fs.ErrNotExist) {
log.Errorf("Failed to remove wallet address file %q: %s", c.conf.WalletFilePath, err)
}
}
}
func findWorkingNode(env common.Environment, nodes []*common.MoneroNode) (*common.MoneroNode, error) {
if len(nodes) == 0 {
return nil, errors.New("no monero nodes")
}
var err error
for _, n := range nodes {
err = validateMonerodNode(env, n)
if err != nil {
log.Warnf("Non-working node: %s", err)
continue
}
return n, nil
}
// err is non-nil if we get here
return nil, fmt.Errorf("failed to validate any monerod RPC node, last error: %w", err)
}
// validateMonerodNode validates the monerod node before we launch monero-wallet-rpc, as
// doing the pre-checks creates more obvious error messages and faster failure.
func validateMonerodNode(env common.Environment, node *common.MoneroNode) error {
endpoint := fmt.Sprintf("http://%s:%d/json_rpc", node.Host, node.Port)
daemonCli := monerorpc.New(endpoint, nil).Daemon
info, err := daemonCli.GetInfo()
if err != nil {
return fmt.Errorf("could not validate monerod endpoint %s: %w", endpoint, err)
}
switch env {
case common.Stagenet:
if !info.Stagenet {
return fmt.Errorf("monerod endpoint %s is not a stagenet node", endpoint)
}
case common.Mainnet:
if !info.Mainnet {
return fmt.Errorf("monerod endpoint %s is not a mainnet node", endpoint)
}
case common.Development:
if info.NetType != "fakechain" {
return fmt.Errorf("monerod endpoint %s should have a network type of \"fakechain\" in dev mode",
endpoint)
}
default:
panic("unhandled environment type")
}
if env != common.Development && info.Offline {
return fmt.Errorf("monerod endpoint %s is offline", endpoint)
}
if !info.Synchronized {
return fmt.Errorf("monerod endpoint %s is not synchronised", endpoint)
}
return nil
}
// createWalletRPCService starts a monero-wallet-rpc instance. Default values are assigned
// to the MonerodHost, MonerodPort, WalletPort and LogPath fields of the config if they
// are not already set.
func createWalletRPCService(
env common.Environment,
walletRPCBinPath string,
walletPort uint,
walletDir string,
logFilePath string,
moneroNode *common.MoneroNode,
) (*os.Process, error) {
walletRPCBinArgs := getWalletRPCFlags(env, walletPort, walletDir, logFilePath, moneroNode)
proc, err := launchMoneroWalletRPCChild(walletRPCBinPath, walletRPCBinArgs...)
if err != nil {
return nil, fmt.Errorf("%w, see %s for details", err, logFilePath)
}
return proc, nil
}
// getMoneroWalletRPCBin returns the monero-wallet-rpc binary. It first looks for
// "./monero-bin/monero-wallet-rpc". If not found, it then looks for "monero-wallet-rpc"
// in the user's path.
func getMoneroWalletRPCBin() (string, error) {
execName := "monero-wallet-rpc"
priorityPath := path.Join("monero-bin", execName)
execPath, err := exec.LookPath(priorityPath)
if err == nil {
return execPath, nil
}
if !errors.Is(err, os.ErrNotExist) {
return "", err
}
// search for the executable in the user's PATH
return exec.LookPath(execName)
}
// getSysProcAttr returns SysProcAttr values that will work on all platforms, but this
// function is overwritten on Linux and FreeBSD.
var getSysProcAttr = func() *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}
func launchMoneroWalletRPCChild(walletRPCBin string, walletRPCBinArgs ...string) (*os.Process, error) {
cmd := exec.Command(walletRPCBin, walletRPCBinArgs...)
pRead, pWrite, err := os.Pipe()
if err != nil {
return nil, err
}
cmd.Stdout = pWrite
cmd.Stderr = pWrite
// Last entry wins if an environment variable is in the list multiple times.
// We parse some output, so we want to force English. NO_COLOR=1 failed to
// remove ansi colour escapes, but setting TERM=dumb succeeded.
cmd.Env = append(os.Environ(), "LANG=C", "LC_ALL=C", "TERM=dumb")
cmd.SysProcAttr = getSysProcAttr()
err = cmd.Start()
// The writing side of the pipe will remain open in the child process after we close it
// here, and the reading side will get an EOF after the last writing side closes. We
// need to close the parent writing side after starting the child, in order for the child
// to inherit the pipe's file descriptor for Stdout/Stderr. We can't close it in a defer
// statement, because we need the scanner below to get EOF if the child process exits
// on error.
_ = pWrite.Close()
// Handle err from cmd.Start() above
if err != nil {
return nil, err
}
log.Debugf("Started monero-wallet-rpc with PID=%d", cmd.Process.Pid)
scanner := bufio.NewScanner(pRead)
started := false
// Loop terminates when the child process exits or when we get the message that the RPC server started
for scanner.Scan() {
line := scanner.Text()
// Skips the first 3 lines of boilerplate output, but logs the version after it
if line != "This is the RPC monero wallet. It needs to connect to a monero" &&
line != "daemon to work correctly." &&
line != "" {
log.Info(moneroWalletRPCLogPrefix, line)
}
if strings.HasSuffix(line, "Starting wallet RPC server") {
started = true
break
}
}
if !started {
_, _ = cmd.Process.Wait() // shouldn't block, process already exited
return nil, errors.New("failed to start monero-wallet-rpc")
}
time.Sleep(200 * time.Millisecond) // additional start time
// Drain additional output. We are not detaching monero-wallet-rpc so it will
// die when we exit. This has the downside that logs are sent both to the
// monero-wallet-rpc.log file and to standard output.
go func() {
for scanner.Scan() {
// We could log here, but it's noisy and we have a separate log file with
// full logs. A future version could parse the log messages and send some
// filtered subset to swapd's logs.
}
log.Warnf("monero-wallet-rpc pid=%d exited", cmd.Process.Pid)
}()
return cmd.Process, nil
}
// getWalletRPCFlags returns the flags used when launching monero-wallet-rpc
func getWalletRPCFlags(
env common.Environment,
walletPort uint,
walletDir string,
logFilePath string,
moneroNode *common.MoneroNode,
) []string {
args := []string{
"--rpc-bind-ip=127.0.0.1",
fmt.Sprintf("--rpc-bind-port=%d", walletPort),
"--disable-rpc-login", // TODO: Enable this?
fmt.Sprintf("--wallet-dir=%s", walletDir),
fmt.Sprintf("--log-file=%s", logFilePath),
"--log-level=0",
fmt.Sprintf("--daemon-host=%s", moneroNode.Host),
fmt.Sprintf("--daemon-port=%d", moneroNode.Port),
}
switch env {
case common.Development:
// See https://github.com/monero-project/monero/issues/8600
args = append(args, "--allow-mismatched-daemon-version")
case common.Mainnet:
// do nothing
case common.Stagenet:
args = append(args, "--stagenet")
default:
panic("unhandled monero environment type")
}
return args
}