mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-10 06:38:04 -05:00
* version independent monerod path with localhost-only binding * fixed lint target and install script * created 3rd ETH key constant to avoid race condition in tests * updated run-integration-tests.sh with the same changes made to run-unit-tests.sh * new design for paritioning out 2 unique keys to test packages * restored accidentally deleted tests/alice.key * removed websocket connection leaks from tests * made file paths unique between tests with better file cleanup * fix for the websocket connection leak commit * reverted increase of GenerateBlocks (didn't mean to commit that) * fixed maker/taker key that I had reversed * fixed incorrect zero-balance check * fixed comment on ClaimOrRefund * added back sample script for installing go in /usr/local/go * etchclient creation cleanup using t.Cleanup() * minor cleanup to ganache_test_keys code * initial dynamic monero-wallet-rpc implementation for tests * converted monero tests to use dynamic monero-wallet-rpc services * unit tests all using dynamic monero-wallet-rpc services * fixed 2 tests that failed after moving to dynamic monero-wallet-rpc services * fixed low balance issues in TestSwapState_NotifyClaimed Co-authored-by: noot <36753753+noot@users.noreply.github.com>
356 lines
9.4 KiB
Go
356 lines
9.4 KiB
Go
package xmrtaker
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/noot/atomic-swap/common"
|
|
"github.com/noot/atomic-swap/common/types"
|
|
mcrypto "github.com/noot/atomic-swap/crypto/monero"
|
|
"github.com/noot/atomic-swap/monero"
|
|
"github.com/noot/atomic-swap/net"
|
|
"github.com/noot/atomic-swap/net/message"
|
|
pcommon "github.com/noot/atomic-swap/protocol"
|
|
"github.com/noot/atomic-swap/swapfactory"
|
|
|
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
|
"github.com/fatih/color" //nolint:misspell
|
|
)
|
|
|
|
// HandleProtocolMessage is called by the network to handle an incoming message.
|
|
// If the message received is not the expected type for the point in the protocol we're at,
|
|
// this function will return an error.
|
|
func (s *swapState) HandleProtocolMessage(msg net.Message) (net.Message, bool, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
|
|
if err := s.checkMessageType(msg); err != nil {
|
|
return nil, true, err
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case *net.SendKeysMessage:
|
|
resp, err := s.handleSendKeysMessage(msg)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
|
|
return resp, false, nil
|
|
case *message.NotifyXMRLock:
|
|
out, err := s.handleNotifyXMRLock(msg)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
|
|
return out, false, nil
|
|
case *message.NotifyClaimed:
|
|
_, err := s.handleNotifyClaimed(msg.TxHash)
|
|
if err != nil {
|
|
log.Error("failed to create monero address: err=", err)
|
|
return nil, true, err
|
|
}
|
|
|
|
s.clearNextExpectedMessage(types.CompletedSuccess)
|
|
return nil, true, nil
|
|
default:
|
|
return nil, false, errUnexpectedMessageType
|
|
}
|
|
}
|
|
|
|
func (s *swapState) clearNextExpectedMessage(status types.Status) {
|
|
s.nextExpectedMessage = nil
|
|
s.info.SetStatus(status)
|
|
if s.statusCh != nil {
|
|
s.statusCh <- status
|
|
}
|
|
}
|
|
|
|
func (s *swapState) setNextExpectedMessage(msg net.Message) {
|
|
if s == nil || s.nextExpectedMessage == nil {
|
|
return
|
|
}
|
|
|
|
if msg.Type() == s.nextExpectedMessage.Type() {
|
|
return
|
|
}
|
|
|
|
s.nextExpectedMessage = msg
|
|
|
|
// TODO: check stage is not unknown (ie. swap completed)
|
|
stage := pcommon.GetStatus(msg.Type())
|
|
if s.statusCh != nil {
|
|
s.statusCh <- stage
|
|
}
|
|
}
|
|
|
|
func (s *swapState) checkMessageType(msg net.Message) error {
|
|
if msg == nil {
|
|
return errNilMessage
|
|
}
|
|
|
|
if s.nextExpectedMessage == nil {
|
|
return nil
|
|
}
|
|
|
|
if msg.Type() != s.nextExpectedMessage.Type() {
|
|
return errIncorrectMessageType
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message, error) {
|
|
if msg.ProvidedAmount < s.info.ReceivedAmount() {
|
|
return nil, fmt.Errorf("receiving amount is not the same as expected: got %v, expected %v",
|
|
msg.ProvidedAmount,
|
|
s.info.ReceivedAmount(),
|
|
)
|
|
}
|
|
|
|
if msg.PublicSpendKey == "" || msg.PrivateViewKey == "" {
|
|
return nil, errMissingKeys
|
|
}
|
|
|
|
if msg.EthAddress == "" {
|
|
return nil, errMissingAddress
|
|
}
|
|
|
|
vk, err := mcrypto.NewPrivateViewKeyFromHex(msg.PrivateViewKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate XMRMaker's private view keys: %w", err)
|
|
}
|
|
|
|
s.xmrmakerAddress = ethcommon.HexToAddress(msg.EthAddress)
|
|
|
|
log.Debugf("got XMRMaker's keys and address: address=%s", s.xmrmakerAddress)
|
|
|
|
sk, err := mcrypto.NewPublicKeyFromHex(msg.PublicSpendKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate XMRMaker's public spend key: %w", err)
|
|
}
|
|
|
|
// verify counterparty's DLEq proof and ensure the resulting secp256k1 key is correct
|
|
secp256k1Pub, err := pcommon.VerifyKeysAndProof(msg.DLEqProof, msg.Secp256k1PublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof(color.New(color.Bold).Sprintf("receiving %v XMR for %v ETH", msg.ProvidedAmount, s.info.ProvidedAmount()))
|
|
|
|
s.setXMRMakerKeys(sk, vk, secp256k1Pub)
|
|
txHash, err := s.lockETH(s.providedAmountInWei())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to lock ETH in contract: %w", err)
|
|
}
|
|
|
|
log.Info("locked ether in swap contract, waiting for XMR to be locked")
|
|
|
|
// start goroutine to check that XMRMaker locks before t_0
|
|
go func() {
|
|
// TODO: this variable is so that we definitely refund before t0.
|
|
// this will vary based on environment (eg. development should be very small,
|
|
// a network with slower block times should be longer)
|
|
const timeoutBuffer = time.Second * 5
|
|
until := time.Until(s.t0)
|
|
|
|
log.Debugf("time until refund: %vs", until.Seconds())
|
|
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case <-time.After(until - timeoutBuffer):
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
|
|
if !s.info.Status().IsOngoing() {
|
|
return
|
|
}
|
|
|
|
// XMRMaker hasn't locked yet, let's call refund
|
|
txhash, err := s.refund()
|
|
if err != nil {
|
|
log.Errorf("failed to refund: err=%s", err)
|
|
return
|
|
}
|
|
|
|
log.Infof("got our ETH back: tx hash=%s", txhash)
|
|
|
|
// send NotifyRefund msg
|
|
if err := s.SendSwapMessage(&message.NotifyRefund{
|
|
TxHash: txhash.String(),
|
|
}, s.ID()); err != nil {
|
|
log.Errorf("failed to send refund message: err=%s", err)
|
|
}
|
|
case <-s.xmrLockedCh:
|
|
return
|
|
}
|
|
|
|
}()
|
|
|
|
s.setNextExpectedMessage(&message.NotifyXMRLock{})
|
|
|
|
out := &message.NotifyETHLocked{
|
|
Address: s.ContractAddr().String(),
|
|
TxHash: txHash.String(),
|
|
ContractSwapID: s.contractSwapID,
|
|
ContractSwap: pcommon.ConvertContractSwapToMsg(s.contractSwap),
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (s *swapState) handleNotifyXMRLock(msg *message.NotifyXMRLock) (net.Message, error) {
|
|
if msg.Address == "" {
|
|
return nil, errNoLockedXMRAddress
|
|
}
|
|
|
|
// check that XMR was locked in expected account, and confirm amount
|
|
vk := mcrypto.SumPrivateViewKeys(s.xmrmakerPrivateViewKey, s.privkeys.ViewKey())
|
|
sk := mcrypto.SumPublicKeys(s.xmrmakerPublicSpendKey, s.pubkeys.SpendKey())
|
|
kp := mcrypto.NewPublicKeyPair(sk, vk.Public())
|
|
|
|
if msg.Address != string(kp.Address(s.Env())) {
|
|
return nil, fmt.Errorf("address received in message does not match expected address")
|
|
}
|
|
|
|
s.LockClient()
|
|
defer s.UnlockClient()
|
|
|
|
t := time.Now().Format("2006-01-02-15:04:05.999999999")
|
|
walletName := fmt.Sprintf("xmrtaker-viewonly-wallet-%s", t)
|
|
if err := s.GenerateViewOnlyWalletFromKeys(vk, kp.Address(s.Env()), walletName, ""); err != nil {
|
|
return nil, fmt.Errorf("failed to generate view-only wallet to verify locked XMR: %w", err)
|
|
}
|
|
|
|
log.Debugf("generated view-only wallet to check funds: %s", walletName)
|
|
|
|
if s.Env() != common.Development {
|
|
log.Infof("waiting for new blocks...")
|
|
// wait for 2 new blocks, otherwise balance might be 0
|
|
// TODO: check transaction hash
|
|
height, err := monero.WaitForBlocks(s.Backend, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof("monero block height: %d", height)
|
|
}
|
|
|
|
log.Debug("refreshing client...")
|
|
|
|
if err := s.Refresh(); err != nil {
|
|
return nil, fmt.Errorf("failed to refresh client: %w", err)
|
|
}
|
|
|
|
accounts, err := s.GetAccounts()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get accounts: %w", err)
|
|
}
|
|
|
|
var (
|
|
balance *monero.GetBalanceResponse
|
|
)
|
|
|
|
for i, acc := range accounts.SubaddressAccounts {
|
|
addr, ok := acc["base_address"].(string)
|
|
if !ok {
|
|
panic("address is not a string!")
|
|
}
|
|
|
|
if mcrypto.Address(addr) == kp.Address(s.Env()) {
|
|
balance, err = s.GetBalance(uint(i))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get balance: %w", err)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if balance == nil {
|
|
return nil, fmt.Errorf("failed to find account with address %s", kp.Address(s.Env()))
|
|
}
|
|
|
|
log.Debugf("checking locked wallet, address=%s balance=%v", kp.Address(s.Env()), balance.Balance)
|
|
|
|
// TODO: also check that the balance isn't unlocked only after an unreasonable amount of blocks
|
|
if balance.Balance < float64(s.receivedAmountInPiconero()) {
|
|
return nil, fmt.Errorf("locked XMR amount is less than expected: got %v, expected %v",
|
|
balance.Balance, float64(s.receivedAmountInPiconero()))
|
|
}
|
|
|
|
if err := s.CloseWallet(); err != nil {
|
|
return nil, fmt.Errorf("failed to close wallet: %w", err)
|
|
}
|
|
|
|
close(s.xmrLockedCh)
|
|
log.Info("XMR was locked successfully, setting contract to ready...")
|
|
|
|
if err := s.ready(); err != nil {
|
|
return nil, fmt.Errorf("failed to call Ready: %w", err)
|
|
}
|
|
|
|
go func() {
|
|
until := time.Until(s.t1)
|
|
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case <-time.After(until + time.Second):
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
|
|
if !s.info.Status().IsOngoing() {
|
|
return
|
|
}
|
|
|
|
// XMRMaker hasn't claimed, and we're after t_1. let's call Refund
|
|
txhash, err := s.refund()
|
|
if err != nil {
|
|
log.Errorf("failed to refund: err=%s", err)
|
|
return
|
|
}
|
|
|
|
log.Infof("got our ETH back: tx hash=%s", txhash)
|
|
s.clearNextExpectedMessage(types.CompletedRefund) // TODO: duplicate?
|
|
|
|
// send NotifyRefund msg
|
|
if err = s.SendSwapMessage(&message.NotifyRefund{
|
|
TxHash: txhash.String(),
|
|
}, s.ID()); err != nil {
|
|
log.Errorf("failed to send refund message: err=%s", err)
|
|
}
|
|
|
|
_ = s.Exit()
|
|
case <-s.claimedCh:
|
|
return
|
|
}
|
|
}()
|
|
|
|
s.setNextExpectedMessage(&message.NotifyClaimed{})
|
|
return &message.NotifyReady{}, nil
|
|
}
|
|
|
|
// handleNotifyClaimed handles XMRMaker's reveal after he calls Claim().
|
|
// it calls `createMoneroWallet` to create XMRTaker's wallet, allowing her to own the XMR.
|
|
func (s *swapState) handleNotifyClaimed(txHash string) (mcrypto.Address, error) {
|
|
log.Debugf("got NotifyClaimed, txHash=%s", txHash)
|
|
receipt, err := s.WaitForReceipt(s.ctx, ethcommon.HexToHash(txHash))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed check claim transaction receipt: %w", err)
|
|
}
|
|
|
|
if len(receipt.Logs) == 0 {
|
|
return "", errClaimTxHasNoLogs
|
|
}
|
|
|
|
log.Infof("counterparty claimed ETH; tx hash=%s", txHash)
|
|
|
|
skB, err := swapfactory.GetSecretFromLog(receipt.Logs[0], "Claimed")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get secret from log: %w", err)
|
|
}
|
|
|
|
return s.claimMonero(skB)
|
|
}
|