mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-10 06:38:04 -05:00
utilize t0 and t1 logic in protocol code for Alice and Bob (#30)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -39,4 +39,6 @@ Cargo.lock
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,rust
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,rust
|
||||
|
||||
*.key
|
||||
155
alice/net.go
155
alice/net.go
@@ -2,9 +2,7 @@ package alice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
"github.com/noot/atomic-swap/net"
|
||||
)
|
||||
|
||||
@@ -12,29 +10,42 @@ func (a *alice) Provides() net.ProvidesCoin {
|
||||
return net.ProvidesETH
|
||||
}
|
||||
|
||||
func (a *alice) SendKeysMessage() (*net.SendKeysMessage, error) {
|
||||
kp, err := a.generateKeys()
|
||||
if err != nil {
|
||||
// InitiateProtocol is called when an RPC call is made from the user to initiate a swap.
|
||||
func (a *alice) InitiateProtocol(providesAmount, desiredAmount uint64) (net.SwapState, error) {
|
||||
if err := a.initiate(providesAmount, desiredAmount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &net.SendKeysMessage{
|
||||
PublicSpendKey: kp.SpendKey().Hex(),
|
||||
PublicViewKey: kp.ViewKey().Hex(),
|
||||
}, nil
|
||||
return a.swapState, nil
|
||||
}
|
||||
|
||||
func (a *alice) InitiateProtocol(providesAmount, desiredAmount uint64) error {
|
||||
if err := a.initiate(providesAmount, desiredAmount); err != nil {
|
||||
return err
|
||||
// HandleInitiateMessage is called when we receive a network message from a peer that they wish to initiate a swap.
|
||||
func (a *alice) HandleInitiateMessage(msg *net.InitiateMessage) (net.SwapState, net.Message, error) {
|
||||
if msg.Provides != net.ProvidesXMR {
|
||||
return nil, nil, errors.New("peer does not provide XMR")
|
||||
}
|
||||
|
||||
a.setNextExpectedMessage(&net.SendKeysMessage{})
|
||||
return nil
|
||||
// TODO: notify the user via the CLI/websockets that someone wishes to initiate a swap with them.
|
||||
|
||||
// the other party initiated, saying what they will provide and what they desire.
|
||||
// we initiate our protocol, saying we will provide what they desire and vice versa.
|
||||
if err := a.initiate(msg.DesiredAmount, msg.ProvidesAmount); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := a.swapState.handleSendKeysMessage(msg.SendKeysMessage)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return a.swapState, resp, nil
|
||||
}
|
||||
|
||||
func (a *alice) initiate(providesAmount, desiredAmount uint64) error {
|
||||
if a.initiated {
|
||||
a.swapMu.Lock()
|
||||
defer a.swapMu.Unlock()
|
||||
|
||||
if a.swapState != nil {
|
||||
return errors.New("protocol already in progress")
|
||||
}
|
||||
|
||||
@@ -44,120 +55,10 @@ func (a *alice) initiate(providesAmount, desiredAmount uint64) error {
|
||||
}
|
||||
|
||||
// check user's balance and that they actualy have what they will provide
|
||||
if balance.Uint64() <= a.providesAmount {
|
||||
if balance.Uint64() <= providesAmount {
|
||||
return errors.New("balance lower than amount to be provided")
|
||||
}
|
||||
|
||||
a.initiated = true
|
||||
a.providesAmount = providesAmount
|
||||
a.desiredAmount = desiredAmount
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *alice) ProtocolComplete() {
|
||||
a.initiated = false
|
||||
a.setNextExpectedMessage(&net.InitiateMessage{})
|
||||
}
|
||||
|
||||
func (a *alice) HandleProtocolMessage(msg net.Message) (net.Message, bool, error) {
|
||||
if err := a.checkMessageType(msg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *net.InitiateMessage:
|
||||
if msg.Provides != net.ProvidesXMR {
|
||||
return nil, true, errors.New("peer does not provide XMR")
|
||||
}
|
||||
|
||||
// TODO: notify the user via the CLI/websockets that someone wishes to initiate a swap with them.
|
||||
|
||||
// the other party initiated, saying what they will provide and what they desire.
|
||||
// we initiate our protocol, saying we will provide what they desire and vice versa.
|
||||
if err := a.initiate(msg.DesiredAmount, msg.ProvidesAmount); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
resp, err := a.handleSendKeysMessage(msg.SendKeysMessage)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
return resp, false, nil
|
||||
case *net.SendKeysMessage:
|
||||
resp, err := a.handleSendKeysMessage(msg)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
return resp, false, nil
|
||||
case *net.NotifyXMRLock:
|
||||
if msg.Address == "" {
|
||||
return nil, true, errors.New("got empty address for locked XMR")
|
||||
}
|
||||
|
||||
// check that XMR was locked in expected account, and confirm amount
|
||||
a.setNextExpectedMessage(&net.NotifyClaimed{})
|
||||
|
||||
if err := a.ready(); err != nil {
|
||||
return nil, true, fmt.Errorf("failed to call Ready: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("set swap.IsReady == true")
|
||||
|
||||
out := &net.NotifyReady{}
|
||||
return out, false, nil
|
||||
case *net.NotifyClaimed:
|
||||
address, err := a.handleNotifyClaimed(msg.TxHash)
|
||||
if err != nil {
|
||||
log.Error("failed to create monero address: err=", err)
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
log.Info("successfully created monero wallet from our secrets: address=", address)
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, errors.New("unexpected message type")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alice) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message, error) {
|
||||
if msg.PublicSpendKey == "" || msg.PrivateViewKey == "" {
|
||||
return nil, errors.New("did not receive Bob's public spend or private view key")
|
||||
}
|
||||
|
||||
log.Debug("got Bob's keys")
|
||||
a.setNextExpectedMessage(&net.NotifyXMRLock{})
|
||||
|
||||
sk, err := monero.NewPublicKeyFromHex(msg.PublicSpendKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Bob's public spend key: %w", err)
|
||||
}
|
||||
|
||||
vk, err := monero.NewPrivateViewKeyFromHex(msg.PrivateViewKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Bob's private view keys: %w", err)
|
||||
}
|
||||
|
||||
a.setBobKeys(sk, vk)
|
||||
address, err := a.deployAndLockETH(a.providesAmount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deploy contract: %w", err)
|
||||
}
|
||||
|
||||
log.Info("deployed Swap contract, waiting for XMR to be locked: address=", address)
|
||||
|
||||
out := &net.NotifyContractDeployed{
|
||||
Address: address.String(),
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *alice) checkMessageType(msg net.Message) error {
|
||||
if msg.Type() != a.nextExpectedMessage.Type() {
|
||||
return errors.New("received unexpected message")
|
||||
}
|
||||
|
||||
a.swapState = newSwapState(a, providesAmount, desiredAmount)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
@@ -23,37 +26,27 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.Logger("alice")
|
||||
log = logging.Logger("alice")
|
||||
defaultTimeoutDuration = big.NewInt(60 * 60 * 24) // 1 day = 60s * 60min * 24hr
|
||||
)
|
||||
|
||||
func reverse(s []byte) []byte {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// alice implements the functions that will be called by a user who owns ETH
|
||||
// and wishes to swap for XMR.
|
||||
type alice struct {
|
||||
ctx context.Context
|
||||
t0, t1 time.Time //nolint
|
||||
ctx context.Context
|
||||
|
||||
privkeys *monero.PrivateKeyPair
|
||||
pubkeys *monero.PublicKeyPair
|
||||
bobSpendKey *monero.PublicKey
|
||||
bobViewKey *monero.PrivateViewKey
|
||||
client monero.Client
|
||||
client monero.Client
|
||||
|
||||
contract *swap.Swap
|
||||
ethPrivKey *ecdsa.PrivateKey
|
||||
ethClient *ethclient.Client
|
||||
auth *bind.TransactOpts
|
||||
callOpts *bind.CallOpts
|
||||
|
||||
nextExpectedMessage net.Message
|
||||
net net.MessageSender
|
||||
|
||||
initiated bool
|
||||
providesAmount, desiredAmount uint64
|
||||
// non-nil if a swap is currently happening, nil otherwise
|
||||
swapMu sync.Mutex
|
||||
swapState *swapState
|
||||
}
|
||||
|
||||
// NewAlice returns a new instance of Alice.
|
||||
@@ -75,96 +68,111 @@ func NewAlice(ctx context.Context, moneroEndpoint, ethEndpoint, ethPrivKey strin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pub := pk.Public().(*ecdsa.PublicKey)
|
||||
|
||||
// TODO: check that Alice's monero-wallet-cli endpoint has wallet-dir configured
|
||||
|
||||
return &alice{
|
||||
ctx: ctx,
|
||||
ethPrivKey: pk,
|
||||
ethClient: ec,
|
||||
client: monero.NewClient(moneroEndpoint),
|
||||
auth: auth,
|
||||
nextExpectedMessage: &net.InitiateMessage{},
|
||||
ctx: ctx,
|
||||
ethPrivKey: pk,
|
||||
ethClient: ec,
|
||||
client: monero.NewClient(moneroEndpoint),
|
||||
auth: auth,
|
||||
callOpts: &bind.CallOpts{
|
||||
From: crypto.PubkeyToAddress(*pub),
|
||||
Context: ctx,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *alice) setNextExpectedMessage(msg net.Message) {
|
||||
a.nextExpectedMessage = msg
|
||||
func (a *alice) SetMessageSender(n net.MessageSender) {
|
||||
a.net = n
|
||||
}
|
||||
|
||||
// generateKeys generates Alice's monero spend and view keys (S_b, V_b)
|
||||
// It returns Alice's public spend key
|
||||
func (a *alice) generateKeys() (*monero.PublicKeyPair, error) {
|
||||
if a.privkeys != nil {
|
||||
return a.pubkeys, nil
|
||||
func (s *swapState) generateKeys() (*monero.PublicKeyPair, error) {
|
||||
if s.privkeys != nil {
|
||||
return s.pubkeys, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
a.privkeys, err = monero.GenerateKeys()
|
||||
s.privkeys, err = monero.GenerateKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: configure basepath
|
||||
if err := common.WriteKeysToFile("./alice-xmr", a.privkeys); err != nil {
|
||||
// TODO: add swap ID
|
||||
if err := common.WriteKeysToFile("/tmp/alice-xmr", s.privkeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.pubkeys = a.privkeys.PublicKeyPair()
|
||||
return a.pubkeys, nil
|
||||
s.pubkeys = s.privkeys.PublicKeyPair()
|
||||
return s.pubkeys, nil
|
||||
}
|
||||
|
||||
// setBobKeys sets Bob's public spend key (to be stored in the contract) and Bob's
|
||||
// private view key (used to check XMR balance before calling Ready())
|
||||
func (a *alice) setBobKeys(sk *monero.PublicKey, vk *monero.PrivateViewKey) {
|
||||
a.bobSpendKey = sk
|
||||
a.bobViewKey = vk
|
||||
func (s *swapState) setBobKeys(sk *monero.PublicKey, vk *monero.PrivateViewKey) {
|
||||
s.bobSpendKey = sk
|
||||
s.bobViewKey = vk
|
||||
}
|
||||
|
||||
// deployAndLockETH deploys an instance of the Swap contract and locks `amount` ether in it.
|
||||
func (a *alice) deployAndLockETH(amount uint64) (ethcommon.Address, error) {
|
||||
pkAlice := reverse(a.pubkeys.SpendKey().Bytes())
|
||||
pkBob := reverse(a.bobSpendKey.Bytes())
|
||||
func (s *swapState) deployAndLockETH(amount uint64) (ethcommon.Address, error) {
|
||||
if s.pubkeys == nil {
|
||||
return ethcommon.Address{}, errors.New("public keys aren't set")
|
||||
}
|
||||
|
||||
if s.bobSpendKey == nil {
|
||||
return ethcommon.Address{}, errors.New("bob's keys aren't set")
|
||||
}
|
||||
|
||||
pkAlice := s.pubkeys.SpendKey().Bytes()
|
||||
pkBob := s.bobSpendKey.Bytes()
|
||||
|
||||
var pka, pkb [32]byte
|
||||
copy(pka[:], reverse(pkAlice))
|
||||
copy(pkb[:], reverse(pkBob))
|
||||
copy(pka[:], pkAlice)
|
||||
copy(pkb[:], pkBob)
|
||||
|
||||
log.Debug("locking amount: ", amount)
|
||||
a.auth.Value = big.NewInt(int64(amount))
|
||||
// TODO: put auth in swapState
|
||||
s.alice.auth.Value = big.NewInt(int64(amount))
|
||||
defer func() {
|
||||
a.auth.Value = nil
|
||||
s.alice.auth.Value = nil
|
||||
}()
|
||||
|
||||
address, _, swap, err := swap.DeploySwap(a.auth, a.ethClient, pka, pkb)
|
||||
address, _, swap, err := swap.DeploySwap(s.alice.auth, s.alice.ethClient, pkb, pka, defaultTimeoutDuration)
|
||||
if err != nil {
|
||||
return ethcommon.Address{}, err
|
||||
}
|
||||
|
||||
balance, err := a.ethClient.BalanceAt(a.ctx, address, nil)
|
||||
balance, err := s.alice.ethClient.BalanceAt(s.ctx, address, nil)
|
||||
if err != nil {
|
||||
return ethcommon.Address{}, err
|
||||
}
|
||||
|
||||
log.Debug("contract balance: ", balance)
|
||||
|
||||
a.contract = swap
|
||||
s.contract = swap
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// ready calls the Ready() method on the Swap contract, indicating to Bob he has until time t_1 to
|
||||
// call Claim(). Ready() should only be called once Alice sees Bob lock his XMR.
|
||||
// If time t_0 has passed, there is no point of calling Ready().
|
||||
func (a *alice) ready() error {
|
||||
_, err := a.contract.SetReady(a.auth)
|
||||
func (s *swapState) ready() error {
|
||||
_, err := s.contract.SetReady(s.alice.auth)
|
||||
return err
|
||||
}
|
||||
|
||||
// watchForClaim watches for Bob to call Claim() on the Swap contract.
|
||||
// When Claim() is called, revealing Bob's secret s_b, the secret key corresponding
|
||||
// to (s_a + s_b) will be sent over this channel, allowing Alice to claim the XMR it contains.
|
||||
func (a *alice) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //nolint:unused
|
||||
func (s *swapState) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //nolint:unused
|
||||
watchOpts := &bind.WatchOpts{
|
||||
Context: a.ctx,
|
||||
Context: s.ctx,
|
||||
}
|
||||
|
||||
out := make(chan *monero.PrivateKeyPair)
|
||||
@@ -172,7 +180,7 @@ func (a *alice) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //nolin
|
||||
defer close(out)
|
||||
|
||||
// watch for Refund() event on chain, calculate unlock key as result
|
||||
sub, err := a.contract.WatchClaimed(watchOpts, ch)
|
||||
sub, err := s.contract.WatchClaimed(watchOpts, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,13 +213,13 @@ func (a *alice) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //nolin
|
||||
return
|
||||
}
|
||||
|
||||
skAB := monero.SumPrivateSpendKeys(skB, a.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(vkA, a.privkeys.ViewKey())
|
||||
skAB := monero.SumPrivateSpendKeys(skB, s.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(vkA, s.privkeys.ViewKey())
|
||||
kpAB := monero.NewPrivateKeyPair(skAB, vkAB)
|
||||
|
||||
out <- kpAB
|
||||
return
|
||||
case <-a.ctx.Done():
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -223,40 +231,48 @@ func (a *alice) watchForClaim() (<-chan *monero.PrivateKeyPair, error) { //nolin
|
||||
// refund calls the Refund() method in the Swap contract, revealing Alice's secret
|
||||
// and returns to her the ether in the contract.
|
||||
// If time t_1 passes and Claim() has not been called, Alice should call Refund().
|
||||
func (a *alice) refund() error { //nolint:unused
|
||||
secret := a.privkeys.SpendKeyBytes()
|
||||
s := big.NewInt(0).SetBytes(reverse(secret))
|
||||
_, err := a.contract.Refund(a.auth, s)
|
||||
return err
|
||||
func (s *swapState) refund() (string, error) {
|
||||
secret := s.privkeys.SpendKeyBytes()
|
||||
sc := big.NewInt(0).SetBytes(secret)
|
||||
|
||||
log.Infof("attempting to call Refund()...")
|
||||
tx, err := s.contract.Refund(s.alice.auth, sc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tx.Hash().String(), nil
|
||||
}
|
||||
|
||||
// createMoneroWallet creates Alice's monero wallet after Bob calls Claim().
|
||||
func (a *alice) createMoneroWallet(kpAB *monero.PrivateKeyPair) (monero.Address, error) {
|
||||
func (s *swapState) createMoneroWallet(kpAB *monero.PrivateKeyPair) (monero.Address, error) {
|
||||
t := time.Now().Format("2006-Jan-2-15:04:05")
|
||||
walletName := fmt.Sprintf("alice-swap-wallet-%s", t)
|
||||
if err := a.client.GenerateFromKeys(kpAB, walletName, ""); err != nil {
|
||||
if err := s.alice.client.GenerateFromKeys(kpAB, walletName, ""); err != nil {
|
||||
// TODO: save the keypair on disk!!! otherwise we lose the keys
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("created wallet: ", walletName)
|
||||
|
||||
if err := a.client.Refresh(); err != nil {
|
||||
if err := s.alice.client.Refresh(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
balance, err := a.client.GetBalance(0)
|
||||
balance, err := s.alice.client.GetBalance(0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("wallet balance: ", balance.Balance)
|
||||
s.success = true
|
||||
return kpAB.Address(), nil
|
||||
}
|
||||
|
||||
// handleNotifyClaimed handles Bob's reveal after he calls Claim().
|
||||
// it calls `createMoneroWallet` to create Alice's wallet, allowing her to own the XMR.
|
||||
func (a *alice) handleNotifyClaimed(txHash string) (monero.Address, error) {
|
||||
receipt, err := a.ethClient.TransactionReceipt(a.ctx, ethcommon.HexToHash(txHash))
|
||||
func (s *swapState) handleNotifyClaimed(txHash string) (monero.Address, error) {
|
||||
receipt, err := s.alice.ethClient.TransactionReceipt(s.ctx, ethcommon.HexToHash(txHash))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -265,7 +281,7 @@ func (a *alice) handleNotifyClaimed(txHash string) (monero.Address, error) {
|
||||
return "", errors.New("claim transaction has no logs")
|
||||
}
|
||||
|
||||
abi, err := swap.SwapMetaData.GetAbi()
|
||||
abi, err := abi.JSON(strings.NewReader(swap.SwapABI))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -285,17 +301,23 @@ func (a *alice) handleNotifyClaimed(txHash string) (monero.Address, error) {
|
||||
|
||||
skB, err := monero.NewPrivateSpendKey(sb[:])
|
||||
if err != nil {
|
||||
log.Error("failed to convert Bob's secret into a key: %s\n", err)
|
||||
log.Errorf("failed to convert Bob's secret into a key: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
skAB := monero.SumPrivateSpendKeys(skB, a.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(a.bobViewKey, a.privkeys.ViewKey())
|
||||
skAB := monero.SumPrivateSpendKeys(skB, s.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(s.bobViewKey, s.privkeys.ViewKey())
|
||||
kpAB := monero.NewPrivateKeyPair(skAB, vkAB)
|
||||
|
||||
// write keys to file in case something goes wrong
|
||||
// TODO: configure basepath
|
||||
if err = common.WriteKeysToFile("/tmp/swap-xmr", kpAB); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pkAB := kpAB.PublicKeyPair()
|
||||
log.Info("public spend keys: ", pkAB.SpendKey().Hex())
|
||||
log.Info("public view keys: ", pkAB.ViewKey().Hex())
|
||||
|
||||
return a.createMoneroWallet(kpAB)
|
||||
return s.createMoneroWallet(kpAB)
|
||||
}
|
||||
|
||||
294
alice/swap_state.go
Normal file
294
alice/swap_state.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package alice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
"github.com/noot/atomic-swap/net"
|
||||
"github.com/noot/atomic-swap/swap-contract"
|
||||
)
|
||||
|
||||
var nextID uint64 = 0
|
||||
|
||||
var (
|
||||
errMissingKeys = errors.New("did not receive Bob's public spend or private view key")
|
||||
)
|
||||
|
||||
// swapState is an instance of a swap. it holds the info needed for the swap,
|
||||
// and its current state.
|
||||
type swapState struct {
|
||||
*alice
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
id uint64
|
||||
// amount of ETH we are providing this swap, and the amount of XMR we should receive.
|
||||
providesAmount, desiredAmount uint64
|
||||
|
||||
// our keys for this session
|
||||
privkeys *monero.PrivateKeyPair
|
||||
pubkeys *monero.PublicKeyPair
|
||||
|
||||
// Bob's keys for this session
|
||||
bobSpendKey *monero.PublicKey
|
||||
bobViewKey *monero.PrivateViewKey
|
||||
|
||||
// swap contract and timeouts in it; set once contract is deployed
|
||||
contract *swap.Swap
|
||||
t0, t1 time.Time
|
||||
|
||||
// next expected network message
|
||||
nextExpectedMessage net.Message // TODO: change to type?
|
||||
|
||||
// channels
|
||||
xmrLockedCh chan struct{}
|
||||
claimedCh chan struct{}
|
||||
|
||||
// set to true upon creating of the XMR wallet
|
||||
success bool
|
||||
}
|
||||
|
||||
func newSwapState(a *alice, providesAmount, desiredAmount uint64) *swapState {
|
||||
ctx, cancel := context.WithCancel(a.ctx)
|
||||
|
||||
s := &swapState{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
alice: a,
|
||||
id: nextID,
|
||||
providesAmount: providesAmount,
|
||||
desiredAmount: desiredAmount,
|
||||
nextExpectedMessage: &net.SendKeysMessage{}, // should this be &net.InitiateMessage{}?
|
||||
xmrLockedCh: make(chan struct{}),
|
||||
claimedCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
nextID++
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
|
||||
kp, err := s.generateKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &net.SendKeysMessage{
|
||||
PublicSpendKey: kp.SpendKey().Hex(),
|
||||
PublicViewKey: kp.ViewKey().Hex(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ProtocolComplete is called by the network when the protocol stream closes.
|
||||
// If it closes prematurely, we need to perform recovery.
|
||||
func (s *swapState) ProtocolComplete() {
|
||||
// stop all running goroutines
|
||||
s.cancel()
|
||||
|
||||
defer func() {
|
||||
s.alice.swapState = nil
|
||||
}()
|
||||
|
||||
if s.success {
|
||||
return
|
||||
}
|
||||
|
||||
switch s.nextExpectedMessage.(type) {
|
||||
case *net.SendKeysMessage:
|
||||
// we are fine, as we only just initiated the protocol.
|
||||
case *net.NotifyXMRLock:
|
||||
// we already deployed the contract, so we should call Refund().
|
||||
if err := s.tryRefund(); err != nil {
|
||||
log.Errorf("failed to refund: err=%s", err)
|
||||
return
|
||||
}
|
||||
case *net.NotifyClaimed:
|
||||
// the XMR has been locked, but the ETH hasn't been claimed.
|
||||
// we should also refund in this case.
|
||||
if err := s.tryRefund(); err != nil {
|
||||
log.Errorf("failed to refund: err=%s", err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Errorf("unexpected nextExpectedMessage in ProtocolComplete: type=%T", s.nextExpectedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *swapState) tryRefund() error {
|
||||
untilT0 := time.Until(s.t0)
|
||||
untilT1 := time.Until(s.t1)
|
||||
|
||||
if untilT0 > 0 && untilT1 < 0 {
|
||||
// we've passed t0 but aren't past t1 yet, so we need to wait until t1
|
||||
log.Infof("waiting until time %s to refund", s.t1)
|
||||
<-time.After(untilT1)
|
||||
}
|
||||
|
||||
_, err := s.refund()
|
||||
return err
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 *net.NotifyXMRLock:
|
||||
if msg.Address == "" {
|
||||
return nil, true, errors.New("got empty address for locked XMR")
|
||||
}
|
||||
|
||||
// TODO: check that XMR was locked in expected account, and confirm amount
|
||||
s.nextExpectedMessage = &net.NotifyClaimed{}
|
||||
close(s.xmrLockedCh)
|
||||
|
||||
if err := s.ready(); err != nil {
|
||||
return nil, true, fmt.Errorf("failed to call Ready: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("set swap.IsReady == true")
|
||||
|
||||
go func() {
|
||||
until := time.Until(s.t1)
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-time.After(until):
|
||||
// Bob 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)
|
||||
|
||||
// send NotifyRefund msg
|
||||
if err = s.net.SendSwapMessage(&net.NotifyRefund{
|
||||
TxHash: txhash,
|
||||
}); err != nil {
|
||||
log.Errorf("failed to send refund message: err=%s", err)
|
||||
}
|
||||
case <-s.claimedCh:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
out := &net.NotifyReady{}
|
||||
return out, false, nil
|
||||
case *net.NotifyClaimed:
|
||||
address, err := s.handleNotifyClaimed(msg.TxHash)
|
||||
if err != nil {
|
||||
log.Error("failed to create monero address: err=", err)
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
close(s.claimedCh)
|
||||
|
||||
log.Info("successfully created monero wallet from our secrets: address=", address)
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, errors.New("unexpected message type")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message, error) {
|
||||
if msg.PublicSpendKey == "" || msg.PrivateViewKey == "" {
|
||||
return nil, errMissingKeys
|
||||
}
|
||||
|
||||
log.Debug("got Bob's keys")
|
||||
s.nextExpectedMessage = &net.NotifyXMRLock{}
|
||||
|
||||
sk, err := monero.NewPublicKeyFromHex(msg.PublicSpendKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Bob's public spend key: %w", err)
|
||||
}
|
||||
|
||||
vk, err := monero.NewPrivateViewKeyFromHex(msg.PrivateViewKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate Bob's private view keys: %w", err)
|
||||
}
|
||||
|
||||
s.setBobKeys(sk, vk)
|
||||
address, err := s.deployAndLockETH(s.providesAmount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deploy contract: %w", err)
|
||||
}
|
||||
|
||||
log.Info("deployed Swap contract, waiting for XMR to be locked: address=", address)
|
||||
|
||||
// set t0 and t1
|
||||
st0, err := s.contract.Timeout0(s.alice.callOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get timeout0 from contract: err=%w", err)
|
||||
}
|
||||
|
||||
s.t0 = time.Unix(st0.Int64(), 0)
|
||||
|
||||
st1, err := s.contract.Timeout1(s.alice.callOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get timeout1 from contract: err=%w", err)
|
||||
}
|
||||
|
||||
s.t1 = time.Unix(st1.Int64(), 0)
|
||||
|
||||
// start goroutine to check that Bob locks before t_0
|
||||
go func() {
|
||||
const timeoutBuffer = time.Minute * 5
|
||||
until := time.Until(s.t0)
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-time.After(until - timeoutBuffer):
|
||||
// Bob 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.net.SendSwapMessage(&net.NotifyRefund{
|
||||
TxHash: txhash,
|
||||
}); err != nil {
|
||||
log.Errorf("failed to send refund message: err=%s", err)
|
||||
}
|
||||
case <-s.xmrLockedCh:
|
||||
return
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
out := &net.NotifyContractDeployed{
|
||||
Address: address.String(),
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *swapState) checkMessageType(msg net.Message) error {
|
||||
if msg.Type() != s.nextExpectedMessage.Type() {
|
||||
return errors.New("received unexpected message")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
128
alice/swap_state_test.go
Normal file
128
alice/swap_state_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package alice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/noot/atomic-swap/common"
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
"github.com/noot/atomic-swap/net"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockNet struct {
|
||||
msg net.Message
|
||||
}
|
||||
|
||||
func (n *mockNet) SendSwapMessage(msg net.Message) error {
|
||||
n.msg = msg
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTestAlice(t *testing.T) (*alice, *swapState) {
|
||||
alice, err := NewAlice(context.Background(), common.DefaultAliceMoneroEndpoint, common.DefaultEthEndpoint, common.DefaultPrivKeyAlice)
|
||||
require.NoError(t, err)
|
||||
swapState := newSwapState(alice, 1, 1)
|
||||
return alice, swapState
|
||||
}
|
||||
|
||||
func TestSwapState_HandleProtocolMessage_SendKeysMessage(t *testing.T) {
|
||||
_, s := newTestAlice(t)
|
||||
defer s.cancel()
|
||||
|
||||
msg := &net.SendKeysMessage{}
|
||||
_, _, err := s.HandleProtocolMessage(msg)
|
||||
require.Equal(t, errMissingKeys, err)
|
||||
|
||||
_, err = s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobPrivKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
msg = &net.SendKeysMessage{
|
||||
PublicSpendKey: bobPrivKeys.SpendKey().Public().Hex(),
|
||||
PrivateViewKey: bobPrivKeys.ViewKey().Hex(),
|
||||
}
|
||||
resp, done, err := s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.False(t, done)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, time.Second*time.Duration(defaultTimeoutDuration.Int64()), s.t1.Sub(s.t0))
|
||||
require.Equal(t, bobPrivKeys.SpendKey().Public().Hex(), s.bobSpendKey.Hex())
|
||||
require.Equal(t, bobPrivKeys.ViewKey().Hex(), s.bobViewKey.Hex())
|
||||
}
|
||||
|
||||
func TestSwapState_HandleProtocolMessage_SendKeysMessage_Refund(t *testing.T) {
|
||||
_, s := newTestAlice(t)
|
||||
defer s.cancel()
|
||||
s.net = new(mockNet)
|
||||
|
||||
// set timeout to 2s
|
||||
// TODO: pass this as a param to newSwapState
|
||||
defaultTimeoutDuration = big.NewInt(2)
|
||||
defer func() {
|
||||
defaultTimeoutDuration = big.NewInt(60 * 60 * 24)
|
||||
}()
|
||||
|
||||
_, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobPrivKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := &net.SendKeysMessage{
|
||||
PublicSpendKey: bobPrivKeys.SpendKey().Public().Hex(),
|
||||
PrivateViewKey: bobPrivKeys.ViewKey().Hex(),
|
||||
}
|
||||
|
||||
resp, done, err := s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.False(t, done)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, net.NotifyContractDeployedType, resp.Type())
|
||||
require.Equal(t, time.Second*time.Duration(defaultTimeoutDuration.Int64()), s.t1.Sub(s.t0))
|
||||
require.Equal(t, bobPrivKeys.SpendKey().Public().Hex(), s.bobSpendKey.Hex())
|
||||
require.Equal(t, bobPrivKeys.ViewKey().Hex(), s.bobViewKey.Hex())
|
||||
|
||||
// ensure we refund before t0
|
||||
time.Sleep(time.Second * 2)
|
||||
require.NotNil(t, s.net.(*mockNet).msg)
|
||||
require.Equal(t, net.NotifyRefundType, s.net.(*mockNet).msg.Type())
|
||||
}
|
||||
|
||||
func TestSwapState_NotifyXMRLock(t *testing.T) {
|
||||
_, s := newTestAlice(t)
|
||||
defer s.cancel()
|
||||
s.nextExpectedMessage = &net.NotifyXMRLock{}
|
||||
|
||||
_, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobPrivKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
s.setBobKeys(bobPrivKeys.SpendKey().Public(), bobPrivKeys.ViewKey())
|
||||
|
||||
_, err = s.deployAndLockETH(1)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := &net.NotifyXMRLock{
|
||||
Address: "asdf", // TODO: this is unused
|
||||
}
|
||||
|
||||
resp, done, err := s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.False(t, done)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, net.NotifyReadyType, resp.Type())
|
||||
|
||||
// TODO: test refund case
|
||||
}
|
||||
|
||||
func TestSwapState_NotifyClaimed(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
147
bob/net.go
147
bob/net.go
@@ -2,11 +2,7 @@ package bob
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
"github.com/noot/atomic-swap/net"
|
||||
)
|
||||
|
||||
@@ -14,29 +10,20 @@ func (b *bob) Provides() net.ProvidesCoin {
|
||||
return net.ProvidesXMR
|
||||
}
|
||||
|
||||
func (b *bob) SendKeysMessage() (*net.SendKeysMessage, error) {
|
||||
sk, vk, err := b.generateKeys()
|
||||
if err != nil {
|
||||
// InitiateProtocol is called when an RPC call is made from the user to initiate a swap.
|
||||
func (b *bob) InitiateProtocol(providesAmount, desiredAmount uint64) (net.SwapState, error) {
|
||||
if err := b.initiate(providesAmount, desiredAmount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &net.SendKeysMessage{
|
||||
PublicSpendKey: sk.Hex(),
|
||||
PrivateViewKey: vk.Hex(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *bob) InitiateProtocol(providesAmount, desiredAmount uint64) error {
|
||||
if err := b.initiate(providesAmount, desiredAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.setNextExpectedMessage(&net.SendKeysMessage{})
|
||||
return nil
|
||||
return b.swapState, nil
|
||||
}
|
||||
|
||||
func (b *bob) initiate(providesAmount, desiredAmount uint64) error {
|
||||
if b.initiated {
|
||||
b.swapMu.Lock()
|
||||
defer b.swapMu.Unlock()
|
||||
|
||||
if b.swapState != nil {
|
||||
return errors.New("protocol already in progress")
|
||||
}
|
||||
|
||||
@@ -46,122 +33,32 @@ func (b *bob) initiate(providesAmount, desiredAmount uint64) error {
|
||||
}
|
||||
|
||||
// check user's balance and that they actualy have what they will provide
|
||||
if balance.UnlockedBalance <= float64(b.providesAmount) {
|
||||
if balance.UnlockedBalance <= float64(providesAmount) {
|
||||
return errors.New("balance lower than amount to be provided")
|
||||
}
|
||||
|
||||
b.initiated = true
|
||||
b.providesAmount = providesAmount
|
||||
b.desiredAmount = desiredAmount
|
||||
b.swapState = newSwapState(b, providesAmount, desiredAmount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProtocolComplete is called when the protocol is done, whether it finished successfully or not.
|
||||
func (b *bob) ProtocolComplete() {
|
||||
b.initiated = false
|
||||
b.setNextExpectedMessage(&net.InitiateMessage{})
|
||||
}
|
||||
|
||||
func (b *bob) HandleProtocolMessage(msg net.Message) (net.Message, bool, error) {
|
||||
if err := b.checkMessageType(msg); err != nil {
|
||||
return nil, true, err
|
||||
// HandleInitiateMessage is called when we receive a network message from a peer that they wish to initiate a swap.
|
||||
func (b *bob) HandleInitiateMessage(msg *net.InitiateMessage) (net.SwapState, net.Message, error) {
|
||||
if msg.Provides != net.ProvidesETH {
|
||||
return nil, nil, errors.New("peer does not provide ETH")
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *net.InitiateMessage:
|
||||
if msg.Provides != net.ProvidesETH {
|
||||
return nil, true, errors.New("peer does not provide ETH")
|
||||
}
|
||||
|
||||
if err := b.handleSendKeysMessage(msg.SendKeysMessage); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
resp, err := b.SendKeysMessage()
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
if err = b.initiate(msg.DesiredAmount, msg.ProvidesAmount); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
return resp, false, nil
|
||||
case *net.SendKeysMessage:
|
||||
if err := b.handleSendKeysMessage(msg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
// we initiated, so we're now waiting for Alice to deploy the contract.
|
||||
return nil, false, nil
|
||||
case *net.NotifyContractDeployed:
|
||||
if msg.Address == "" {
|
||||
return nil, true, errors.New("got empty contract address")
|
||||
}
|
||||
|
||||
b.setNextExpectedMessage(&net.NotifyReady{})
|
||||
log.Info("got Swap contract address! address=%s\n", msg.Address)
|
||||
|
||||
if err := b.setContract(ethcommon.HexToAddress(msg.Address)); err != nil {
|
||||
return nil, true, fmt.Errorf("failed to instantiate contract instance: %w", err)
|
||||
}
|
||||
|
||||
// TODO: add t0 timeout case
|
||||
|
||||
addrAB, err := b.lockFunds(b.providesAmount)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("failed to lock funds: %w", err)
|
||||
}
|
||||
|
||||
out := &net.NotifyXMRLock{
|
||||
Address: string(addrAB),
|
||||
}
|
||||
|
||||
return out, false, nil
|
||||
case *net.NotifyReady:
|
||||
log.Debug("Alice called Ready(), attempting to claim funds...")
|
||||
|
||||
// contract ready, let's claim our ether
|
||||
txHash, err := b.claimFunds()
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("failed to redeem ether: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("funds claimed!!")
|
||||
out := &net.NotifyClaimed{
|
||||
TxHash: txHash,
|
||||
}
|
||||
|
||||
return out, true, nil
|
||||
case *net.NotifyRefund:
|
||||
// TODO: generate wallet
|
||||
return nil, false, errors.New("unimplemented")
|
||||
default:
|
||||
return nil, false, errors.New("unexpected message type")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bob) handleSendKeysMessage(msg *net.SendKeysMessage) error {
|
||||
if msg.PublicSpendKey == "" || msg.PublicViewKey == "" {
|
||||
return errors.New("did not receive Alice's public spend or view key")
|
||||
if err := b.initiate(msg.DesiredAmount, msg.ProvidesAmount); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
log.Debug("got Alice's public keys")
|
||||
b.setNextExpectedMessage(&net.NotifyContractDeployed{})
|
||||
if err := b.swapState.handleSendKeysMessage(msg.SendKeysMessage); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
kp, err := monero.NewPublicKeyPairFromHex(msg.PublicSpendKey, msg.PublicViewKey)
|
||||
resp, err := b.swapState.SendKeysMessage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate Alice's public keys: %w", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
b.setAlicePublicKeys(kp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bob) checkMessageType(msg net.Message) error {
|
||||
if msg.Type() != b.nextExpectedMessage.Type() {
|
||||
return errors.New("received unexpected message")
|
||||
}
|
||||
|
||||
return nil
|
||||
return b.swapState, resp, nil
|
||||
}
|
||||
|
||||
136
bob/protocol.go
136
bob/protocol.go
@@ -5,15 +5,13 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
|
||||
"github.com/noot/atomic-swap/common"
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
@@ -27,36 +25,23 @@ var (
|
||||
log = logging.Logger("bob")
|
||||
)
|
||||
|
||||
// SwapContract is the interface that must be implemented by the Swap contract.
|
||||
type SwapContract interface {
|
||||
WatchIsReady(opts *bind.WatchOpts, sink chan<- *swap.SwapIsReady) (event.Subscription, error)
|
||||
WatchRefunded(opts *bind.WatchOpts, sink chan<- *swap.SwapRefunded) (event.Subscription, error)
|
||||
Claim(opts *bind.TransactOpts, _s *big.Int) (*ethtypes.Transaction, error)
|
||||
}
|
||||
|
||||
// bob implements the functions that will be called by a user who owns XMR
|
||||
// and wishes to swap for ETH.
|
||||
type bob struct {
|
||||
ctx context.Context
|
||||
t0, t1 time.Time //nolint
|
||||
ctx context.Context
|
||||
|
||||
privkeys *monero.PrivateKeyPair
|
||||
pubkeys *monero.PublicKeyPair
|
||||
client monero.Client
|
||||
daemonClient monero.DaemonClient
|
||||
|
||||
contract SwapContract
|
||||
contractAddr ethcommon.Address
|
||||
ethClient *ethclient.Client
|
||||
auth *bind.TransactOpts
|
||||
ethClient *ethclient.Client
|
||||
ethPrivKey *ecdsa.PrivateKey
|
||||
auth *bind.TransactOpts
|
||||
callOpts *bind.CallOpts
|
||||
|
||||
ethPrivKey *ecdsa.PrivateKey
|
||||
alicePublicKeys *monero.PublicKeyPair
|
||||
net net.MessageSender
|
||||
|
||||
nextExpectedMessage net.Message
|
||||
|
||||
initiated bool
|
||||
providesAmount, desiredAmount uint64
|
||||
swapMu sync.Mutex
|
||||
swapState *swapState
|
||||
}
|
||||
|
||||
// NewBob returns a new instance of Bob.
|
||||
@@ -77,62 +62,68 @@ func NewBob(ctx context.Context, moneroEndpoint, moneroDaemonEndpoint, ethEndpoi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pub := pk.Public().(*ecdsa.PublicKey)
|
||||
|
||||
return &bob{
|
||||
ctx: ctx,
|
||||
client: monero.NewClient(moneroEndpoint),
|
||||
daemonClient: monero.NewClient(moneroDaemonEndpoint),
|
||||
ethClient: ec,
|
||||
ethPrivKey: pk,
|
||||
auth: auth,
|
||||
nextExpectedMessage: &net.InitiateMessage{},
|
||||
ctx: ctx,
|
||||
client: monero.NewClient(moneroEndpoint),
|
||||
daemonClient: monero.NewClient(moneroDaemonEndpoint),
|
||||
ethClient: ec,
|
||||
ethPrivKey: pk,
|
||||
auth: auth,
|
||||
callOpts: &bind.CallOpts{
|
||||
From: crypto.PubkeyToAddress(*pub),
|
||||
Context: ctx,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *bob) setNextExpectedMessage(msg net.Message) {
|
||||
b.nextExpectedMessage = msg
|
||||
func (b *bob) SetMessageSender(n net.MessageSender) {
|
||||
b.net = n
|
||||
}
|
||||
|
||||
// generateKeys generates Bob's spend and view keys (S_b, V_b)
|
||||
// generateKeys generates Bob's spend and view keys (s_b, v_b)
|
||||
// It returns Bob's public spend key and his private view key, so that Alice can see
|
||||
// if the funds are locked.
|
||||
func (b *bob) generateKeys() (*monero.PublicKey, *monero.PrivateViewKey, error) {
|
||||
if b.privkeys != nil {
|
||||
return b.pubkeys.SpendKey(), b.privkeys.ViewKey(), nil
|
||||
func (s *swapState) generateKeys() (*monero.PublicKey, *monero.PrivateViewKey, error) {
|
||||
if s.privkeys != nil {
|
||||
return s.pubkeys.SpendKey(), s.privkeys.ViewKey(), nil
|
||||
}
|
||||
|
||||
var err error
|
||||
b.privkeys, err = monero.GenerateKeys()
|
||||
s.privkeys, err = monero.GenerateKeys()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: configure basepath
|
||||
if err := common.WriteKeysToFile("./bob-xmr", b.privkeys); err != nil {
|
||||
// TODO: write swap ID
|
||||
if err := common.WriteKeysToFile("/tmp/bob-xmr", s.privkeys); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
b.pubkeys = b.privkeys.PublicKeyPair()
|
||||
return b.pubkeys.SpendKey(), b.privkeys.ViewKey(), nil
|
||||
s.pubkeys = s.privkeys.PublicKeyPair()
|
||||
return s.pubkeys.SpendKey(), s.privkeys.ViewKey(), nil
|
||||
}
|
||||
|
||||
// setAlicePublicKeys sets Alice's public spend and view keys
|
||||
func (b *bob) setAlicePublicKeys(sk *monero.PublicKeyPair) {
|
||||
b.alicePublicKeys = sk
|
||||
func (s *swapState) setAlicePublicKeys(sk *monero.PublicKeyPair) {
|
||||
s.alicePublicKeys = sk
|
||||
}
|
||||
|
||||
// setContract sets the contract in which Alice has locked her ETH.
|
||||
func (b *bob) setContract(address ethcommon.Address) error {
|
||||
func (s *swapState) setContract(address ethcommon.Address) error {
|
||||
var err error
|
||||
b.contractAddr = address
|
||||
b.contract, err = swap.NewSwap(address, b.ethClient)
|
||||
s.contractAddr = address
|
||||
s.contract, err = swap.NewSwap(address, s.bob.ethClient)
|
||||
return err
|
||||
}
|
||||
|
||||
// watchForReady watches for Alice to call Ready() on the swap contract, allowing
|
||||
// Bob to call Claim().
|
||||
func (b *bob) watchForReady() (<-chan struct{}, error) { //nolint:unused
|
||||
func (s *swapState) watchForReady() (<-chan struct{}, error) { //nolint:unused
|
||||
watchOpts := &bind.WatchOpts{
|
||||
Context: b.ctx,
|
||||
Context: s.ctx,
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -140,7 +131,7 @@ func (b *bob) watchForReady() (<-chan struct{}, error) { //nolint:unused
|
||||
defer close(done)
|
||||
|
||||
// watch for Refund() event on chain, calculate unlock key as result
|
||||
sub, err := b.contract.WatchIsReady(watchOpts, ch)
|
||||
sub, err := s.contract.WatchIsReady(watchOpts, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,7 +148,7 @@ func (b *bob) watchForReady() (<-chan struct{}, error) { //nolint:unused
|
||||
|
||||
// contract is ready!!
|
||||
close(done)
|
||||
case <-b.ctx.Done():
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -174,9 +165,9 @@ func (b *bob) watchForReady() (<-chan struct{}, error) { //nolint:unused
|
||||
// the private spend and view keys that contain the previously locked monero
|
||||
// ((s_a + s_b), (v_a + v_b)) are sent over the channel.
|
||||
// Bob can then use these keys to move his funds if he wishes.
|
||||
func (b *bob) watchForRefund() (<-chan *monero.PrivateKeyPair, error) { //nolint:unused
|
||||
func (s *swapState) watchForRefund() (<-chan *monero.PrivateKeyPair, error) { //nolint:unused
|
||||
watchOpts := &bind.WatchOpts{
|
||||
Context: b.ctx,
|
||||
Context: s.ctx,
|
||||
}
|
||||
|
||||
out := make(chan *monero.PrivateKeyPair)
|
||||
@@ -184,7 +175,7 @@ func (b *bob) watchForRefund() (<-chan *monero.PrivateKeyPair, error) { //nolint
|
||||
defer close(out)
|
||||
|
||||
// watch for Refund() event on chain, calculate unlock key as result
|
||||
sub, err := b.contract.WatchRefunded(watchOpts, ch)
|
||||
sub, err := s.contract.WatchRefunded(watchOpts, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,11 +207,11 @@ func (b *bob) watchForRefund() (<-chan *monero.PrivateKeyPair, error) { //nolint
|
||||
return
|
||||
}
|
||||
|
||||
skAB := monero.SumPrivateSpendKeys(skA, b.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(vkA, b.privkeys.ViewKey())
|
||||
skAB := monero.SumPrivateSpendKeys(skA, s.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(vkA, s.privkeys.ViewKey())
|
||||
kpAB := monero.NewPrivateKeyPair(skAB, vkAB)
|
||||
out <- kpAB
|
||||
case <-b.ctx.Done():
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -233,14 +224,14 @@ func (b *bob) watchForRefund() (<-chan *monero.PrivateKeyPair, error) { //nolint
|
||||
// (S_a + S_b), viewable with (V_a + V_b)
|
||||
// It accepts the amount to lock as the input
|
||||
// TODO: units
|
||||
func (b *bob) lockFunds(amount uint64) (monero.Address, error) {
|
||||
kp := monero.SumSpendAndViewKeys(b.alicePublicKeys, b.pubkeys)
|
||||
func (s *swapState) lockFunds(amount uint64) (monero.Address, error) {
|
||||
kp := monero.SumSpendAndViewKeys(s.alicePublicKeys, s.pubkeys)
|
||||
|
||||
log.Debug("public spend keys: ", kp.SpendKey().Hex())
|
||||
log.Debug("public view keys: ", kp.ViewKey().Hex())
|
||||
log.Infof("going to lock XMR funds, amount=%d", amount)
|
||||
|
||||
balance, err := b.client.GetBalance(0)
|
||||
balance, err := s.bob.client.GetBalance(0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -250,20 +241,22 @@ func (b *bob) lockFunds(amount uint64) (monero.Address, error) {
|
||||
log.Debug("blocks to unlock: ", balance.BlocksToUnlock)
|
||||
|
||||
address := kp.Address()
|
||||
if err := b.client.Transfer(address, 0, uint(amount)); err != nil {
|
||||
if err := s.bob.client.Transfer(address, 0, uint(amount)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bobAddr, err := b.client.GetAddress(0)
|
||||
bobAddr, err := s.bob.client.GetAddress(0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := b.daemonClient.GenerateBlocks(bobAddr.Address, 1); err != nil {
|
||||
// TODO: this has gonna be removed before stagenet/mainnet, will need to add
|
||||
// waiting logs or such
|
||||
if err := s.bob.daemonClient.GenerateBlocks(bobAddr.Address, 1); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := b.client.Refresh(); err != nil {
|
||||
if err := s.bob.client.Refresh(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -272,11 +265,11 @@ func (b *bob) lockFunds(amount uint64) (monero.Address, error) {
|
||||
}
|
||||
|
||||
// claimFunds redeems Bob's ETH funds by calling Claim() on the contract
|
||||
func (b *bob) claimFunds() (string, error) {
|
||||
pub := b.ethPrivKey.Public().(*ecdsa.PublicKey)
|
||||
func (s *swapState) claimFunds() (string, error) {
|
||||
pub := s.ethPrivKey.Public().(*ecdsa.PublicKey)
|
||||
addr := ethcrypto.PubkeyToAddress(*pub)
|
||||
|
||||
balance, err := b.ethClient.BalanceAt(b.ctx, addr, nil)
|
||||
balance, err := s.ethClient.BalanceAt(s.ctx, addr, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -284,10 +277,10 @@ func (b *bob) claimFunds() (string, error) {
|
||||
log.Info("Bob's balance before claim: ", balance)
|
||||
|
||||
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing Bob's secret spend key
|
||||
secret := b.privkeys.SpendKeyBytes()
|
||||
s := big.NewInt(0).SetBytes(secret)
|
||||
secret := s.privkeys.SpendKeyBytes()
|
||||
sc := big.NewInt(0).SetBytes(secret)
|
||||
|
||||
tx, err := b.contract.Claim(b.auth, s)
|
||||
tx, err := s.contract.Claim(s.bob.auth, sc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -295,7 +288,7 @@ func (b *bob) claimFunds() (string, error) {
|
||||
log.Info("success! Bob claimed funds")
|
||||
log.Info("tx hash: ", tx.Hash())
|
||||
|
||||
receipt, err := b.ethClient.TransactionReceipt(b.ctx, tx.Hash())
|
||||
receipt, err := s.bob.ethClient.TransactionReceipt(s.ctx, tx.Hash())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -304,11 +297,12 @@ func (b *bob) claimFunds() (string, error) {
|
||||
log.Info("included in block number: ", receipt.Logs[0].BlockNumber)
|
||||
log.Info("secret: ", fmt.Sprintf("%x", secret))
|
||||
|
||||
balance, err = b.ethClient.BalanceAt(b.ctx, addr, nil)
|
||||
balance, err = s.bob.ethClient.BalanceAt(s.ctx, addr, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("Bob's balance after claim: ", balance)
|
||||
s.success = true
|
||||
return tx.Hash().String(), nil
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package bob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/noot/atomic-swap/common"
|
||||
"github.com/noot/atomic-swap/swap-contract"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestBob(t *testing.T) *bob {
|
||||
bob, err := NewBob(context.Background(), common.DefaultBobMoneroEndpoint, common.DefaultMoneroDaemonEndpoint, common.DefaultEthEndpoint, common.DefaultPrivKeyBob)
|
||||
require.NoError(t, err)
|
||||
_, _, err = bob.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
pkBob, err := crypto.HexToECDSA(common.DefaultPrivKeyBob)
|
||||
require.NoError(t, err)
|
||||
|
||||
bob.auth, err = bind.NewKeyedTransactorWithChainID(pkBob, big.NewInt(common.GanacheChainID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var pubkey [32]byte
|
||||
copy(pubkey[:], bob.pubkeys.SpendKey().Bytes())
|
||||
bob.contractAddr, _, bob.contract, err = swap.DeploySwap(bob.auth, conn, pubkey, [32]byte{})
|
||||
require.NoError(t, err)
|
||||
return bob
|
||||
}
|
||||
|
||||
func TestBob_GenerateKeys(t *testing.T) {
|
||||
bob := newTestBob(t)
|
||||
|
||||
pubSpendKey, privViewKey, err := bob.generateKeys()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, bob.privkeys)
|
||||
require.NotNil(t, bob.pubkeys)
|
||||
require.NotNil(t, pubSpendKey)
|
||||
require.NotNil(t, privViewKey)
|
||||
}
|
||||
|
||||
func TestBob_ClaimFunds(t *testing.T) {
|
||||
bob := newTestBob(t)
|
||||
|
||||
_, err := bob.contract.(*swap.Swap).SetReady(bob.auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
txHash, err := bob.claimFunds()
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, "", txHash)
|
||||
}
|
||||
359
bob/swap_state.go
Normal file
359
bob/swap_state.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package bob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/noot/atomic-swap/common"
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
"github.com/noot/atomic-swap/net"
|
||||
"github.com/noot/atomic-swap/swap-contract"
|
||||
)
|
||||
|
||||
var nextID uint64 = 0
|
||||
|
||||
var (
|
||||
errMissingKeys = errors.New("did not receive Alice's public spend or view key")
|
||||
errMissingAddress = errors.New("got empty contract address")
|
||||
)
|
||||
|
||||
type swapState struct {
|
||||
*bob
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
id uint64
|
||||
providesAmount, desiredAmount uint64
|
||||
|
||||
// our keys for this session
|
||||
privkeys *monero.PrivateKeyPair
|
||||
pubkeys *monero.PublicKeyPair
|
||||
|
||||
// swap contract and timeouts in it; set once contract is deployed
|
||||
contract *swap.Swap
|
||||
contractAddr ethcommon.Address
|
||||
t0, t1 time.Time
|
||||
|
||||
// Alice's keys for this session
|
||||
alicePublicKeys *monero.PublicKeyPair
|
||||
|
||||
// next expected network message
|
||||
nextExpectedMessage net.Message
|
||||
|
||||
// channels
|
||||
readyCh chan struct{}
|
||||
|
||||
// set to true on claiming the ETH
|
||||
success bool
|
||||
}
|
||||
|
||||
func newSwapState(b *bob, providesAmount, desiredAmount uint64) *swapState {
|
||||
ctx, cancel := context.WithCancel(b.ctx)
|
||||
|
||||
s := &swapState{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
bob: b,
|
||||
id: nextID,
|
||||
providesAmount: providesAmount,
|
||||
desiredAmount: desiredAmount,
|
||||
nextExpectedMessage: &net.SendKeysMessage{},
|
||||
readyCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
nextID++
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
|
||||
sk, vk, err := s.generateKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &net.SendKeysMessage{
|
||||
PublicSpendKey: sk.Hex(),
|
||||
PrivateViewKey: vk.Hex(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ProtocolComplete is called by the network when the protocol stream closes.
|
||||
// If it closes prematurely, we need to perform recovery.
|
||||
func (s *swapState) ProtocolComplete() {
|
||||
// stop all running goroutines
|
||||
s.cancel()
|
||||
|
||||
defer func() {
|
||||
s.bob.swapState = nil
|
||||
}()
|
||||
|
||||
if s.success {
|
||||
return
|
||||
}
|
||||
|
||||
switch s.nextExpectedMessage.(type) {
|
||||
case *net.SendKeysMessage:
|
||||
// we are fine, as we only just initiated the protocol.
|
||||
case *net.NotifyContractDeployed:
|
||||
// we were waiting for the contract to be deployed, but haven't
|
||||
// locked out funds yet, so we're fine.
|
||||
case *net.NotifyReady:
|
||||
// we already locked our funds - need to wait until we can claim
|
||||
// the funds (ie. wait until after t0)
|
||||
if err := s.tryClaim(); err != nil {
|
||||
log.Errorf("failed to claim funds: err=%s", err)
|
||||
}
|
||||
default:
|
||||
log.Errorf("unexpected nextExpectedMessage in ProtocolComplete: type=%T", s.nextExpectedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *swapState) tryClaim() error {
|
||||
untilT0 := time.Until(s.t0)
|
||||
untilT1 := time.Until(s.t1)
|
||||
|
||||
if untilT0 < 0 {
|
||||
// we need to wait until t0 to claim
|
||||
log.Infof("waiting until time %s to refund", s.t0)
|
||||
<-time.After(untilT0)
|
||||
}
|
||||
|
||||
if untilT1 > 0 { //nolint
|
||||
// we've passed t1, our only option now is for Alice to refund
|
||||
// and we can regain control of the locked XMR.
|
||||
// TODO: watch contract for Refund() to be called.
|
||||
}
|
||||
|
||||
_, err := s.claimFunds()
|
||||
return err
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if err := s.checkMessageType(msg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *net.SendKeysMessage:
|
||||
if err := s.handleSendKeysMessage(msg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
// we initiated, so we're now waiting for Alice to deploy the contract.
|
||||
return nil, false, nil
|
||||
case *net.NotifyContractDeployed:
|
||||
if msg.Address == "" {
|
||||
return nil, true, errMissingAddress
|
||||
}
|
||||
|
||||
s.nextExpectedMessage = &net.NotifyReady{}
|
||||
log.Infof("got Swap contract address! address=%s", msg.Address)
|
||||
|
||||
if err := s.setContract(ethcommon.HexToAddress(msg.Address)); err != nil {
|
||||
return nil, true, fmt.Errorf("failed to instantiate contract instance: %w", err)
|
||||
}
|
||||
|
||||
addrAB, err := s.lockFunds(s.providesAmount)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("failed to lock funds: %w", err)
|
||||
}
|
||||
|
||||
out := &net.NotifyXMRLock{
|
||||
Address: string(addrAB),
|
||||
}
|
||||
|
||||
// set t0 and t1
|
||||
st0, err := s.contract.Timeout0(s.bob.callOpts)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("failed to get timeout0 from contract: err=%w", err)
|
||||
}
|
||||
|
||||
s.t0 = time.Unix(st0.Int64(), 0)
|
||||
|
||||
st1, err := s.contract.Timeout1(s.bob.callOpts)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("failed to get timeout1 from contract: err=%w", err)
|
||||
}
|
||||
|
||||
s.t1 = time.Unix(st1.Int64(), 0)
|
||||
|
||||
go func() {
|
||||
until := time.Until(s.t0)
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-time.After(until):
|
||||
// we can now call Claim()
|
||||
txHash, err := s.claimFunds()
|
||||
if err != nil {
|
||||
log.Errorf("failed to claim: err=%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("funds claimed!")
|
||||
|
||||
// send *net.NotifyClaimed
|
||||
if err := s.net.SendSwapMessage(&net.NotifyClaimed{
|
||||
TxHash: txHash,
|
||||
}); err != nil {
|
||||
log.Errorf("failed to send NotifyClaimed message: err=%s", err)
|
||||
}
|
||||
case <-s.readyCh:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return out, false, nil
|
||||
case *net.NotifyReady:
|
||||
log.Debug("Alice called Ready(), attempting to claim funds...")
|
||||
close(s.readyCh)
|
||||
|
||||
// contract ready, let's claim our ether
|
||||
txHash, err := s.claimFunds()
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("failed to redeem ether: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("funds claimed!!")
|
||||
out := &net.NotifyClaimed{
|
||||
TxHash: txHash,
|
||||
}
|
||||
|
||||
return out, true, nil
|
||||
case *net.NotifyRefund:
|
||||
// generate monero wallet, regaining control over locked funds
|
||||
addr, err := s.handleRefund(msg.TxHash)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
log.Infof("regained control over monero account %s", addr)
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, true, errors.New("unexpected message type")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) error {
|
||||
if msg.PublicSpendKey == "" || msg.PublicViewKey == "" {
|
||||
return errMissingKeys
|
||||
}
|
||||
|
||||
log.Debug("got Alice's public keys")
|
||||
s.nextExpectedMessage = &net.NotifyContractDeployed{}
|
||||
|
||||
kp, err := monero.NewPublicKeyPairFromHex(msg.PublicSpendKey, msg.PublicViewKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate Alice's public keys: %w", err)
|
||||
}
|
||||
|
||||
s.setAlicePublicKeys(kp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *swapState) handleRefund(txHash string) (monero.Address, error) {
|
||||
receipt, err := s.bob.ethClient.TransactionReceipt(s.ctx, ethcommon.HexToHash(txHash))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(receipt.Logs) == 0 {
|
||||
return "", errors.New("claim transaction has no logs")
|
||||
}
|
||||
|
||||
abi, err := abi.JSON(strings.NewReader(swap.SwapABI))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := receipt.Logs[0].Data
|
||||
res, err := abi.Unpack("Refunded", data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug("got Alice's secret: ", hex.EncodeToString(res[0].(*big.Int).Bytes()))
|
||||
|
||||
// got Alice's secret
|
||||
sbBytes := res[0].(*big.Int).Bytes()
|
||||
var sa [32]byte
|
||||
copy(sa[:], sbBytes)
|
||||
|
||||
skA, err := monero.NewPrivateSpendKey(sa[:])
|
||||
if err != nil {
|
||||
log.Errorf("failed to convert Alice's secret into a key: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
vkA, err := skA.View()
|
||||
if err != nil {
|
||||
log.Errorf("failed to convert Alice's spend key into a view key: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
skAB := monero.SumPrivateSpendKeys(skA, s.privkeys.SpendKey())
|
||||
vkAB := monero.SumPrivateViewKeys(vkA, s.privkeys.ViewKey())
|
||||
kpAB := monero.NewPrivateKeyPair(skAB, vkAB)
|
||||
|
||||
// write keys to file in case something goes wrong
|
||||
// TODO: configure basepath
|
||||
if err = common.WriteKeysToFile("/tmp/swap-xmr", kpAB); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pkAB := kpAB.PublicKeyPair()
|
||||
log.Info("public spend keys: ", pkAB.SpendKey().Hex())
|
||||
log.Info("public view keys: ", pkAB.ViewKey().Hex())
|
||||
|
||||
return s.createMoneroWallet(kpAB)
|
||||
}
|
||||
|
||||
// createMoneroWallet creates Alice's monero wallet after Bob calls Claim().
|
||||
func (s *swapState) createMoneroWallet(kpAB *monero.PrivateKeyPair) (monero.Address, error) {
|
||||
t := time.Now().Format("2006-Jan-2-15:04:05")
|
||||
walletName := fmt.Sprintf("bob-swap-wallet-%s", t)
|
||||
if err := s.bob.client.GenerateFromKeys(kpAB, walletName, ""); err != nil {
|
||||
// TODO: save the keypair on disk!!! otherwise we lose the keys
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("created wallet: ", walletName)
|
||||
|
||||
if err := s.bob.client.Refresh(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
balance, err := s.bob.client.GetBalance(0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("wallet balance: ", balance.Balance)
|
||||
s.success = true
|
||||
return kpAB.Address(), nil
|
||||
}
|
||||
|
||||
func (s *swapState) checkMessageType(msg net.Message) error {
|
||||
// Alice might refund anytime before t0 or after t1, so we should allow this.
|
||||
if _, ok := msg.(*net.NotifyRefund); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if msg.Type() != s.nextExpectedMessage.Type() {
|
||||
return errors.New("received unexpected message")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
280
bob/swap_state_test.go
Normal file
280
bob/swap_state_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package bob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/noot/atomic-swap/common"
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
"github.com/noot/atomic-swap/net"
|
||||
"github.com/noot/atomic-swap/swap-contract"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockNet struct {
|
||||
msg net.Message
|
||||
}
|
||||
|
||||
func (n *mockNet) SendSwapMessage(msg net.Message) error {
|
||||
n.msg = msg
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultTimeoutDuration = big.NewInt(60 * 60 * 24) // 1 day = 60s * 60min * 24hr
|
||||
|
||||
func newTestBob(t *testing.T) (*bob, *swapState) {
|
||||
bob, err := NewBob(context.Background(), common.DefaultBobMoneroEndpoint, common.DefaultMoneroDaemonEndpoint, common.DefaultEthEndpoint, common.DefaultPrivKeyBob)
|
||||
require.NoError(t, err)
|
||||
|
||||
bobAddr, err := bob.client.GetAddress(0)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = bob.daemonClient.GenerateBlocks(bobAddr.Address, 61)
|
||||
require.NoError(t, err)
|
||||
|
||||
//time.Sleep(time.Second * 5)
|
||||
|
||||
swapState := newSwapState(bob, 33, 33)
|
||||
return bob, swapState
|
||||
}
|
||||
|
||||
func TestSwapState_GenerateKeys(t *testing.T) {
|
||||
_, swapState := newTestBob(t)
|
||||
|
||||
pubSpendKey, privViewKey, err := swapState.generateKeys()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, swapState.privkeys)
|
||||
require.NotNil(t, swapState.pubkeys)
|
||||
require.NotNil(t, pubSpendKey)
|
||||
require.NotNil(t, privViewKey)
|
||||
}
|
||||
|
||||
func TestSwapState_ClaimFunds(t *testing.T) {
|
||||
bob, swapState := newTestBob(t)
|
||||
_, _, err := swapState.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
pkBob, err := crypto.HexToECDSA(common.DefaultPrivKeyBob)
|
||||
require.NoError(t, err)
|
||||
|
||||
bob.auth, err = bind.NewKeyedTransactorWithChainID(pkBob, big.NewInt(common.GanacheChainID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var pubkey [32]byte
|
||||
copy(pubkey[:], swapState.pubkeys.SpendKey().Bytes())
|
||||
swapState.contractAddr, _, swapState.contract, err = swap.DeploySwap(bob.auth, conn, pubkey, [32]byte{}, defaultTimeoutDuration)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = swapState.contract.SetReady(bob.auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
txHash, err := swapState.claimFunds()
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, "", txHash)
|
||||
}
|
||||
|
||||
func TestSwapState_handleSendKeysMessage(t *testing.T) {
|
||||
_, s := newTestBob(t)
|
||||
|
||||
msg := &net.SendKeysMessage{}
|
||||
err := s.handleSendKeysMessage(msg)
|
||||
require.Equal(t, errMissingKeys, err)
|
||||
|
||||
alicePrivKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
alicePubKeys := alicePrivKeys.PublicKeyPair()
|
||||
|
||||
msg = &net.SendKeysMessage{
|
||||
PublicSpendKey: alicePrivKeys.SpendKey().Public().Hex(),
|
||||
PublicViewKey: alicePrivKeys.ViewKey().Public().Hex(),
|
||||
}
|
||||
|
||||
err = s.handleSendKeysMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &net.NotifyContractDeployed{}, s.nextExpectedMessage)
|
||||
require.Equal(t, alicePubKeys.SpendKey().Hex(), s.alicePublicKeys.SpendKey().Hex())
|
||||
require.Equal(t, alicePubKeys.ViewKey().Hex(), s.alicePublicKeys.ViewKey().Hex())
|
||||
}
|
||||
|
||||
func deploySwap(t *testing.T, bob *bob, swapState *swapState, timeout time.Duration) (ethcommon.Address, *swap.Swap) {
|
||||
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
tm := big.NewInt(int64(timeout.Seconds()))
|
||||
|
||||
var pubkey [32]byte
|
||||
copy(pubkey[:], swapState.pubkeys.SpendKey().Bytes())
|
||||
addr, _, contract, err := swap.DeploySwap(bob.auth, conn, pubkey, [32]byte{}, tm)
|
||||
require.NoError(t, err)
|
||||
return addr, contract
|
||||
}
|
||||
|
||||
func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_ok(t *testing.T) {
|
||||
bob, s := newTestBob(t)
|
||||
defer s.cancel()
|
||||
s.nextExpectedMessage = &net.NotifyContractDeployed{}
|
||||
_, _, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
s.setAlicePublicKeys(aliceKeys.PublicKeyPair())
|
||||
|
||||
msg := &net.NotifyContractDeployed{}
|
||||
resp, done, err := s.HandleProtocolMessage(msg)
|
||||
require.Equal(t, errMissingAddress, err)
|
||||
require.Nil(t, resp)
|
||||
require.True(t, done)
|
||||
|
||||
duration, err := time.ParseDuration("2s")
|
||||
require.NoError(t, err)
|
||||
addr, _ := deploySwap(t, bob, s, duration)
|
||||
|
||||
msg = &net.NotifyContractDeployed{
|
||||
Address: addr.String(),
|
||||
}
|
||||
|
||||
resp, done, err = s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, net.NotifyXMRLockType, resp.Type())
|
||||
require.False(t, done)
|
||||
require.NotNil(t, s.contract)
|
||||
require.Equal(t, addr, s.contractAddr)
|
||||
require.Equal(t, duration, s.t1.Sub(s.t0))
|
||||
require.Equal(t, &net.NotifyReady{}, s.nextExpectedMessage)
|
||||
}
|
||||
|
||||
func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_timeout(t *testing.T) {
|
||||
bob, s := newTestBob(t)
|
||||
defer s.cancel()
|
||||
s.net = new(mockNet)
|
||||
s.nextExpectedMessage = &net.NotifyContractDeployed{}
|
||||
_, _, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
s.setAlicePublicKeys(aliceKeys.PublicKeyPair())
|
||||
|
||||
msg := &net.NotifyContractDeployed{}
|
||||
resp, done, err := s.HandleProtocolMessage(msg)
|
||||
require.Equal(t, errMissingAddress, err)
|
||||
require.Nil(t, resp)
|
||||
require.True(t, done)
|
||||
|
||||
duration, err := time.ParseDuration("1s")
|
||||
require.NoError(t, err)
|
||||
addr, _ := deploySwap(t, bob, s, duration)
|
||||
|
||||
msg = &net.NotifyContractDeployed{
|
||||
Address: addr.String(),
|
||||
}
|
||||
|
||||
resp, done, err = s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, net.NotifyXMRLockType, resp.Type())
|
||||
require.False(t, done)
|
||||
require.NotNil(t, s.contract)
|
||||
require.Equal(t, addr, s.contractAddr)
|
||||
require.Equal(t, duration, s.t1.Sub(s.t0))
|
||||
require.Equal(t, &net.NotifyReady{}, s.nextExpectedMessage)
|
||||
|
||||
time.Sleep(duration)
|
||||
require.NotNil(t, s.net.(*mockNet).msg)
|
||||
}
|
||||
|
||||
func TestSwapState_HandleProtocolMessage_NotifyReady(t *testing.T) {
|
||||
bob, s := newTestBob(t)
|
||||
|
||||
s.nextExpectedMessage = &net.NotifyReady{}
|
||||
_, _, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
duration, err := time.ParseDuration("1s")
|
||||
require.NoError(t, err)
|
||||
_, s.contract = deploySwap(t, bob, s, duration)
|
||||
time.Sleep(duration)
|
||||
|
||||
msg := &net.NotifyReady{}
|
||||
|
||||
resp, done, err := s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
require.True(t, done)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, net.NotifyClaimedType, resp.Type())
|
||||
}
|
||||
|
||||
func TestSwapState_handleRefund(t *testing.T) {
|
||||
bob, s := newTestBob(t)
|
||||
|
||||
// TODO: wallet-rpc doesn't allow for both --wallet-file and --wallet-dir, this is an issue
|
||||
_, _, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
s.setAlicePublicKeys(aliceKeys.PublicKeyPair())
|
||||
|
||||
duration, err := time.ParseDuration("10m")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, s.contract = deploySwap(t, bob, s, duration)
|
||||
|
||||
// lock XMR
|
||||
_, err = s.lockFunds(s.providesAmount)
|
||||
require.NoError(t, err)
|
||||
|
||||
// call refund w/ Alice's spend key
|
||||
secret := aliceKeys.SpendKeyBytes()
|
||||
sc := big.NewInt(0).SetBytes(secret)
|
||||
tx, err := s.contract.Refund(s.bob.auth, sc)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _ = s.handleRefund(tx.Hash().String())
|
||||
// TODO: currently fails w/ rpc error "No wallet dir configured"
|
||||
}
|
||||
|
||||
func TestSwapState_HandleProtocolMessage_NotifyRefund(t *testing.T) {
|
||||
bob, s := newTestBob(t)
|
||||
|
||||
_, _, err := s.generateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceKeys, err := monero.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
s.setAlicePublicKeys(aliceKeys.PublicKeyPair())
|
||||
|
||||
duration, err := time.ParseDuration("10m")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, s.contract = deploySwap(t, bob, s, duration)
|
||||
|
||||
// lock XMR
|
||||
_, err = s.lockFunds(s.providesAmount)
|
||||
require.NoError(t, err)
|
||||
|
||||
// call refund w/ Alice's spend key
|
||||
secret := aliceKeys.SpendKeyBytes()
|
||||
sc := big.NewInt(0).SetBytes(secret)
|
||||
tx, err := s.contract.Refund(s.bob.auth, sc)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := &net.NotifyRefund{
|
||||
TxHash: tx.Hash().String(),
|
||||
}
|
||||
|
||||
_, _, _ = s.HandleProtocolMessage(msg)
|
||||
// TODO: currently fails w/ rpc error "No wallet dir configured"
|
||||
}
|
||||
35
cmd/client/main.go
Normal file
35
cmd/client/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
logging "github.com/ipfs/go-log"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var log = logging.Logger("cmd")
|
||||
|
||||
var (
|
||||
app = &cli.App{
|
||||
Name: "swapcli",
|
||||
Usage: "Client for swapd",
|
||||
Action: runClient,
|
||||
Flags: []cli.Flag{
|
||||
&cli.UintFlag{
|
||||
Name: "daemon-addr",
|
||||
Usage: "address of swap daemon; default http://localhost:5001",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runClient(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
@@ -149,18 +149,24 @@ func runDaemon(c *cli.Context) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
type Handler interface {
|
||||
net.Handler
|
||||
rpc.Protocol
|
||||
SetMessageSender(net.MessageSender)
|
||||
}
|
||||
|
||||
var (
|
||||
protocol rpc.Protocol
|
||||
err error
|
||||
handler Handler
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case isAlice:
|
||||
protocol, err = alice.NewAlice(ctx, moneroEndpoint, ethEndpoint, ethPrivKey)
|
||||
handler, err = alice.NewAlice(ctx, moneroEndpoint, ethEndpoint, ethPrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case isBob:
|
||||
protocol, err = bob.NewBob(ctx, moneroEndpoint, daemonEndpoint, ethEndpoint, ethPrivKey)
|
||||
handler, err = bob.NewBob(ctx, moneroEndpoint, daemonEndpoint, ethEndpoint, ethPrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -191,7 +197,7 @@ func runDaemon(c *cli.Context) error {
|
||||
ExchangeRate: defaultExchangeRate,
|
||||
KeyFile: defaultAliceLibp2pKey, // TODO: make flag
|
||||
Bootnodes: bootnodes,
|
||||
Handler: protocol,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
if c.Bool("bob") {
|
||||
@@ -206,6 +212,9 @@ func runDaemon(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// connect network to protocol handler
|
||||
handler.SetMessageSender(host)
|
||||
|
||||
if err = host.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -213,7 +222,7 @@ func runDaemon(c *cli.Context) error {
|
||||
cfg := &rpc.Config{
|
||||
Port: port,
|
||||
Net: host,
|
||||
Protocol: protocol,
|
||||
Protocol: handler,
|
||||
}
|
||||
|
||||
s, err := rpc.NewServer(cfg)
|
||||
@@ -5,7 +5,7 @@ pragma solidity ^0.8.5;
|
||||
// import "./Ed25519.sol";
|
||||
import "./Ed25519_alt.sol";
|
||||
|
||||
contract SwapOnChain {
|
||||
contract Swap {
|
||||
// Ed25519 library
|
||||
Ed25519 immutable ed25519;
|
||||
|
||||
@@ -20,13 +20,12 @@ contract SwapOnChain {
|
||||
// this public key is a point on the ed25519 curve
|
||||
bytes32 public immutable pubKeyRefund;
|
||||
|
||||
// time period from contract creation
|
||||
// during which Alice can call either set_ready or refund
|
||||
// timestamp (set at contract creation)
|
||||
// before which Alice can call either set_ready or refund
|
||||
uint256 public immutable timeout_0;
|
||||
|
||||
// time period from the moment Alice calls `set_ready`
|
||||
// during which Bob can claim. After this, Alice can refund again
|
||||
uint256 public timeout_1;
|
||||
// timestamp after which Bob cannot claim, only Alice can refund.
|
||||
uint256 public immutable timeout_1;
|
||||
|
||||
// Alice sets ready to true when she sees the funds locked on the other chain.
|
||||
// this prevents Bob from withdrawing funds without locking funds on the other chain first
|
||||
@@ -37,20 +36,20 @@ contract SwapOnChain {
|
||||
event Claimed(uint256 s);
|
||||
event Refunded(uint256 s);
|
||||
|
||||
constructor(bytes32 _pubKeyClaim, bytes32 _pubKeyRefund) payable {
|
||||
constructor(bytes32 _pubKeyClaim, bytes32 _pubKeyRefund, uint256 _timeoutDuration) payable {
|
||||
owner = payable(msg.sender);
|
||||
pubKeyClaim = _pubKeyClaim;
|
||||
pubKeyRefund = _pubKeyRefund;
|
||||
timeout_0 = block.timestamp + 1 days;
|
||||
timeout_0 = block.timestamp + _timeoutDuration;
|
||||
timeout_1 = block.timestamp + (_timeoutDuration * 2);
|
||||
ed25519 = new Ed25519();
|
||||
emit Constructed(_pubKeyRefund);
|
||||
}
|
||||
|
||||
// Alice must call set_ready() within t_0 once she verifies the XMR has been locked
|
||||
function set_ready() external {
|
||||
require(!isReady && msg.sender == owner && block.timestamp < timeout_0);
|
||||
require(!isReady && msg.sender == owner);
|
||||
isReady = true;
|
||||
timeout_1 = block.timestamp + 1 days;
|
||||
emit IsReady(true);
|
||||
}
|
||||
|
||||
@@ -58,14 +57,8 @@ contract SwapOnChain {
|
||||
// - Alice doesn't call set_ready or refund within t_0, or
|
||||
// - Alice calls ready within t_0, in which case Bob can call claim until t_1
|
||||
function claim(uint256 _s) external {
|
||||
if (isReady == true) {
|
||||
require(block.timestamp < timeout_1, "Too late to claim!");
|
||||
} else {
|
||||
require(
|
||||
block.timestamp >= timeout_0,
|
||||
"'isReady == false' cannot claim yet!"
|
||||
);
|
||||
}
|
||||
require(block.timestamp < timeout_1 && (block.timestamp >= timeout_0 || isReady),
|
||||
"too late or early to claim!");
|
||||
|
||||
verifySecret(_s, pubKeyClaim);
|
||||
emit Claimed(_s);
|
||||
@@ -78,14 +71,10 @@ contract SwapOnChain {
|
||||
// - Until t_0 unless she calls set_ready
|
||||
// - After t_1, if she called set_ready
|
||||
function refund(uint256 _s) external {
|
||||
if (isReady == true) {
|
||||
require(
|
||||
block.timestamp >= timeout_1,
|
||||
"It's Bob's turn now, please wait!"
|
||||
);
|
||||
} else {
|
||||
require(block.timestamp < timeout_0, "Missed your chance!");
|
||||
}
|
||||
require(
|
||||
block.timestamp >= timeout_1 || ( block.timestamp < timeout_0 && !isReady),
|
||||
"It's Bob's turn now, please wait!"
|
||||
);
|
||||
|
||||
verifySecret(_s, pubKeyRefund);
|
||||
emit Refunded(_s);
|
||||
|
||||
1
go.mod
1
go.mod
@@ -88,6 +88,7 @@ require (
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
|
||||
5
go.sum
5
go.sum
@@ -841,8 +841,9 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
@@ -1070,6 +1071,8 @@ github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
|
||||
github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
|
||||
github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||
|
||||
@@ -147,6 +147,7 @@ func (kp *PrivateKeyPair) Marshal() ([]byte, error) {
|
||||
m := make(map[string]string)
|
||||
m["PrivateSpendKey"] = kp.sk.Hex()
|
||||
m["PrivateViewKey"] = kp.vk.Hex()
|
||||
m["Address"] = string(kp.Address())
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -48,3 +50,9 @@ func TestGeneratePrivateKeyPair(t *testing.T) {
|
||||
_, err := GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestKeccak256(t *testing.T) {
|
||||
res := crypto.Keccak256([]byte{1})
|
||||
res2 := Keccak256([]byte{1})
|
||||
require.Equal(t, res, res2[:])
|
||||
}
|
||||
|
||||
44
net/host.go
44
net/host.go
@@ -27,18 +27,18 @@ const (
|
||||
var log = logging.Logger("net")
|
||||
var _ Host = &host{}
|
||||
|
||||
type MessageInfo struct {
|
||||
Message Message
|
||||
Who peer.ID
|
||||
}
|
||||
|
||||
type Host interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
|
||||
Discover(provides ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error)
|
||||
Query(who peer.AddrInfo) (*QueryResponse, error)
|
||||
Initiate(who peer.AddrInfo, msg *InitiateMessage) error
|
||||
Initiate(who peer.AddrInfo, msg *InitiateMessage, s SwapState) error
|
||||
MessageSender
|
||||
}
|
||||
|
||||
type MessageSender interface {
|
||||
SendSwapMessage(Message) error
|
||||
}
|
||||
|
||||
type host struct {
|
||||
@@ -51,6 +51,11 @@ type host struct {
|
||||
discovery *discovery
|
||||
handler Handler
|
||||
|
||||
// swap instance info
|
||||
swapMu sync.Mutex
|
||||
swapState SwapState
|
||||
swapStream libp2pnetwork.Stream
|
||||
|
||||
queryMu sync.Mutex
|
||||
queryBuf []byte
|
||||
}
|
||||
@@ -129,12 +134,7 @@ func NewHost(cfg *Config) (*host, error) {
|
||||
return hst, nil
|
||||
}
|
||||
|
||||
func (h *host) Discover(provides ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error) {
|
||||
return h.discovery.discover(provides, searchTime)
|
||||
}
|
||||
|
||||
func (h *host) Start() error {
|
||||
//h.h.SetStreamHandler(protocolID, h.handleStream)
|
||||
h.h.SetStreamHandler(protocolID+queryID, h.handleQueryStream)
|
||||
h.h.SetStreamHandler(protocolID+subProtocolID, h.handleProtocolStream)
|
||||
|
||||
@@ -171,6 +171,24 @@ func (h *host) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discover searches the DHT for peers that advertise that they provide the given coin.
|
||||
// It searches for up to `searchTime` duration of time.
|
||||
func (h *host) Discover(provides ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error) {
|
||||
return h.discovery.discover(provides, searchTime)
|
||||
}
|
||||
|
||||
// SendSwapMessage sends a message to the peer who we're currently doing a swap with.
|
||||
func (h *host) SendSwapMessage(msg Message) error {
|
||||
h.swapMu.Lock()
|
||||
defer h.swapMu.Unlock()
|
||||
|
||||
if h.swapStream == nil {
|
||||
return errors.New("no swap currently happening")
|
||||
}
|
||||
|
||||
return h.writeToStream(h.swapStream, msg)
|
||||
}
|
||||
|
||||
func (h *host) getBootnodes() []peer.AddrInfo {
|
||||
addrs := h.bootnodes
|
||||
for _, p := range h.h.Network().Peers() {
|
||||
@@ -193,8 +211,6 @@ func (h *host) multiaddrs() (multiaddrs []ma.Multiaddr) {
|
||||
}
|
||||
|
||||
func (h *host) writeToStream(s libp2pnetwork.Stream, msg Message) error {
|
||||
//defer s.Close()
|
||||
|
||||
encMsg, err := msg.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -236,7 +252,7 @@ func readStream(stream libp2pnetwork.Stream, buf []byte) (int, error) {
|
||||
}
|
||||
|
||||
if length == 0 {
|
||||
return 0, nil // msg length of 0 is allowed, for example transactions handshake
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if length > uint64(len(buf)) {
|
||||
|
||||
116
net/initiate.go
116
net/initiate.go
@@ -2,6 +2,7 @@ package net
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -12,8 +13,17 @@ import (
|
||||
// Handler handles incoming protocol messages.
|
||||
// It is implemented by *alice.alice and *bob.bob
|
||||
type Handler interface {
|
||||
HandleInitiateMessage(msg *InitiateMessage) (s SwapState, resp Message, err error)
|
||||
}
|
||||
|
||||
// SwapState handles incoming protocol messages for an initiated protocol.
|
||||
// It is implemented by *alice.swapState and *bob.swapState
|
||||
type SwapState interface {
|
||||
HandleProtocolMessage(msg Message) (resp Message, done bool, err error)
|
||||
ProtocolComplete()
|
||||
|
||||
// used by RPC
|
||||
SendKeysMessage() (*SendKeysMessage, error)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -21,11 +31,59 @@ const (
|
||||
protocolTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
func (h *host) Initiate(who peer.AddrInfo, msg *InitiateMessage, s SwapState) error {
|
||||
h.swapMu.Lock()
|
||||
defer h.swapMu.Unlock()
|
||||
|
||||
// TODO: need a lock for this, otherwise two streams can enter this func
|
||||
if h.swapState != nil {
|
||||
return errors.New("already have ongoing swap")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(h.ctx, protocolTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := h.h.Connect(ctx, who); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream, err := h.h.NewStream(ctx, who.ID, protocolID+subProtocolID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open stream with peer: err=%w", err)
|
||||
}
|
||||
|
||||
log.Debug(
|
||||
"opened protocol stream, peer=", who.ID,
|
||||
)
|
||||
|
||||
if err := h.writeToStream(stream, msg); err != nil {
|
||||
log.Warnf("failed to send InitiateMessage to peer: err=%s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
h.swapState = s
|
||||
h.handleProtocolStreamInner(stream)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleProtocolStream is called when there is an incoming protocol stream.
|
||||
func (h *host) handleProtocolStream(stream libp2pnetwork.Stream) {
|
||||
h.swapMu.Lock()
|
||||
defer h.swapMu.Unlock()
|
||||
|
||||
if h.swapState != nil {
|
||||
log.Debug("failed to handling incoming swap stream, already have ongoing swap")
|
||||
}
|
||||
|
||||
h.handleProtocolStreamInner(stream)
|
||||
}
|
||||
|
||||
// handleProtocolStreamInner is called to handle a protocol stream, in both ingoing and outgoing cases.
|
||||
func (h *host) handleProtocolStreamInner(stream libp2pnetwork.Stream) {
|
||||
defer func() {
|
||||
log.Debugf("closing stream: peer=%s protocol=%s", stream.Conn().RemotePeer(), stream.Protocol())
|
||||
_ = stream.Close()
|
||||
h.handler.ProtocolComplete()
|
||||
h.swapState.ProtocolComplete()
|
||||
}()
|
||||
|
||||
msgBytes := make([]byte, 2048)
|
||||
@@ -48,10 +106,32 @@ func (h *host) handleProtocolStream(stream libp2pnetwork.Stream) {
|
||||
"received message from peer, peer=", stream.Conn().RemotePeer(), " msg=", msg.String(),
|
||||
)
|
||||
|
||||
resp, done, err := h.handler.HandleProtocolMessage(msg)
|
||||
if err != nil {
|
||||
log.Warnf("failed to handle protocol message: err=%s", err)
|
||||
return
|
||||
var (
|
||||
resp Message
|
||||
done bool
|
||||
)
|
||||
|
||||
if h.swapState == nil {
|
||||
im, ok := msg.(*InitiateMessage)
|
||||
if !ok {
|
||||
log.Warnf("failed to handle protocol message: message was not InitiateMessage")
|
||||
return
|
||||
}
|
||||
|
||||
var s SwapState
|
||||
s, resp, err = h.handler.HandleInitiateMessage(im)
|
||||
if err != nil {
|
||||
log.Warnf("failed to handle protocol message: err=%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.swapState = s
|
||||
} else {
|
||||
resp, done, err = h.swapState.HandleProtocolMessage(msg)
|
||||
if err != nil {
|
||||
log.Warnf("failed to handle protocol message: err=%s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
@@ -69,29 +149,3 @@ func (h *host) handleProtocolStream(stream libp2pnetwork.Stream) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *host) Initiate(who peer.AddrInfo, msg *InitiateMessage) error {
|
||||
ctx, cancel := context.WithTimeout(h.ctx, protocolTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := h.h.Connect(ctx, who); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream, err := h.h.NewStream(ctx, who.ID, protocolID+subProtocolID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open stream with peer: err=%w", err)
|
||||
}
|
||||
|
||||
log.Debug(
|
||||
"opened protocol stream, peer=", who.ID,
|
||||
)
|
||||
|
||||
if err := h.writeToStream(stream, msg); err != nil {
|
||||
log.Warnf("failed to send InitiateMessage to peer: err=%s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
h.handleProtocolStream(stream)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -253,10 +253,12 @@ func (m *NotifyClaimed) Type() byte {
|
||||
}
|
||||
|
||||
// NotifyRefund is sent by Alice to Bob after calling Refund() on the contract.
|
||||
type NotifyRefund struct{}
|
||||
type NotifyRefund struct {
|
||||
TxHash string
|
||||
}
|
||||
|
||||
func (m *NotifyRefund) String() string {
|
||||
return "NotifyRefund"
|
||||
return fmt.Sprintf("NotifyClaimed %s", m.TxHash)
|
||||
}
|
||||
|
||||
func (m *NotifyRefund) Encode() ([]byte, error) {
|
||||
|
||||
30
rpc/net.go
30
rpc/net.go
@@ -17,27 +17,22 @@ const defaultSearchTime = time.Second * 12
|
||||
type Net interface {
|
||||
Discover(provides net.ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error)
|
||||
Query(who peer.AddrInfo) (*net.QueryResponse, error)
|
||||
Initiate(who peer.AddrInfo, msg *net.InitiateMessage) error
|
||||
Initiate(who peer.AddrInfo, msg *net.InitiateMessage, s net.SwapState) error
|
||||
}
|
||||
|
||||
type Protocol interface {
|
||||
Provides() net.ProvidesCoin
|
||||
InitiateProtocol(providesAmount, desiredAmount uint64) error
|
||||
SendKeysMessage() (*net.SendKeysMessage, error)
|
||||
|
||||
// TODO: this isn't used here, but in the network package
|
||||
HandleProtocolMessage(msg net.Message) (net.Message, bool, error)
|
||||
ProtocolComplete()
|
||||
InitiateProtocol(providesAmount, desiredAmount uint64) (net.SwapState, error)
|
||||
}
|
||||
|
||||
type NetService struct {
|
||||
backend Net
|
||||
net Net
|
||||
protocol Protocol
|
||||
}
|
||||
|
||||
func NewNetService(net Net, protocol Protocol) *NetService {
|
||||
return &NetService{
|
||||
backend: net,
|
||||
net: net,
|
||||
protocol: protocol,
|
||||
}
|
||||
}
|
||||
@@ -62,7 +57,7 @@ func (s *NetService) Discover(_ *http.Request, req *DiscoverRequest, resp *Disco
|
||||
searchTime = defaultSearchTime
|
||||
}
|
||||
|
||||
peers, err := s.backend.Discover(net.ProvidesCoin(req.Provides), searchTime)
|
||||
peers, err := s.net.Discover(net.ProvidesCoin(req.Provides), searchTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -100,7 +95,7 @@ func (s *NetService) QueryPeer(_ *http.Request, req *QueryPeerRequest, resp *Que
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := s.backend.Query(who)
|
||||
msg, err := s.net.Query(who)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,7 +122,12 @@ func (s *NetService) Initiate(_ *http.Request, req *InitiateRequest, resp *Initi
|
||||
return errors.New("must specify 'provides' coin")
|
||||
}
|
||||
|
||||
skm, err := s.protocol.SendKeysMessage()
|
||||
swapState, err := s.protocol.InitiateProtocol(req.ProvidesAmount, req.DesiredAmount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
skm, err := swapState.SendKeysMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -144,11 +144,7 @@ func (s *NetService) Initiate(_ *http.Request, req *InitiateRequest, resp *Initi
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.protocol.InitiateProtocol(req.ProvidesAmount, req.DesiredAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.backend.Initiate(who, msg); err != nil {
|
||||
if err = s.net.Initiate(who, msg, swapState); err != nil {
|
||||
resp.Success = false
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd cmd && go build -o atomic-swap
|
||||
mv atomic-swap ..
|
||||
cd ..
|
||||
cd cmd/daemon && go build -o swapd
|
||||
mv swapd ../..
|
||||
cd ../client && go build -o swapcli
|
||||
mv swapcli ../..
|
||||
cd ../..
|
||||
File diff suppressed because one or more lines are too long
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/noot/atomic-swap/monero"
|
||||
)
|
||||
|
||||
var defaultTimeoutDuration = big.NewInt(60) // 60 seconds
|
||||
|
||||
func reverse(s []byte) []byte {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
@@ -39,7 +41,7 @@ func TestDeploySwap(t *testing.T) {
|
||||
authAlice, err := bind.NewKeyedTransactorWithChainID(pk_a, big.NewInt(common.GanacheChainID))
|
||||
require.NoError(t, err)
|
||||
|
||||
address, tx, swapContract, err := DeploySwap(authAlice, conn, [32]byte{}, [32]byte{})
|
||||
address, tx, swapContract, err := DeploySwap(authAlice, conn, [32]byte{}, [32]byte{}, defaultTimeoutDuration)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(address)
|
||||
@@ -88,7 +90,7 @@ func TestSwap_Claim(t *testing.T) {
|
||||
copy(pkAliceFixed[:], reverse(pubKeyAlice))
|
||||
var pkBobFixed [32]byte
|
||||
copy(pkBobFixed[:], reverse(pubKeyBob))
|
||||
contractAddress, deployTx, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed)
|
||||
contractAddress, deployTx, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed, defaultTimeoutDuration)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("Deploy Tx Gas Cost:", deployTx.Gas())
|
||||
|
||||
@@ -115,7 +117,7 @@ func TestSwap_Claim(t *testing.T) {
|
||||
fmt.Println("Secret:", hex.EncodeToString(reverse(secretBob)))
|
||||
fmt.Println("PubKey:", hex.EncodeToString(reverse(pubKeyBob)))
|
||||
_, err = swap.Claim(txOptsBob, s)
|
||||
require.Regexp(t, ".*'isReady == false' cannot claim yet!", err)
|
||||
require.Regexp(t, ".*too late or early to claim!", err)
|
||||
|
||||
// Alice calls set_ready on the contract
|
||||
setReadyTx, err := swap.SetReady(txOpts)
|
||||
@@ -175,7 +177,7 @@ func TestSwap_Refund_Within_T0(t *testing.T) {
|
||||
copy(pkAliceFixed[:], reverse(pubKeyAlice))
|
||||
var pkBobFixed [32]byte
|
||||
copy(pkBobFixed[:], reverse(pubKeyBob))
|
||||
contractAddress, _, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed)
|
||||
contractAddress, _, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed, defaultTimeoutDuration)
|
||||
require.NoError(t, err)
|
||||
|
||||
txOpts := &bind.TransactOpts{
|
||||
@@ -230,7 +232,7 @@ func TestSwap_Refund_After_T1(t *testing.T) {
|
||||
copy(pkAliceFixed[:], reverse(pubKeyAlice))
|
||||
var pkBobFixed [32]byte
|
||||
copy(pkBobFixed[:], reverse(pubKeyBob))
|
||||
contractAddress, _, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed)
|
||||
contractAddress, _, swap, err := DeploySwap(authAlice, conn, pkBobFixed, pkAliceFixed, defaultTimeoutDuration)
|
||||
require.NoError(t, err)
|
||||
|
||||
txOpts := &bind.TransactOpts{
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user