mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
841 lines
25 KiB
Go
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
|
|
}
|