mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
370 lines
12 KiB
Go
370 lines
12 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
package monero
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
logging "github.com/ipfs/go-log/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/athanorlabs/atomic-swap/coins"
|
|
"github.com/athanorlabs/atomic-swap/common"
|
|
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
|
)
|
|
|
|
var moneroWalletRPCPath = path.Join("..", "monero-bin", "monero-wallet-rpc")
|
|
|
|
func init() {
|
|
_ = logging.SetLogLevel("monero", "debug")
|
|
}
|
|
|
|
func TestClient_Transfer(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
transferAmt := coins.MoneroToPiconero(coins.StrToDecimal("10"))
|
|
transferAmtPlusFees := coins.MoneroToPiconero(coins.StrToDecimal("10.01"))
|
|
|
|
cXMRMaker := CreateWalletClient(t)
|
|
MineMinXMRBalance(t, cXMRMaker, transferAmtPlusFees)
|
|
|
|
balanceBob := GetBalance(t, cXMRMaker)
|
|
t.Logf("Bob's initial balance: bal=%s XMR, unlocked=%s XMR, blocks-to-unlock=%d",
|
|
coins.FmtPiconeroAsXMR(balanceBob.Balance),
|
|
coins.FmtPiconeroAsXMR(balanceBob.UnlockedBalance),
|
|
balanceBob.BlocksToUnlock)
|
|
|
|
transferAmtU64, err := transferAmt.Uint64()
|
|
require.NoError(t, err)
|
|
require.Greater(t, balanceBob.UnlockedBalance, transferAmtU64)
|
|
|
|
kpA, err := mcrypto.GenerateKeys()
|
|
require.NoError(t, err)
|
|
|
|
kpB, err := mcrypto.GenerateKeys()
|
|
require.NoError(t, err)
|
|
|
|
abAddress := mcrypto.SumSpendAndViewKeys(kpA.PublicKeyPair(), kpB.PublicKeyPair()).Address(common.Development)
|
|
vkABPriv := mcrypto.SumPrivateViewKeys(kpA.ViewKey(), kpB.ViewKey())
|
|
|
|
// Transfer from Bob's account to the Alice+Bob swap account
|
|
transfer, err := cXMRMaker.Transfer(ctx, abAddress, 0, transferAmt, MinSpendConfirmations)
|
|
require.NoError(t, err)
|
|
t.Logf("Bob sent %s (+fee %s) XMR to A+B address with TX ID %s",
|
|
coins.FmtPiconeroAsXMR(transfer.Amount),
|
|
coins.FmtPiconeroAsXMR(transfer.Fee),
|
|
transfer.TxID)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, transfer.Confirmations, uint64(MinSpendConfirmations))
|
|
t.Logf("Bob's TX was mined at height %d with %d confirmations", transfer.Height, transfer.Confirmations)
|
|
cXMRMaker.Close() // Done with bob, make sure no one uses him again
|
|
cXMRMaker = nil
|
|
|
|
// Establish Alice's primary wallet
|
|
alicePrimaryWallet := "test-swap-wallet"
|
|
cXMRTaker, err := NewWalletClient(&WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: path.Join(t.TempDir(), "wallet", alicePrimaryWallet),
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
})
|
|
require.NoError(t, err)
|
|
alicePrimaryAddr := cXMRTaker.PrimaryAddress()
|
|
|
|
// Alice generates a view-only wallet for A+B to confirm that Bob sent the funds
|
|
conf := cXMRTaker.CreateWalletConf("alice-view-wallet-to-verify-funds")
|
|
abViewCli, err := CreateViewOnlyWalletFromKeys(conf, vkABPriv, abAddress, transfer.Height)
|
|
require.NoError(t, err)
|
|
defer abViewCli.CloseAndRemoveWallet()
|
|
|
|
balanceABWal := GetBalance(t, abViewCli)
|
|
height, err := abViewCli.GetHeight()
|
|
require.NoError(t, err)
|
|
t.Logf("A+B View-Only wallet balance: bal=%s unlocked=%s blocks-to-unlock=%d, cur-height=%d",
|
|
coins.FmtPiconeroAsXMR(balanceABWal.Balance),
|
|
coins.FmtPiconeroAsXMR(balanceABWal.UnlockedBalance),
|
|
balanceABWal.BlocksToUnlock, height)
|
|
require.Zero(t, balanceABWal.BlocksToUnlock)
|
|
require.Equal(t, balanceABWal.Balance, balanceABWal.UnlockedBalance)
|
|
require.Equal(t, transferAmtU64, balanceABWal.UnlockedBalance)
|
|
|
|
// At this point Alice has received the key from Bob to create an A+B spend wallet.
|
|
// She'll now sweep the funds from the A+B spend wallet into her primary wallet.
|
|
abWalletKeyPair := mcrypto.NewPrivateKeyPair(
|
|
mcrypto.SumPrivateSpendKeys(kpA.SpendKey(), kpB.SpendKey()),
|
|
mcrypto.SumPrivateViewKeys(kpA.ViewKey(), kpB.ViewKey()),
|
|
)
|
|
require.NoError(t, err)
|
|
conf = abViewCli.CreateWalletConf("alice-spend-wallet-to-claim")
|
|
abSpendCli, err := CreateSpendWalletFromKeys(conf, abWalletKeyPair, transfer.Height)
|
|
require.NoError(t, err)
|
|
defer abSpendCli.CloseAndRemoveWallet()
|
|
require.Equal(t, abSpendCli.PrimaryAddress(), abViewCli.PrimaryAddress())
|
|
|
|
balanceABWal = GetBalance(t, abSpendCli)
|
|
// Verify that the spend wallet, like the view-only wallet, has the exact amount expected in it
|
|
require.Equal(t, transferAmtU64, balanceABWal.UnlockedBalance)
|
|
|
|
// Alice transfers from A+B spend wallet to her primary wallet's address
|
|
transfers, err := abSpendCli.SweepAll(ctx, alicePrimaryAddr, 0, SweepToSelfConfirmations)
|
|
require.NoError(t, err)
|
|
t.Logf("Alice swept AB wallet funds with %d transfers", len(transfers))
|
|
require.Len(t, transfers, 1) // In our case, it should always be a single transaction
|
|
sweepAmount := transfers[0].Amount
|
|
sweepFee := transfers[0].Fee
|
|
|
|
t.Logf("Sweep of A+B wallet sent %s XMR with fees %s XMR to Alice's primary wallet",
|
|
coins.FmtPiconeroAsXMR(sweepAmount), coins.FmtPiconeroAsXMR(sweepFee))
|
|
require.Equal(t, transferAmtU64, sweepAmount+sweepFee)
|
|
t.Logf("Alice's sweep transactions was mined at height %d with %d confirmations",
|
|
transfer.Height, transfer.Confirmations)
|
|
|
|
// Verify zero balance of A+B wallet after sweep
|
|
balanceABWal = GetBalance(t, abSpendCli)
|
|
require.Equal(t, balanceABWal.Balance, uint64(0))
|
|
|
|
balanceAlice := GetBalance(t, cXMRTaker)
|
|
t.Logf("Alice's primary wallet after sweep: bal=%s XMR, unlocked=%s XMR, blocks-to-unlock=%d",
|
|
coins.FmtPiconeroAsXMR(balanceAlice.Balance),
|
|
coins.FmtPiconeroAsXMR(balanceAlice.UnlockedBalance),
|
|
balanceAlice.BlocksToUnlock)
|
|
require.Equal(t, balanceAlice.Balance, sweepAmount)
|
|
}
|
|
|
|
func Test_walletClient_SweepAll_nothingToSweepReturnsError(t *testing.T) {
|
|
emptyWallet := CreateWalletClient(t)
|
|
takerWallet := CreateWalletClient(t)
|
|
|
|
addrResp, err := takerWallet.GetAddress(0)
|
|
require.NoError(t, err)
|
|
|
|
destAddr, err := mcrypto.NewAddress(addrResp.Address, common.Development)
|
|
require.NoError(t, err)
|
|
|
|
_, err = emptyWallet.SweepAll(context.Background(), destAddr, 0, SweepToSelfConfirmations)
|
|
require.ErrorContains(t, err, "no balance to sweep")
|
|
}
|
|
|
|
func TestClient_CloseAndRemoveWallet(t *testing.T) {
|
|
password := t.Name()
|
|
walletPath := path.Join(t.TempDir(), "wallet", "test-wallet")
|
|
c, err := NewWalletClient(&WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: walletPath,
|
|
WalletPassword: password,
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
})
|
|
require.NoError(t, err)
|
|
info, err := os.Stat(walletPath)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, info.Size(), int64(0))
|
|
c.CloseAndRemoveWallet()
|
|
_, err = os.Stat(walletPath)
|
|
require.ErrorIs(t, err, os.ErrNotExist)
|
|
}
|
|
|
|
func TestClient_GetAccounts(t *testing.T) {
|
|
c, err := NewWalletClient(&WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: path.Join(t.TempDir(), "wallet", "test-wallet"),
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
})
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
resp, err := c.GetAccounts()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(resp.SubaddressAccounts))
|
|
}
|
|
|
|
func TestClient_GetHeight(t *testing.T) {
|
|
c, err := NewWalletClient(&WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: path.Join(t.TempDir(), "wallet", "test-wallet"),
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
})
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
walletHeight, err := c.GetHeight()
|
|
require.NoError(t, err)
|
|
chainHeight, err := c.(*walletClient).getChainHeight()
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, chainHeight, walletHeight)
|
|
require.LessOrEqual(t, chainHeight-walletHeight, uint64(2))
|
|
}
|
|
|
|
func TestCallGenerateFromKeys(t *testing.T) {
|
|
kp, err := mcrypto.GenerateKeys()
|
|
require.NoError(t, err)
|
|
|
|
c, err := NewWalletClient(&WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: path.Join(t.TempDir(), "wallet", "not-used"),
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
})
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
height, err := c.GetHeight()
|
|
require.NoError(t, err)
|
|
|
|
addr, err := c.GetAddress(0)
|
|
require.NoError(t, err)
|
|
t.Logf("Address %s", addr.Address)
|
|
|
|
// initial wallet automatically closed when a new wallet is opened
|
|
err = c.(*walletClient).generateFromKeys(
|
|
kp.SpendKey(),
|
|
kp.ViewKey(),
|
|
kp.PublicKeyPair().Address(common.Mainnet),
|
|
height,
|
|
"swap-deposit-wallet",
|
|
"",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
addr, err = c.GetAddress(0)
|
|
require.NoError(t, err)
|
|
t.Logf("Address %s", addr.Address)
|
|
}
|
|
|
|
// this tests calling generateFromkeys passing an address derived in
|
|
// a non-standard manner; ie. the public view key in the address doesn't
|
|
// match the private view key passed in.
|
|
func TestCallGenerateFromKeys_UnusualAddress(t *testing.T) {
|
|
kp, err := mcrypto.GenerateKeys()
|
|
require.NoError(t, err)
|
|
|
|
kp2, err := mcrypto.GenerateKeys()
|
|
require.NoError(t, err)
|
|
|
|
// create keypair with priv spend key of kp, but a different priv view key
|
|
// use the address of this keypair in the call to `generateFromKeys`
|
|
kp3 := mcrypto.NewPrivateKeyPair(kp.SpendKey(), kp2.ViewKey())
|
|
address := kp3.PublicKeyPair().Address(common.Mainnet)
|
|
t.Log("address", address)
|
|
|
|
conf := &WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: path.Join(t.TempDir(), "wallet", "not-used"),
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
}
|
|
err = conf.Fill()
|
|
require.NoError(t, err)
|
|
|
|
c, err := createWalletFromKeys(
|
|
conf,
|
|
0,
|
|
kp.SpendKey(),
|
|
kp.ViewKey(),
|
|
kp3.PublicKeyPair().Address(common.Mainnet),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
res, err := c.GetAddress(0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, address.String(), res.Address)
|
|
}
|
|
|
|
func Test_getMoneroWalletRPCBin(t *testing.T) {
|
|
wd, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
defer os.Chdir(wd)
|
|
os.Chdir("..")
|
|
walletRPCPath, err := getMoneroWalletRPCBin()
|
|
require.NoError(t, err)
|
|
// monero-bin/monero-wallet-rpc should take precedence over any monero-wallet-rpc in
|
|
// the user's path if the relative path to the binary exists.
|
|
require.Equal(t, "monero-bin/monero-wallet-rpc", walletRPCPath)
|
|
}
|
|
|
|
func Test_validateMonerodConfigs_dev(t *testing.T) {
|
|
env := common.Development
|
|
node, err := findWorkingNode(env, common.ConfigDefaultsForEnv(env).MoneroNodes)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
}
|
|
|
|
func Test_validateMonerodConfigs_stagenet(t *testing.T) {
|
|
env := common.Stagenet
|
|
node, err := findWorkingNode(env, common.ConfigDefaultsForEnv(env).MoneroNodes)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
}
|
|
|
|
func Test_validateMonerodConfigs_mainnet(t *testing.T) {
|
|
env := common.Mainnet
|
|
node, err := findWorkingNode(env, common.ConfigDefaultsForEnv(env).MoneroNodes)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
}
|
|
|
|
func Test_validateMonerodConfig_misMatchedEnv(t *testing.T) {
|
|
node := &common.MoneroNode{
|
|
Host: "127.0.0.1",
|
|
Port: common.DefaultMoneroDaemonDevPort,
|
|
}
|
|
err := validateMonerodNode(common.Mainnet, node)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "is not a mainnet node")
|
|
}
|
|
|
|
func Test_validateMonerodConfig_invalidPort(t *testing.T) {
|
|
nonUsedPort, err := common.GetFreeTCPPort()
|
|
require.NoError(t, err)
|
|
node := &common.MoneroNode{
|
|
Host: "127.0.0.1",
|
|
Port: nonUsedPort,
|
|
}
|
|
err = validateMonerodNode(common.Development, node)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "connection refused")
|
|
}
|
|
|
|
func Test_walletClient_waitForConfirmations_contextCancelled(t *testing.T) {
|
|
const amount = 10
|
|
const numConfirmations = 999999999 // won't be achieved before our context is cancelled
|
|
|
|
minBal := coins.MoneroToPiconero(coins.StrToDecimal("10.01")) // add a little extra for fees
|
|
destAddr, err := mcrypto.NewAddress(blockRewardAddress, common.Development)
|
|
require.NoError(t, err)
|
|
|
|
c := CreateWalletClient(t)
|
|
MineMinXMRBalance(t, c, minBal)
|
|
|
|
// Cancel the context after the transfer has started
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = c.Transfer(ctx, destAddr, 0, coins.NewPiconeroAmount(amount), numConfirmations)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
}
|
|
|
|
func TestCreateWalletFromKeys(t *testing.T) {
|
|
// First wallet is just to get the height and generate the config for the 2nd
|
|
primaryCli, err := NewWalletClient(&WalletClientConf{
|
|
Env: common.Development,
|
|
WalletFilePath: path.Join(t.TempDir(), "wallet", "not-used"),
|
|
MoneroWalletRPCPath: moneroWalletRPCPath,
|
|
})
|
|
require.NoError(t, err)
|
|
defer primaryCli.Close()
|
|
|
|
height, err := primaryCli.GetHeight()
|
|
require.NoError(t, err)
|
|
|
|
kp, err := mcrypto.GenerateKeys()
|
|
require.NoError(t, err)
|
|
|
|
conf := primaryCli.CreateWalletConf("ab-wallet")
|
|
abCli, err := CreateSpendWalletFromKeys(conf, kp, height)
|
|
require.NoError(t, err)
|
|
defer abCli.CloseAndRemoveWallet()
|
|
require.Equal(t, kp.PublicKeyPair().Address(common.Development).String(), abCli.PrimaryAddress().String())
|
|
}
|