mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
704 lines
20 KiB
Go
704 lines
20 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
// Package xmrtaker manages the swap state of individual swaps where the local swapd
|
|
// instance is offering Ethereum assets and accepting Monero in return.
|
|
package xmrtaker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/apd/v3"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
|
|
"github.com/athanorlabs/atomic-swap/coins"
|
|
"github.com/athanorlabs/atomic-swap/common"
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
|
"github.com/athanorlabs/atomic-swap/crypto/secp256k1"
|
|
"github.com/athanorlabs/atomic-swap/db"
|
|
"github.com/athanorlabs/atomic-swap/dleq"
|
|
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
|
"github.com/athanorlabs/atomic-swap/ethereum/watcher"
|
|
"github.com/athanorlabs/atomic-swap/monero"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
|
"github.com/athanorlabs/atomic-swap/protocol/backend"
|
|
pswap "github.com/athanorlabs/atomic-swap/protocol/swap"
|
|
"github.com/athanorlabs/atomic-swap/protocol/txsender"
|
|
|
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
const revertSwapCompleted = "swap is already completed"
|
|
|
|
var claimedTopic = common.GetTopic(common.ClaimedEventSignature)
|
|
|
|
// swapState is an instance of a swap. it holds the info needed for the swap,
|
|
// and its current state.
|
|
type swapState struct {
|
|
backend.Backend
|
|
sender txsender.Sender
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
noTransferBack bool
|
|
|
|
info *pswap.Info
|
|
providedAmount coins.EthAssetAmount
|
|
|
|
// our keys for this session
|
|
dleqProof *dleq.Proof
|
|
secp256k1Pub *secp256k1.PublicKey
|
|
privkeys *mcrypto.PrivateKeyPair
|
|
pubkeys *mcrypto.PublicKeyPair
|
|
|
|
// XMRMaker's keys for this session
|
|
xmrmakerPublicSpendKey *mcrypto.PublicKey
|
|
xmrmakerPrivateViewKey *mcrypto.PrivateViewKey
|
|
xmrmakerSecp256k1PublicKey *secp256k1.PublicKey
|
|
xmrmakerAddress ethcommon.Address
|
|
|
|
// block height at start of swap used for fast wallet creation
|
|
walletScanHeight uint64
|
|
|
|
// swap contract and timeouts in it; set once contract is deployed
|
|
contractSwapID [32]byte
|
|
contractSwap *contracts.SwapCreatorSwap
|
|
t0, t1 time.Time
|
|
|
|
// tracks the state of the swap
|
|
nextExpectedEvent EventType
|
|
// set to true once funds are locked
|
|
fundsLocked bool
|
|
|
|
// channels
|
|
|
|
// channel for swap events
|
|
// the event handler in event.go ensures only one event is being handled at a time
|
|
eventCh chan Event
|
|
// channel for `Claimed` logs seen on-chain
|
|
logClaimedCh chan ethtypes.Log
|
|
// signals the t0 expiration handler to return
|
|
xmrLockedCh chan struct{}
|
|
// signals the t1 expiration handler to return
|
|
claimedCh chan struct{}
|
|
// signals to the creator xmrmaker instance that it can delete this swap
|
|
done chan struct{}
|
|
}
|
|
|
|
func newSwapStateFromStart(
|
|
b backend.Backend,
|
|
makerPeerID peer.ID,
|
|
offerID types.Hash,
|
|
noTransferBack bool,
|
|
providedAmount coins.EthAssetAmount,
|
|
exchangeRate *coins.ExchangeRate,
|
|
ethAsset types.EthAsset,
|
|
) (*swapState, error) {
|
|
stage := types.ExpectingKeys
|
|
statusCh := make(chan types.Status, 16)
|
|
|
|
moneroStartNumber, err := b.XMRClient().GetHeight()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// reduce the scan height a little in case there is a block reorg
|
|
if moneroStartNumber >= monero.MinSpendConfirmations {
|
|
moneroStartNumber -= monero.MinSpendConfirmations
|
|
}
|
|
|
|
ethHeader, err := b.ETHClient().Raw().HeaderByNumber(b.Ctx(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expectedAmount, err := exchangeRate.ToXMR(providedAmount.AsStandard())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info := pswap.NewInfo(
|
|
makerPeerID,
|
|
offerID,
|
|
coins.ProvidesETH,
|
|
providedAmount.AsStandard(),
|
|
expectedAmount,
|
|
exchangeRate,
|
|
ethAsset,
|
|
stage,
|
|
moneroStartNumber,
|
|
statusCh,
|
|
)
|
|
if err = b.SwapManager().AddSwap(info); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s, err := newSwapState(
|
|
b,
|
|
noTransferBack,
|
|
info,
|
|
ethHeader.Number,
|
|
moneroStartNumber,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.generateAndSetKeys(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
statusCh <- stage
|
|
return s, nil
|
|
}
|
|
|
|
func newSwapStateFromOngoing(
|
|
b backend.Backend,
|
|
info *pswap.Info,
|
|
noTransferBack bool,
|
|
ethSwapInfo *db.EthereumSwapInfo,
|
|
sk *mcrypto.PrivateKeyPair,
|
|
) (*swapState, error) {
|
|
if info.Status != types.ETHLocked && info.Status != types.ContractReady {
|
|
return nil, errInvalidStageForRecovery
|
|
}
|
|
|
|
makerSk, makerVk, err := b.RecoveryDB().GetCounterpartySwapKeys(info.OfferID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get xmrmaker swap keys from db: %w", err)
|
|
}
|
|
|
|
s, err := newSwapState(
|
|
b,
|
|
noTransferBack,
|
|
info,
|
|
ethSwapInfo.StartNumber,
|
|
info.MoneroStartHeight,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if b.SwapCreatorAddr() != ethSwapInfo.SwapCreatorAddr {
|
|
return nil, errContractAddrMismatch(ethSwapInfo.SwapCreatorAddr.String())
|
|
}
|
|
|
|
s.setTimeouts(ethSwapInfo.Swap.Timeout0, ethSwapInfo.Swap.Timeout1)
|
|
s.privkeys = sk
|
|
s.pubkeys = sk.PublicKeyPair()
|
|
s.contractSwapID = ethSwapInfo.SwapID
|
|
s.contractSwap = ethSwapInfo.Swap
|
|
s.xmrmakerPublicSpendKey = makerSk
|
|
s.xmrmakerPrivateViewKey = makerVk
|
|
|
|
if info.Status == types.ETHLocked {
|
|
go s.checkForXMRLock()
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func newSwapState(
|
|
b backend.Backend,
|
|
noTransferBack bool,
|
|
info *pswap.Info,
|
|
ethStartNumber *big.Int,
|
|
moneroStartNumber uint64,
|
|
) (*swapState, error) {
|
|
// If the user specified `--external-signer=true` (no private eth key in the
|
|
// client) and explicitly set `--no-transfer-back`, we override their
|
|
// decision and set it back to `false`, because an external signer (UI) must
|
|
// be used, which will prompt the user to set their XMR address for funds to
|
|
// be transferred-back to.
|
|
if !b.ETHClient().HasPrivateKey() {
|
|
noTransferBack = false // front-end must set final deposit address
|
|
}
|
|
|
|
var sender txsender.Sender
|
|
if info.EthAsset.IsToken() {
|
|
erc20Contract, err := contracts.NewIERC20(info.EthAsset.Address(), b.ETHClient().Raw())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sender, err = b.NewTxSender(info.EthAsset.Address(), erc20Contract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
var err error
|
|
sender, err = b.NewTxSender(info.EthAsset.Address(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// set up ethereum event watchers
|
|
const logChSize = 16
|
|
logClaimedCh := make(chan ethtypes.Log, logChSize)
|
|
|
|
ctx, cancel := context.WithCancel(b.Ctx())
|
|
|
|
claimedWatcher := watcher.NewEventFilter(
|
|
ctx,
|
|
b.ETHClient().Raw(),
|
|
b.SwapCreatorAddr(),
|
|
ethStartNumber,
|
|
claimedTopic,
|
|
logClaimedCh,
|
|
)
|
|
|
|
err := claimedWatcher.Start()
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
|
|
var providedAmt coins.EthAssetAmount
|
|
if info.EthAsset.IsETH() {
|
|
providedAmt = coins.EtherToWei(info.ProvidedAmount)
|
|
} else {
|
|
tokenInfo, err := b.ETHClient().ERC20Info(b.Ctx(), info.EthAsset.Address())
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
providedAmt = coins.NewERC20TokenAmountFromDecimals(info.ProvidedAmount, tokenInfo)
|
|
}
|
|
|
|
// note: if this is recovering an ongoing swap, this will only
|
|
// be invoked if our status is ETHLocked or ContractReady; ie.
|
|
// we've locked ETH, but not yet claimed or refunded.
|
|
//
|
|
// dleqProof and secp256k1Pub are never set, as they are only used
|
|
// in the swap step before or where ETH is locked.
|
|
//
|
|
// similarly, xmrmaker secp256k1 public keys and ETH address are also
|
|
// never set, as they're only used in the ETH lock step.
|
|
s := &swapState{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
Backend: b,
|
|
sender: sender,
|
|
noTransferBack: noTransferBack,
|
|
walletScanHeight: moneroStartNumber,
|
|
nextExpectedEvent: nextExpectedEventFromStatus(info.Status),
|
|
eventCh: make(chan Event),
|
|
logClaimedCh: logClaimedCh,
|
|
xmrLockedCh: make(chan struct{}),
|
|
claimedCh: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
info: info,
|
|
providedAmount: providedAmt,
|
|
}
|
|
|
|
go s.runHandleEvents()
|
|
go s.runContractEventWatcher()
|
|
return s, nil
|
|
}
|
|
|
|
// SendKeysMessage ...
|
|
func (s *swapState) SendKeysMessage() common.Message {
|
|
return &message.SendKeysMessage{
|
|
PublicSpendKey: s.pubkeys.SpendKey(),
|
|
PrivateViewKey: s.privkeys.ViewKey(),
|
|
DLEqProof: s.dleqProof.Proof(),
|
|
Secp256k1PublicKey: s.secp256k1Pub,
|
|
}
|
|
}
|
|
|
|
// ExpectedAmount returns the amount received, or expected to be received, at the end of the swap
|
|
func (s *swapState) ExpectedAmount() *apd.Decimal {
|
|
return s.info.ExpectedAmount
|
|
}
|
|
|
|
func (s *swapState) expectedPiconeroAmount() *coins.PiconeroAmount {
|
|
return coins.MoneroToPiconero(s.info.ExpectedAmount)
|
|
}
|
|
|
|
// OfferID returns the Offer ID of the swap
|
|
func (s *swapState) OfferID() types.Hash {
|
|
return s.info.OfferID
|
|
}
|
|
|
|
// Exit is called by the network when the protocol stream closes, or if the swap_refund RPC endpoint is called.
|
|
// It exists the swap by refunding if necessary. If no locking has been done, it simply aborts the swap.
|
|
// If the swap already completed successfully, this function does not do anything regarding the protocol.
|
|
func (s *swapState) Exit() error {
|
|
event := newEventExit()
|
|
s.eventCh <- event
|
|
return <-event.errCh
|
|
}
|
|
|
|
// exit is the same as Exit, but assumes the calling code block already holds the swapState lock.
|
|
func (s *swapState) exit() error {
|
|
defer func() {
|
|
s.CloseProtocolStream(s.OfferID())
|
|
|
|
err := s.SwapManager().CompleteOngoingSwap(s.info)
|
|
if err != nil {
|
|
log.Warnf("failed to mark swap %s as completed: %s", s.info.OfferID, err)
|
|
return
|
|
}
|
|
|
|
err = s.Backend.RecoveryDB().DeleteSwap(s.OfferID())
|
|
if err != nil {
|
|
log.Warnf("failed to delete temporary swap info %s from db: %s", s.OfferID(), err)
|
|
}
|
|
|
|
// Stop all per-swap goroutines
|
|
s.cancel()
|
|
close(s.done)
|
|
|
|
var exitLog string
|
|
switch s.info.Status {
|
|
case types.CompletedSuccess:
|
|
exitLog = color.New(color.Bold).Sprintf("**swap completed successfully: offerID=%s**", s.OfferID())
|
|
case types.CompletedRefund:
|
|
exitLog = color.New(color.Bold).Sprintf("**swap refunded successfully: offerID=%s**", s.OfferID())
|
|
case types.CompletedAbort:
|
|
exitLog = color.New(color.Bold).Sprintf("**swap aborted: id=%s**", s.OfferID())
|
|
}
|
|
|
|
log.Info(exitLog)
|
|
}()
|
|
|
|
log.Debugf("attempting to exit swap: nextExpectedEvent=%s", s.nextExpectedEvent)
|
|
|
|
switch s.nextExpectedEvent {
|
|
case EventKeysReceivedType:
|
|
// we are fine, as we only just initiated the protocol.
|
|
s.clearNextExpectedEvent(types.CompletedAbort)
|
|
return nil
|
|
case EventXMRLockedType, EventETHClaimedType:
|
|
// for EventXMRLocked, we already locked our ETH-asset,
|
|
// so we should call Refund().
|
|
//
|
|
// for EventETHClaimed, the XMR has been locked, but the
|
|
// ETH hasn't been claimed, but the contract has been set to ready.
|
|
// we should also refund in this case, since we might be past t1.
|
|
receipt, err := s.tryRefund()
|
|
if err != nil {
|
|
if errors.Is(err, errRefundSwapCompleted) {
|
|
s.clearNextExpectedEvent(types.CompletedRefund)
|
|
log.Infof("swap was already refunded")
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(err.Error(), revertSwapCompleted) {
|
|
// note: this should NOT ever error; it could if the ethclient
|
|
// or monero clients crash during the course of the claim,
|
|
// but that would be very bad.
|
|
err = s.tryClaim()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to claim even though swap was completed on-chain: %w", err)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed to refund: %w", err)
|
|
}
|
|
|
|
s.clearNextExpectedEvent(types.CompletedRefund)
|
|
log.Infof("refunded ether: txID=%s", receipt.TxHash)
|
|
return nil
|
|
case EventNoneType:
|
|
// the swap completed already, do nothing
|
|
return nil
|
|
default:
|
|
log.Errorf("unexpected nextExpectedEvent: %s", s.nextExpectedEvent)
|
|
s.clearNextExpectedEvent(types.CompletedAbort)
|
|
return errUnexpectedEventType
|
|
}
|
|
}
|
|
|
|
func (s *swapState) tryRefund() (*ethtypes.Receipt, error) {
|
|
stage, err := s.SwapCreator().Swaps(s.ETHClient().CallOpts(s.ctx), s.contractSwapID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch stage {
|
|
case contracts.StageInvalid:
|
|
return nil, fmt.Errorf("%w: contract swap ID: %s", errRefundInvalid, s.contractSwapID)
|
|
case contracts.StageCompleted:
|
|
return nil, errRefundSwapCompleted
|
|
case contracts.StagePending, contracts.StageReady:
|
|
// do nothing
|
|
default:
|
|
panic("Unhandled stage value")
|
|
}
|
|
|
|
isReady := stage == contracts.StageReady
|
|
|
|
ts, err := s.ETHClient().LatestBlockTimestamp(s.ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debugf("tryRefund isReady=%v untilT0=%vs untilT1=%vs",
|
|
isReady, s.t0.Sub(ts).Seconds(), s.t1.Sub(ts).Seconds())
|
|
|
|
if ts.Before(s.t0) && !isReady {
|
|
receipt, err := s.refund() //nolint:govet
|
|
// TODO: Have refund() return errors that we can use errors.Is to check against
|
|
if err == nil {
|
|
return receipt, nil
|
|
}
|
|
|
|
// There is a small, but non-zero chance that our transaction gets placed in a block that is after T0
|
|
// even though the current block is before T0. In this case, the transaction will be reverted, the
|
|
// gas fee is lost, but we can wait until T1 and try again.
|
|
log.Warnf("first refund attempt failed: err=%s", err)
|
|
}
|
|
|
|
if ts.After(s.t1) {
|
|
return s.refund()
|
|
}
|
|
|
|
// the contract is "ready", so we can't do anything until
|
|
// the counterparty claims or until t1 passes.
|
|
//
|
|
// we let the runT1ExpirationHandler() routine continue to run and read
|
|
// from s.eventCh for EventShouldRefund or EventETHClaimed.
|
|
// (since this function is called from inside the event handler routine,
|
|
// it won't handle those events while this function is executing.)
|
|
log.Infof("waiting until time %s to refund", s.t1)
|
|
|
|
waitCtx, waitCtxCancel := context.WithCancel(s.ctx)
|
|
defer waitCtxCancel()
|
|
|
|
waitCh := make(chan error)
|
|
go func() {
|
|
waitCh <- s.ETHClient().WaitForTimestamp(waitCtx, s.t1)
|
|
close(waitCh)
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case event := <-s.eventCh:
|
|
log.Debugf("got event %s while waiting for T1", event.Type())
|
|
switch event.(type) {
|
|
case *EventShouldRefund:
|
|
return s.refund()
|
|
case *EventETHClaimed:
|
|
// we should claim; returning this error
|
|
// causes the calling function to claim
|
|
return nil, fmt.Errorf(revertSwapCompleted)
|
|
case *EventExit:
|
|
// do nothing, we're already exiting
|
|
default:
|
|
panic(fmt.Sprintf("got unexpected event while waiting for Claimed/T1: %s", event.Type()))
|
|
}
|
|
case err = <-waitCh:
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to wait for T1: %w", err)
|
|
}
|
|
|
|
return s.refund()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *swapState) setTimeouts(t0, t1 *big.Int) {
|
|
s.t0 = time.Unix(t0.Int64(), 0)
|
|
s.t1 = time.Unix(t1.Int64(), 0)
|
|
s.info.Timeout0 = &s.t0
|
|
s.info.Timeout1 = &s.t1
|
|
}
|
|
|
|
func (s *swapState) generateAndSetKeys() error {
|
|
if s.privkeys != nil {
|
|
panic("generateAndSetKeys should only be called once")
|
|
}
|
|
|
|
keysAndProof, err := generateKeys()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.dleqProof = keysAndProof.DLEqProof
|
|
s.secp256k1Pub = keysAndProof.Secp256k1PublicKey
|
|
s.privkeys = keysAndProof.PrivateKeyPair
|
|
s.pubkeys = keysAndProof.PublicKeyPair
|
|
|
|
return s.Backend.RecoveryDB().PutSwapPrivateKey(s.OfferID(), s.privkeys.SpendKey())
|
|
}
|
|
|
|
// getSecret secrets returns the current secret scalar used to unlock funds from the contract.
|
|
func (s *swapState) getSecret() [32]byte {
|
|
secret := s.dleqProof.Secret()
|
|
var sc [32]byte
|
|
copy(sc[:], secret[:])
|
|
return sc
|
|
}
|
|
|
|
// setXMRMakerKeys sets XMRMaker's public spend key (to be stored in the contract) and XMRMaker's
|
|
// private view key (used to check XMR balance before calling Ready())
|
|
func (s *swapState) setXMRMakerKeys(
|
|
sk *mcrypto.PublicKey,
|
|
vk *mcrypto.PrivateViewKey,
|
|
secp256k1Pub *secp256k1.PublicKey,
|
|
) error {
|
|
s.xmrmakerPublicSpendKey = sk
|
|
s.xmrmakerPrivateViewKey = vk
|
|
s.xmrmakerSecp256k1PublicKey = secp256k1Pub
|
|
return s.Backend.RecoveryDB().PutCounterpartySwapKeys(s.info.OfferID, sk, vk)
|
|
}
|
|
|
|
// lockAsset calls the Swap contract function new_swap and locks `amount` ether in it.
|
|
func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
|
|
if s.xmrmakerPublicSpendKey == nil || s.xmrmakerPrivateViewKey == nil {
|
|
panic(errCounterpartyKeysNotSet)
|
|
}
|
|
|
|
cmtXMRTaker := s.secp256k1Pub.Keccak256()
|
|
cmtXMRMaker := s.xmrmakerSecp256k1PublicKey.Keccak256()
|
|
providedAmt := s.providedAmount
|
|
|
|
log.Debugf("locking %s %s in contract", providedAmt.AsStandard(), providedAmt.StandardSymbol())
|
|
|
|
nonce := contracts.GenerateNewSwapNonce()
|
|
receipt, err := s.sender.NewSwap(
|
|
cmtXMRMaker,
|
|
cmtXMRTaker,
|
|
s.xmrmakerAddress,
|
|
big.NewInt(int64(s.SwapTimeout().Seconds())),
|
|
nonce,
|
|
providedAmt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to instantiate swap on-chain: %w", err)
|
|
}
|
|
|
|
log.Infof("instantiated swap on-chain: amount=%s asset=%s %s",
|
|
s.providedAmount, s.info.EthAsset, common.ReceiptInfo(receipt))
|
|
|
|
if len(receipt.Logs) == 0 {
|
|
return nil, errSwapInstantiationNoLogs
|
|
}
|
|
|
|
for _, rLog := range receipt.Logs {
|
|
s.contractSwapID, err = contracts.GetIDFromLog(rLog)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("swap ID not found in transaction receipt's logs: %w", err)
|
|
}
|
|
|
|
var t0 *big.Int
|
|
var t1 *big.Int
|
|
for _, log := range receipt.Logs {
|
|
t0, t1, err = contracts.GetTimeoutsFromLog(log)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("timeouts not found in transaction receipt's logs: %w", err)
|
|
}
|
|
|
|
s.fundsLocked = true
|
|
s.setTimeouts(t0, t1)
|
|
|
|
s.contractSwap = &contracts.SwapCreatorSwap{
|
|
Owner: s.ETHClient().Address(),
|
|
Claimer: s.xmrmakerAddress,
|
|
PubKeyClaim: cmtXMRMaker,
|
|
PubKeyRefund: cmtXMRTaker,
|
|
Timeout0: t0,
|
|
Timeout1: t1,
|
|
Asset: ethcommon.Address(s.info.EthAsset),
|
|
Value: s.providedAmount.BigInt(),
|
|
Nonce: nonce,
|
|
}
|
|
|
|
ethInfo := &db.EthereumSwapInfo{
|
|
StartNumber: receipt.BlockNumber,
|
|
SwapID: s.contractSwapID,
|
|
Swap: s.contractSwap,
|
|
SwapCreatorAddr: s.Backend.SwapCreatorAddr(),
|
|
}
|
|
|
|
if err := s.Backend.RecoveryDB().PutContractSwapInfo(s.OfferID(), ethInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof("locked %s in swap contract, waiting for XMR to be locked", providedAmt.StandardSymbol())
|
|
return receipt, nil
|
|
}
|
|
|
|
// ready calls the Ready() method on the Swap contract, indicating to XMRMaker he has until time t_1 to
|
|
// call Claim(). Ready() should only be called once XMRTaker sees XMRMaker lock his XMR.
|
|
// If time t_0 has passed, there is no point of calling Ready().
|
|
func (s *swapState) ready() error {
|
|
stage, err := s.SwapCreator().Swaps(s.ETHClient().CallOpts(s.ctx), s.contractSwapID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if stage != contracts.StagePending {
|
|
if stage == contracts.StageReady {
|
|
log.Warnf("contract already set to ready, ignoring call to ready()")
|
|
return nil
|
|
}
|
|
|
|
if stage == contracts.StageCompleted {
|
|
log.Infof("contract aleady set to completed, ignoring call to ready() and sending EventExit")
|
|
go func() {
|
|
err = s.Exit()
|
|
if err != nil {
|
|
log.Errorf("failed to handle EventExit: %s", err)
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("cannot set contract to ready when swap stage is %s", contracts.StageToString(stage))
|
|
}
|
|
|
|
receipt, err := s.sender.SetReady(s.contractSwap)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), revertSwapCompleted) && !s.info.Status.IsOngoing() {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
log.Infof("contract set to ready %s", common.ReceiptInfo(receipt))
|
|
|
|
return nil
|
|
}
|
|
|
|
// refund calls the Refund() method in the Swap contract, revealing XMRTaker's secret
|
|
// and returns to her the ether in the contract.
|
|
// If time t_1 passes and Claim() has not been called, XMRTaker should call Refund().
|
|
func (s *swapState) refund() (*ethtypes.Receipt, error) {
|
|
sc := s.getSecret()
|
|
|
|
log.Infof("attempting to call Refund()...")
|
|
receipt, err := s.sender.Refund(s.contractSwap, sc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof("refund succeeded %s", common.ReceiptInfo(receipt))
|
|
|
|
s.clearNextExpectedEvent(types.CompletedRefund)
|
|
return receipt, nil
|
|
}
|
|
|
|
// generateKeys generates XMRTaker's monero spend and view keys (S_b, V_b), a secp256k1 public key,
|
|
// and a DLEq proof proving that the two keys correspond.
|
|
func generateKeys() (*pcommon.KeysAndProof, error) {
|
|
return pcommon.GenerateKeysAndProof()
|
|
}
|