mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-09 14:18:03 -05:00
290 lines
7.6 KiB
Go
290 lines
7.6 KiB
Go
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
package xmrmaker
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/athanorlabs/atomic-swap/common/types"
|
|
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
|
"github.com/athanorlabs/atomic-swap/net/message"
|
|
)
|
|
|
|
// EventType represents an event that occurs which moves the swap
|
|
// "state machine" to its next state.
|
|
type EventType byte
|
|
|
|
const (
|
|
// EventETHLockedType is triggered when the taker notifies us that the ETH
|
|
// is locked in the smart contract. Upon verification, it causes us to lock
|
|
// our XMR. After this event, the other possible events are
|
|
// EventContractReadyType (success), EventETHRefundedType (abort), or
|
|
// EventExitType (abort).
|
|
EventETHLockedType EventType = iota
|
|
|
|
// EventContractReadyType is triggered when the taker sets the contract to
|
|
// "ready" or timeout1 is reached. When this event occurs, we can claim ETH
|
|
// from the contract. After this event, the other possible events are
|
|
// EventETHRefundedType (which would only happen if we go offline until
|
|
// timeout2, causing us to refund), or EventExitType (refund).
|
|
EventContractReadyType
|
|
|
|
// EventETHRefundedType is triggered when the taker refunds the
|
|
// contract-locked ETH back to themselves. It causes use to try to refund
|
|
// our XMR. After this event, the only possible event is EventExitType.
|
|
EventETHRefundedType
|
|
|
|
// EventExitType is triggered by the protocol "exiting", which may happen
|
|
// via a swap cancellation via the RPC endpoint, or from the counterparty
|
|
// disconnecting from us on the p2p network. It causes us to attempt to
|
|
// gracefully exit from the swap, which leads to an abort, refund, or claim,
|
|
// depending on the state we're currently in. No other events can occur
|
|
// after this.
|
|
EventExitType
|
|
|
|
// EventNoneType is set as the "nextExpectedEvent" once the swap has exited.
|
|
// It does not trigger any action. No other events can occur after this.
|
|
EventNoneType
|
|
)
|
|
|
|
// nextExpectedEventFromStatus returns the next expected event given the current
|
|
// swap status.
|
|
func nextExpectedEventFromStatus(s types.Status) EventType {
|
|
switch s {
|
|
case types.ExpectingKeys, types.KeysExchanged:
|
|
return EventETHLockedType
|
|
case types.XMRLocked:
|
|
return EventContractReadyType
|
|
default:
|
|
return EventExitType
|
|
}
|
|
}
|
|
|
|
func (t EventType) String() string {
|
|
switch t {
|
|
case EventETHLockedType:
|
|
return "EventETHLockedType"
|
|
case EventContractReadyType:
|
|
return "EventContractReadyType"
|
|
case EventETHRefundedType:
|
|
return "EventETHRefundedType"
|
|
case EventExitType:
|
|
return "EventExitType"
|
|
case EventNoneType:
|
|
return "EventNoneType"
|
|
default:
|
|
panic("invalid EventType")
|
|
}
|
|
}
|
|
|
|
// getStatus returns the status corresponding to the next expected event.
|
|
func (t EventType) getStatus() types.Status {
|
|
switch t {
|
|
case EventETHLockedType:
|
|
return types.KeysExchanged
|
|
case EventContractReadyType:
|
|
return types.XMRLocked
|
|
default:
|
|
// the only possible nextExpectedEvents are EventETHLockedType
|
|
// and EventContractReadyType, so this case shouldn't be hit.
|
|
return types.UnknownStatus
|
|
}
|
|
}
|
|
|
|
// Event represents a swap state event.
|
|
type Event interface {
|
|
Type() EventType
|
|
}
|
|
|
|
// EventETHLocked is the first expected event. It represents ETH being locked
|
|
// on-chain.
|
|
type EventETHLocked struct {
|
|
message *message.NotifyETHLocked
|
|
errCh chan error
|
|
}
|
|
|
|
// Type ...
|
|
func (*EventETHLocked) Type() EventType {
|
|
return EventETHLockedType
|
|
}
|
|
|
|
func newEventETHLocked(msg *message.NotifyETHLocked) *EventETHLocked {
|
|
return &EventETHLocked{
|
|
message: msg,
|
|
errCh: make(chan error),
|
|
}
|
|
}
|
|
|
|
// EventContractReady is the second expected event. It represents the contract being
|
|
// ready for us to claim the ETH.
|
|
type EventContractReady struct {
|
|
errCh chan error
|
|
}
|
|
|
|
// Type ...
|
|
func (*EventContractReady) Type() EventType {
|
|
return EventContractReadyType
|
|
}
|
|
|
|
func newEventContractReady() *EventContractReady {
|
|
return &EventContractReady{
|
|
errCh: make(chan error),
|
|
}
|
|
}
|
|
|
|
// EventETHRefunded is an optional event. It represents the ETH being refunded back
|
|
// to the counterparty, and thus we also must refund.
|
|
type EventETHRefunded struct {
|
|
sk *mcrypto.PrivateSpendKey
|
|
errCh chan error
|
|
}
|
|
|
|
// Type ...
|
|
func (*EventETHRefunded) Type() EventType {
|
|
return EventETHRefundedType
|
|
}
|
|
|
|
func newEventETHRefunded(sk *mcrypto.PrivateSpendKey) *EventETHRefunded {
|
|
return &EventETHRefunded{
|
|
sk: sk,
|
|
errCh: make(chan error),
|
|
}
|
|
}
|
|
|
|
// EventExit is an optional event. It is sent when the protocol should be stopped,
|
|
// for example if the remote peer closes their connection with us before sending all
|
|
// required messages, or we decide to cancel the swap.
|
|
type EventExit struct {
|
|
errCh chan error
|
|
}
|
|
|
|
// Type ...
|
|
func (*EventExit) Type() EventType {
|
|
return EventExitType
|
|
}
|
|
|
|
func newEventExit() *EventExit {
|
|
return &EventExit{
|
|
errCh: make(chan error),
|
|
}
|
|
}
|
|
|
|
func (s *swapState) runHandleEvents() {
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case event := <-s.eventCh:
|
|
s.handleEvent(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *swapState) handleEvent(event Event) {
|
|
// events are only used once, so their error channel can be closed after handling.
|
|
switch e := event.(type) {
|
|
case *EventETHLocked:
|
|
log.Infof("EventETHLocked")
|
|
defer close(e.errCh)
|
|
|
|
if s.nextExpectedEvent != EventETHLockedType {
|
|
e.errCh <- fmt.Errorf("nextExpectedEvent was %s, not %s", s.nextExpectedEvent, e.Type())
|
|
return
|
|
}
|
|
|
|
err := s.handleNotifyETHLocked(e.message)
|
|
if err != nil {
|
|
e.errCh <- fmt.Errorf("failed to handle EventETHLocked: %w", err)
|
|
err = s.exit()
|
|
if err != nil {
|
|
log.Warnf("failed to exit swap: %s", err)
|
|
}
|
|
}
|
|
|
|
// close the stream to the remote peer, since we won't be
|
|
// receiving any more messages.
|
|
s.Backend.CloseProtocolStream(s.OfferID())
|
|
|
|
// nextExpectedEvent was set in s.lockFunds()
|
|
case *EventContractReady:
|
|
log.Infof("EventContractReady")
|
|
defer close(e.errCh)
|
|
|
|
if s.nextExpectedEvent != EventContractReadyType {
|
|
e.errCh <- fmt.Errorf("nextExpectedEvent was %s, not %s", s.nextExpectedEvent, e.Type())
|
|
return
|
|
}
|
|
|
|
err := s.handleEventContractReady()
|
|
if err != nil {
|
|
e.errCh <- fmt.Errorf("failed to handle EventContractReady: %w", err)
|
|
return
|
|
}
|
|
|
|
err = s.exit()
|
|
if err != nil {
|
|
log.Warnf("failed to exit swap: %s", err)
|
|
}
|
|
case *EventETHRefunded:
|
|
log.Infof("EventETHRefunded")
|
|
defer close(e.errCh)
|
|
|
|
err := s.handleEventETHRefunded(e)
|
|
if err != nil {
|
|
e.errCh <- fmt.Errorf("failed to handle EventETHRefunded: %w", err)
|
|
return
|
|
}
|
|
|
|
err = s.exit()
|
|
if err != nil {
|
|
log.Warnf("failed to exit swap: %s", err)
|
|
}
|
|
case *EventExit:
|
|
// this can happen at any stage.
|
|
log.Infof("EventExit")
|
|
defer close(e.errCh)
|
|
|
|
err := s.exit()
|
|
if err != nil {
|
|
e.errCh <- fmt.Errorf("failed to handle EventExit: %w", err)
|
|
}
|
|
default:
|
|
panic("unhandled event type")
|
|
}
|
|
}
|
|
|
|
func (s *swapState) handleEventContractReady() error {
|
|
log.Debug("contract ready, attempting to claim funds...")
|
|
close(s.readyCh)
|
|
s.readyWatcher.Stop()
|
|
|
|
// contract ready, let's claim our ether
|
|
receipt, err := s.claimFunds()
|
|
if err != nil {
|
|
log.Warnf("failed to claim funds from contract, attempting to safely exit: %s", err)
|
|
|
|
// TODO: retry claim, depending on error (#162)
|
|
if err2 := s.exit(); err2 != nil {
|
|
return fmt.Errorf("failed to exit after failing to claim: %w", err2)
|
|
}
|
|
|
|
return fmt.Errorf("failed to claim: %w", err)
|
|
}
|
|
|
|
log.Debugf("funds claimed, tx: %s", receipt.TxHash)
|
|
s.clearNextExpectedEvent(types.CompletedSuccess)
|
|
return nil
|
|
}
|
|
|
|
func (s *swapState) handleEventETHRefunded(e *EventETHRefunded) error {
|
|
// generate monero wallet, regaining control over locked funds
|
|
err := s.reclaimMonero(e.sk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.clearNextExpectedEvent(types.CompletedRefund)
|
|
return nil
|
|
}
|