ui: metamask integration, swapd: implement external sender for front-end integration (#126)

This commit is contained in:
noot
2022-06-07 18:06:24 -04:00
committed by GitHub
parent 0eb4f73dcc
commit 11e3b27c2e
37 changed files with 9084 additions and 1384 deletions

View File

@@ -288,6 +288,11 @@ func runMake(ctx *cli.Context) error {
fmt.Printf("Made offer with ID=%s\n", id)
taken := <-takenCh
if taken == nil {
fmt.Printf("connection closed\n")
return nil
}
fmt.Printf("Offer taken! Swap ID=%d\n", taken.ID)
for stage := range statusCh {

View File

@@ -2,6 +2,7 @@ package main
import (
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
@@ -15,6 +16,10 @@ import (
"github.com/ethereum/go-ethereum/ethclient"
)
var (
errNoEthereumPrivateKey = errors.New("must provide --ethereum-privkey file for non-development environment")
)
func getOrDeploySwapFactory(address ethcommon.Address, env common.Environment, basepath string, chainID *big.Int,
privkey *ecdsa.PrivateKey, ec *ethclient.Client) (*swapfactory.SwapFactory, ethcommon.Address, error) {
var (
@@ -22,6 +27,10 @@ func getOrDeploySwapFactory(address ethcommon.Address, env common.Environment, b
)
if env != common.Mainnet && (address == ethcommon.Address{}) {
if privkey == nil {
return nil, ethcommon.Address{}, errNoEthereumPrivateKey
}
txOpts, err := bind.NewKeyedTransactorWithChainID(privkey, chainID)
if err != nil {
return nil, ethcommon.Address{}, fmt.Errorf("failed to make transactor: %w", err)

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
@@ -69,6 +70,7 @@ const (
flagContractAddress = "contract-address"
flagGasPrice = "gas-price"
flagGasLimit = "gas-limit"
flagUseExternalSigner = "external-signer"
flagDevXMRTaker = "dev-xmrtaker"
flagDevXMRMaker = "dev-xmrmaker"
@@ -172,6 +174,10 @@ var (
Name: flagLog,
Usage: "set log level: one of [error|warn|info|debug]",
},
&cli.BoolFlag{
Name: flagUseExternalSigner,
Usage: "use external signer, for usage with the swap UI",
},
},
}
)
@@ -406,11 +412,19 @@ func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg
ethEndpoint = common.DefaultEthEndpoint
}
ethPrivKey, err := utils.GetEthereumPrivateKey(c, env, devXMRMaker)
ethPrivKey, err := utils.GetEthereumPrivateKey(c, env, devXMRMaker, c.Bool(flagUseExternalSigner))
if err != nil {
return nil, err
}
var pk *ecdsa.PrivateKey
if ethPrivKey != "" {
pk, err = ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
return nil, err
}
}
if c.String(flagMoneroDaemonEndpoint) != "" {
daemonEndpoint = c.String(flagMoneroDaemonEndpoint)
} else {
@@ -431,11 +445,6 @@ func newBackend(ctx context.Context, c *cli.Context, env common.Environment, cfg
contractAddr = ethcommon.HexToAddress(contractAddrStr)
}
pk, err := ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
return nil, err
}
ec, err := ethclient.Dial(ethEndpoint)
if err != nil {
return nil, err

View File

@@ -263,7 +263,8 @@ func createBackend(ctx context.Context, c *cli.Context, env common.Environment,
ethEndpoint = common.DefaultEthEndpoint
}
ethPrivKey, err := utils.GetEthereumPrivateKey(c, env, false)
// TODO: add --external-signer option to allow front-end integration
ethPrivKey, err := utils.GetEthereumPrivateKey(c, env, false, false)
if err != nil {
return nil, err
}

View File

@@ -27,7 +27,8 @@ var (
)
// GetEthereumPrivateKey returns an ethereum private key hex string given the CLI options.
func GetEthereumPrivateKey(c *cli.Context, env common.Environment, devXMRMaker bool) (ethPrivKey string, err error) {
func GetEthereumPrivateKey(c *cli.Context, env common.Environment, devXMRMaker,
useExternal bool) (ethPrivKey string, err error) {
if c.String(flagEthereumPrivKey) != "" {
ethPrivKeyFile := c.String(flagEthereumPrivKey)
key, err := os.ReadFile(filepath.Clean(ethPrivKeyFile))
@@ -41,9 +42,10 @@ func GetEthereumPrivateKey(c *cli.Context, env common.Environment, devXMRMaker b
ethPrivKey = string(key)
} else {
if env != common.Development {
if env != common.Development || useExternal {
// TODO: allow this to be set via RPC
return "", errNoEthereumPrivateKey
log.Warnf("%s", errNoEthereumPrivateKey)
return "", nil
}
log.Warn("no ethereum private key file provided, using ganache deterministic key")

View File

@@ -61,3 +61,24 @@ type MakeOfferResponse struct {
ID string `json:"offerID"`
InfoFile string `json:"infoFile"`
}
// SignerRequest initiates the signer_subscribe handler from the front-end
type SignerRequest struct {
OfferID string `json:"offerID"`
EthAddress string `json:"ethAddress"`
XMRAddress string `json:"xmrAddress"`
}
// SignerResponse sends a tx to be signed to the front-end
type SignerResponse struct {
OfferID string `json:"offerID"`
To string `json:"to"`
Data string `json:"data"`
Value string `json:"value"`
}
// SignerTxSigned is a response from the front-end saying the given tx has been submitted successfully
type SignerTxSigned struct {
OfferID string `json:"offerID"`
TxHash string `json:"txHash"`
}

View File

@@ -1,6 +1,8 @@
package mcrypto
import (
"fmt"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/crypto"
)
@@ -8,11 +10,25 @@ import (
const (
addressPrefixMainnet byte = 18
addressPrefixStagenet byte = 24
// AddressLength is the length of a Monero address
AddressLength = 1 + 32 + 32 + 4
)
// Address represents a base58-encoded string
type Address string
// ValidateAddress checks if the given address is valid
// TODO: also check chain prefix
func ValidateAddress(addr string) error {
b := DecodeMoneroBase58(addr)
if len(b) != AddressLength {
return fmt.Errorf("invalid monero address length: got %d, expected %d", len(b), AddressLength)
}
return nil
}
func getChecksum(data ...[]byte) (result [4]byte) {
keccak256 := crypto.Keccak256(data...)
copy(result[:], keccak256[:4])

View File

@@ -61,7 +61,7 @@ make build
10. Copy `goerli.key` into this directory. If you are using an Infura Goerli endpoint, copy-paste your API key into the field below following the `--ethereum-endpoint` flag. Otherwise, change `--ethereum-endpoint` to point to your endpoint. Finally, start the `swapd` atomic swap daemon process:
```bash
./swapd --env stagenet --ethereum-privkey=goerli.key --monero-endpoint=http://localhost:18083/json_rpc --wallet-file=stagenet-wallet --ethereum-endpoint=https://goerli.infura.io/v3/<your-api-key> --ethereum-chain-id=5 --contract-address=0xe532f0C720dCD102854281aeF1a8Be01f464C8fE --bootnodes /ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5,/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWSc4yFkPWBFmPToTMbhChH3FAgGH96DNzSg5fio1pQYoN,/ip4/67.207.89.83/tcp/9900/p2p/12D3KooWLbfkLZZvvn8Lxs1KDU3u7gyvBk88ZNtJBbugytBr5RCG,/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5,/ip4/164.92.103.160/tcp/9900/p2p/12D3KooWAZtRECEv7zN69zU1e7sPrHbMgfqFUn7QTLh1pKGiMuaM,/ip4/164.92.103.159/tcp/9900/p2p/12D3KooWSNQF1eNyapxC2zA3jJExgLX7jWhEyw8B3k7zMW5ZRvQz,/ip4/164.92.123.10/tcp/9900/p2p/12D3KooWG8z9fXVTB72XL8hQbahpfEjutREL9vbBQ4FzqtDKzTBu,/ip4/161.35.110.210/tcp/9900/p2p/12D3KooWS8iKxqsGTiL3Yc1VaAfg99U5km1AE7bWYQiuavXj3Yz6,/ip4/206.189.47.220/tcp/9900/p2p/12D3KooWGVzz2d2LSceVFFdqTYqmQXTqc5eWziw7PLRahCWGJhKB --rpc-port=5001
./swapd --env stagenet --ethereum-privkey=goerli.key --monero-endpoint=http://localhost:18083/json_rpc --wallet-file=stagenet-wallet --ethereum-endpoint=https://goerli.infura.io/v3/<your-api-key> --ethereum-chain-id=5 --contract-address=0x0adc492ADe62c4BbE8c517D4B735B5268Bbf0552 --bootnodes /ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5,/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWSc4yFkPWBFmPToTMbhChH3FAgGH96DNzSg5fio1pQYoN,/ip4/67.207.89.83/tcp/9900/p2p/12D3KooWLbfkLZZvvn8Lxs1KDU3u7gyvBk88ZNtJBbugytBr5RCG,/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5,/ip4/164.92.103.160/tcp/9900/p2p/12D3KooWAZtRECEv7zN69zU1e7sPrHbMgfqFUn7QTLh1pKGiMuaM,/ip4/164.92.103.159/tcp/9900/p2p/12D3KooWSNQF1eNyapxC2zA3jJExgLX7jWhEyw8B3k7zMW5ZRvQz,/ip4/164.92.123.10/tcp/9900/p2p/12D3KooWG8z9fXVTB72XL8hQbahpfEjutREL9vbBQ4FzqtDKzTBu,/ip4/161.35.110.210/tcp/9900/p2p/12D3KooWS8iKxqsGTiL3Yc1VaAfg99U5km1AE7bWYQiuavXj3Yz6,/ip4/206.189.47.220/tcp/9900/p2p/12D3KooWGVzz2d2LSceVFFdqTYqmQXTqc5eWziw7PLRahCWGJhKB --rpc-port=5001
```
> Note: please also see the [RPC documentation](./rpc.md) for complete documentation on available RPC calls and their parameters.

View File

@@ -192,8 +192,8 @@ func (h *host) logPeers() {
return
}
log.Infof("peer count: %d", len(h.h.Network().Peers()))
time.Sleep(time.Second * 30)
log.Debugf("peer count: %d", len(h.h.Network().Peers()))
time.Sleep(time.Minute)
}
}

View File

@@ -13,9 +13,11 @@ import (
"github.com/ethereum/go-ethereum/ethclient"
"github.com/noot/atomic-swap/common"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/monero"
"github.com/noot/atomic-swap/net"
"github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/protocol/txsender"
"github.com/noot/atomic-swap/swapfactory"
logging "github.com/ipfs/go-log"
@@ -38,6 +40,7 @@ type Backend interface {
monero.Client
monero.DaemonClient
net.MessageSender
txsender.Sender
// ethclient methods
BalanceAt(ctx context.Context, account ethcommon.Address, blockNumber *big.Int) (*big.Int, error)
@@ -61,10 +64,16 @@ type Backend interface {
ContractAddr() ethcommon.Address
Net() net.MessageSender
SwapTimeout() time.Duration
ExternalSender() *txsender.ExternalSender
XMRDepositAddress() mcrypto.Address
// setters
SetSwapTimeout(timeout time.Duration)
SetGasPrice(uint64)
SetEthAddress(ethcommon.Address)
SetXMRDepositAddress(mcrypto.Address)
SetContract(*swapfactory.SwapFactory)
SetContractAddress(ethcommon.Address)
}
type backend struct {
@@ -76,6 +85,9 @@ type backend struct {
monero.Client
monero.DaemonClient
// monero deposit address (used if xmrtaker has transferBack set to true)
xmrDepositAddr mcrypto.Address
// ethereum endpoint and variables
ethClient *ethclient.Client
ethPrivKey *ecdsa.PrivateKey
@@ -84,6 +96,7 @@ type backend struct {
chainID *big.Int
gasPrice *big.Int
gasLimit uint64
txsender.Sender
// swap contract
contract *swapfactory.SwapFactory
@@ -127,7 +140,26 @@ func NewBackend(cfg *Config) (Backend, error) {
defaultTimeoutDuration = time.Hour
}
addr := common.EthereumPrivateKeyToAddress(cfg.EthereumPrivateKey)
var (
addr ethcommon.Address
sender txsender.Sender
)
if cfg.EthereumPrivateKey != nil {
txOpts, err := bind.NewKeyedTransactorWithChainID(cfg.EthereumPrivateKey, cfg.ChainID)
if err != nil {
return nil, err
}
addr = common.EthereumPrivateKeyToAddress(cfg.EthereumPrivateKey)
sender = txsender.NewSenderWithPrivateKey(cfg.Ctx, cfg.EthereumClient, cfg.SwapContract, txOpts)
} else {
log.Debugf("instantiated backend with external sender")
var err error
sender, err = txsender.NewExternalSender(cfg.Ctx, cfg.EthereumClient, cfg.SwapContractAddress)
if err != nil {
return nil, err
}
}
// monero-wallet-rpc client
walletClient := monero.NewClient(cfg.MoneroWalletEndpoint)
@@ -153,6 +185,7 @@ func NewBackend(cfg *Config) (Backend, error) {
From: addr,
Context: cfg.Ctx,
},
Sender: sender,
ethAddress: addr,
chainID: cfg.ChainID,
gasPrice: cfg.GasPrice,
@@ -197,6 +230,15 @@ func (b *backend) EthClient() *ethclient.Client {
return b.ethClient
}
func (b *backend) ExternalSender() *txsender.ExternalSender {
s, ok := b.Sender.(*txsender.ExternalSender)
if !ok {
return nil
}
return s
}
func (b *backend) Net() net.MessageSender {
return b.MessageSender
}
@@ -247,6 +289,10 @@ func (b *backend) TxOpts() (*bind.TransactOpts, error) {
return txOpts, nil
}
func (b *backend) XMRDepositAddress() mcrypto.Address {
return b.xmrDepositAddr
}
// WaitForReceipt waits for the receipt for the given transaction to be available and returns it.
func (b *backend) WaitForReceipt(ctx context.Context, txHash ethcommon.Hash) (*types.Receipt, error) {
for i := 0; i < maxRetries; i++ {
@@ -272,3 +318,24 @@ func (b *backend) WaitForReceipt(ctx context.Context, txHash ethcommon.Hash) (*t
func (b *backend) NewSwapFactory(addr ethcommon.Address) (*swapfactory.SwapFactory, error) {
return swapfactory.NewSwapFactory(addr, b.ethClient)
}
func (b *backend) SetEthAddress(addr ethcommon.Address) {
if b.ExternalSender() == nil {
return
}
b.ethAddress = addr
}
func (b *backend) SetXMRDepositAddress(addr mcrypto.Address) {
b.xmrDepositAddr = addr
}
func (b *backend) SetContract(contract *swapfactory.SwapFactory) {
b.contract = contract
b.Sender.SetContract(contract)
}
func (b *backend) SetContractAddress(addr ethcommon.Address) {
b.contractAddr = addr
}

View File

@@ -0,0 +1,164 @@
package txsender
import (
"context"
"errors"
"fmt"
"math/big"
"time"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/swapfactory"
"github.com/ethereum/go-ethereum/accounts/abi"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
var (
errTransactionTimeout = errors.New("timed out waiting for transaction to be signed")
transactionTimeout = time.Minute * 2 // arbitrary, TODO vary this based on env
)
// Transaction represents a transaction to be signed by the front-end
type Transaction struct {
To ethcommon.Address
Data string
Value string
}
// ExternalSender represents a transaction signer and sender that is external to the daemon (ie. a front-end)
type ExternalSender struct {
ctx context.Context
ec *ethclient.Client
abi *abi.ABI
contractAddr ethcommon.Address
// outgoing encoded txs to be signed
out chan *Transaction
// incoming tx hashes
in chan ethcommon.Hash
}
// NewExternalSender returns a new ExternalSender
func NewExternalSender(ctx context.Context, ec *ethclient.Client,
contractAddr ethcommon.Address) (*ExternalSender, error) {
abi, err := swapfactory.SwapFactoryMetaData.GetAbi()
if err != nil {
return nil, err
}
return &ExternalSender{
ctx: ctx,
ec: ec,
abi: abi,
contractAddr: contractAddr,
out: make(chan *Transaction),
in: make(chan ethcommon.Hash),
}, nil
}
// SetContract ...
func (s *ExternalSender) SetContract(_ *swapfactory.SwapFactory) {}
// SetContractAddress ...
func (s *ExternalSender) SetContractAddress(addr ethcommon.Address) {
s.contractAddr = addr
}
// OngoingCh returns the channel of outgoing transactions to be signed and submitted
func (s *ExternalSender) OngoingCh() <-chan *Transaction {
return s.out
}
// IncomingCh returns the channel of incoming transaction hashes that have been signed and submitted
func (s *ExternalSender) IncomingCh() chan<- ethcommon.Hash {
return s.in
}
// NewSwap prompts the external sender to sign a new_swap transaction
func (s *ExternalSender) NewSwap(_pubKeyClaim [32]byte, _pubKeyRefund [32]byte, _claimer ethcommon.Address,
_timeoutDuration *big.Int, _nonce *big.Int, value *big.Int) (ethcommon.Hash, *ethtypes.Receipt, error) {
input, err := s.abi.Pack("new_swap", _pubKeyClaim, _pubKeyRefund, _claimer, _timeoutDuration, _nonce)
if err != nil {
return ethcommon.Hash{}, nil, err
}
tx := &Transaction{
To: s.contractAddr,
Data: fmt.Sprintf("0x%x", input),
Value: fmt.Sprintf("%v", common.EtherAmount(*value).AsEther()),
}
s.out <- tx
var txHash ethcommon.Hash
select {
case <-time.After(transactionTimeout):
return ethcommon.Hash{}, nil, errTransactionTimeout
case txHash = <-s.in:
}
receipt, err := waitForReceipt(s.ctx, s.ec, txHash)
if err != nil {
return ethcommon.Hash{}, nil, err
}
return txHash, receipt, nil
}
// SetReady prompts the external sender to sign a set_ready transaction
func (s *ExternalSender) SetReady(_swap swapfactory.SwapFactorySwap) (ethcommon.Hash, *ethtypes.Receipt, error) {
input, err := s.abi.Pack("set_ready", _swap)
if err != nil {
return ethcommon.Hash{}, nil, err
}
return s.sendAndReceive(input)
}
// Claim prompts the external sender to sign a claim transaction
func (s *ExternalSender) Claim(_swap swapfactory.SwapFactorySwap,
_s [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error) {
input, err := s.abi.Pack("claim", _swap, _s)
if err != nil {
return ethcommon.Hash{}, nil, err
}
return s.sendAndReceive(input)
}
// Refund prompts the external sender to sign a refund transaction
func (s *ExternalSender) Refund(_swap swapfactory.SwapFactorySwap,
_s [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error) {
input, err := s.abi.Pack("refund", _swap, _s)
if err != nil {
return ethcommon.Hash{}, nil, err
}
return s.sendAndReceive(input)
}
func (s *ExternalSender) sendAndReceive(input []byte) (ethcommon.Hash, *ethtypes.Receipt, error) {
tx := &Transaction{
To: s.contractAddr,
Data: fmt.Sprintf("0x%x", input),
}
s.out <- tx
var txHash ethcommon.Hash
select {
case <-time.After(transactionTimeout):
return ethcommon.Hash{}, nil, errTransactionTimeout
case txHash = <-s.in:
}
receipt, err := waitForReceipt(s.ctx, s.ec, txHash)
if err != nil {
return ethcommon.Hash{}, nil, err
}
return txHash, receipt, nil
}

137
protocol/txsender/sender.go Normal file
View File

@@ -0,0 +1,137 @@
package txsender
import (
"context"
"errors"
"math/big"
"time"
"github.com/noot/atomic-swap/swapfactory"
"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/ethclient"
)
const (
maxRetries = 360
receiptSleepDuration = time.Second * 10
)
var (
errReceiptTimeOut = errors.New("failed to get receipt, timed out")
)
// Sender signs and submits transactions to the chain
type Sender interface {
SetContract(*swapfactory.SwapFactory)
SetContractAddress(ethcommon.Address)
NewSwap(_pubKeyClaim [32]byte, _pubKeyRefund [32]byte, _claimer ethcommon.Address, _timeoutDuration *big.Int,
_nonce *big.Int, amount *big.Int) (ethcommon.Hash, *ethtypes.Receipt, error)
SetReady(_swap swapfactory.SwapFactorySwap) (ethcommon.Hash, *ethtypes.Receipt, error)
Claim(_swap swapfactory.SwapFactorySwap, _s [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error)
Refund(_swap swapfactory.SwapFactorySwap, _s [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error)
}
type privateKeySender struct {
ctx context.Context
ec *ethclient.Client
contract *swapfactory.SwapFactory
txOpts *bind.TransactOpts
}
// NewSenderWithPrivateKey returns a new *privateKeySender
func NewSenderWithPrivateKey(ctx context.Context, ec *ethclient.Client, contract *swapfactory.SwapFactory,
txOpts *bind.TransactOpts) Sender {
return &privateKeySender{
ctx: ctx,
ec: ec,
contract: contract,
txOpts: txOpts,
}
}
func (s *privateKeySender) SetContract(contract *swapfactory.SwapFactory) {
s.contract = contract
}
func (s *privateKeySender) SetContractAddress(_ ethcommon.Address) {}
func (s *privateKeySender) NewSwap(_pubKeyClaim [32]byte, _pubKeyRefund [32]byte, _claimer ethcommon.Address,
_timeoutDuration *big.Int, _nonce *big.Int, value *big.Int) (ethcommon.Hash, *ethtypes.Receipt, error) {
s.txOpts.Value = value
defer func() {
s.txOpts.Value = nil
}()
tx, err := s.contract.NewSwap(s.txOpts, _pubKeyClaim, _pubKeyRefund, _claimer, _timeoutDuration, _nonce)
if err != nil {
return ethcommon.Hash{}, nil, err
}
receipt, err := waitForReceipt(s.ctx, s.ec, tx.Hash())
if err != nil {
return ethcommon.Hash{}, nil, err
}
return tx.Hash(), receipt, nil
}
func (s *privateKeySender) SetReady(_swap swapfactory.SwapFactorySwap) (ethcommon.Hash, *ethtypes.Receipt, error) {
tx, err := s.contract.SetReady(s.txOpts, _swap)
if err != nil {
return ethcommon.Hash{}, nil, err
}
receipt, err := waitForReceipt(s.ctx, s.ec, tx.Hash())
if err != nil {
return ethcommon.Hash{}, nil, err
}
return tx.Hash(), receipt, nil
}
func (s *privateKeySender) Claim(_swap swapfactory.SwapFactorySwap,
_s [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error) {
tx, err := s.contract.Claim(s.txOpts, _swap, _s)
if err != nil {
return ethcommon.Hash{}, nil, err
}
receipt, err := waitForReceipt(s.ctx, s.ec, tx.Hash())
if err != nil {
return ethcommon.Hash{}, nil, err
}
return tx.Hash(), receipt, nil
}
func (s *privateKeySender) Refund(_swap swapfactory.SwapFactorySwap,
_s [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error) {
tx, err := s.contract.Refund(s.txOpts, _swap, _s)
if err != nil {
return ethcommon.Hash{}, nil, err
}
receipt, err := waitForReceipt(s.ctx, s.ec, tx.Hash())
if err != nil {
return ethcommon.Hash{}, nil, err
}
return tx.Hash(), receipt, nil
}
func waitForReceipt(ctx context.Context, ec *ethclient.Client, txHash ethcommon.Hash) (*ethtypes.Receipt, error) {
for i := 0; i < maxRetries; i++ {
receipt, err := ec.TransactionReceipt(ctx, txHash)
if err != nil {
time.Sleep(receiptSleepDuration)
continue
}
return receipt, nil
}
return nil, errReceiptTimeOut
}

View File

@@ -150,7 +150,7 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (net.Mes
}
contractAddr := ethcommon.HexToAddress(msg.Address)
if err := checkContractCode(s.ctx, s.backend, contractAddr); err != nil {
if err := checkContractCode(s.ctx, s, contractAddr); err != nil {
return nil, err
}
@@ -208,7 +208,7 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (net.Mes
s.clearNextExpectedMessage(types.CompletedSuccess)
// send *message.NotifyClaimed
if err := s.backend.SendSwapMessage(&message.NotifyClaimed{
if err := s.SendSwapMessage(&message.NotifyClaimed{
TxHash: txHash.String(),
}); err != nil {
log.Errorf("failed to send NotifyClaimed message: err=%s", err)
@@ -244,7 +244,7 @@ func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) error {
}
func (s *swapState) handleRefund(txHash string) (mcrypto.Address, error) {
receipt, err := s.backend.TransactionReceipt(s.ctx, ethcommon.HexToHash(txHash))
receipt, err := s.TransactionReceipt(s.ctx, ethcommon.HexToHash(txHash))
if err != nil {
return "", err
}

View File

@@ -21,6 +21,7 @@ import (
net "github.com/noot/atomic-swap/net"
message "github.com/noot/atomic-swap/net/message"
swap "github.com/noot/atomic-swap/protocol/swap"
txsender "github.com/noot/atomic-swap/protocol/txsender"
swapfactory "github.com/noot/atomic-swap/swapfactory"
)
@@ -90,6 +91,22 @@ func (mr *MockBackendMockRecorder) ChainID() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainID", reflect.TypeOf((*MockBackend)(nil).ChainID))
}
// Claim mocks base method.
func (m *MockBackend) Claim(arg0 swapfactory.SwapFactorySwap, arg1 [32]byte) (common.Hash, *types.Receipt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Claim", arg0, arg1)
ret0, _ := ret[0].(common.Hash)
ret1, _ := ret[1].(*types.Receipt)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Claim indicates an expected call of Claim.
func (mr *MockBackendMockRecorder) Claim(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Claim", reflect.TypeOf((*MockBackend)(nil).Claim), arg0, arg1)
}
// CloseWallet mocks base method.
func (m *MockBackend) CloseWallet() error {
m.ctrl.T.Helper()
@@ -203,6 +220,20 @@ func (mr *MockBackendMockRecorder) EthAddress() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthAddress", reflect.TypeOf((*MockBackend)(nil).EthAddress))
}
// ExternalSender mocks base method.
func (m *MockBackend) ExternalSender() *txsender.ExternalSender {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExternalSender")
ret0, _ := ret[0].(*txsender.ExternalSender)
return ret0
}
// ExternalSender indicates an expected call of ExternalSender.
func (mr *MockBackendMockRecorder) ExternalSender() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalSender", reflect.TypeOf((*MockBackend)(nil).ExternalSender))
}
// FilterLogs mocks base method.
func (m *MockBackend) FilterLogs(arg0 context.Context, arg1 ethereum.FilterQuery) ([]types.Log, error) {
m.ctrl.T.Helper()
@@ -334,6 +365,22 @@ func (mr *MockBackendMockRecorder) Net() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Net", reflect.TypeOf((*MockBackend)(nil).Net))
}
// NewSwap mocks base method.
func (m *MockBackend) NewSwap(arg0, arg1 [32]byte, arg2 common.Address, arg3, arg4, arg5 *big.Int) (common.Hash, *types.Receipt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewSwap", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].(common.Hash)
ret1, _ := ret[1].(*types.Receipt)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// NewSwap indicates an expected call of NewSwap.
func (mr *MockBackendMockRecorder) NewSwap(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSwap", reflect.TypeOf((*MockBackend)(nil).NewSwap), arg0, arg1, arg2, arg3, arg4, arg5)
}
// NewSwapFactory mocks base method.
func (m *MockBackend) NewSwapFactory(arg0 common.Address) (*swapfactory.SwapFactory, error) {
m.ctrl.T.Helper()
@@ -377,6 +424,22 @@ func (mr *MockBackendMockRecorder) Refresh() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockBackend)(nil).Refresh))
}
// Refund mocks base method.
func (m *MockBackend) Refund(arg0 swapfactory.SwapFactorySwap, arg1 [32]byte) (common.Hash, *types.Receipt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refund", arg0, arg1)
ret0, _ := ret[0].(common.Hash)
ret1, _ := ret[1].(*types.Receipt)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Refund indicates an expected call of Refund.
func (mr *MockBackendMockRecorder) Refund(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refund", reflect.TypeOf((*MockBackend)(nil).Refund), arg0, arg1)
}
// SendSwapMessage mocks base method.
func (m *MockBackend) SendSwapMessage(arg0 message.Message) error {
m.ctrl.T.Helper()
@@ -391,6 +454,42 @@ func (mr *MockBackendMockRecorder) SendSwapMessage(arg0 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSwapMessage", reflect.TypeOf((*MockBackend)(nil).SendSwapMessage), arg0)
}
// SetContract mocks base method.
func (m *MockBackend) SetContract(arg0 *swapfactory.SwapFactory) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetContract", arg0)
}
// SetContract indicates an expected call of SetContract.
func (mr *MockBackendMockRecorder) SetContract(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetContract", reflect.TypeOf((*MockBackend)(nil).SetContract), arg0)
}
// SetContractAddress mocks base method.
func (m *MockBackend) SetContractAddress(arg0 common.Address) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetContractAddress", arg0)
}
// SetContractAddress indicates an expected call of SetContractAddress.
func (mr *MockBackendMockRecorder) SetContractAddress(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetContractAddress", reflect.TypeOf((*MockBackend)(nil).SetContractAddress), arg0)
}
// SetEthAddress mocks base method.
func (m *MockBackend) SetEthAddress(arg0 common.Address) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetEthAddress", arg0)
}
// SetEthAddress indicates an expected call of SetEthAddress.
func (mr *MockBackendMockRecorder) SetEthAddress(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEthAddress", reflect.TypeOf((*MockBackend)(nil).SetEthAddress), arg0)
}
// SetGasPrice mocks base method.
func (m *MockBackend) SetGasPrice(arg0 uint64) {
m.ctrl.T.Helper()
@@ -403,6 +502,22 @@ func (mr *MockBackendMockRecorder) SetGasPrice(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGasPrice", reflect.TypeOf((*MockBackend)(nil).SetGasPrice), arg0)
}
// SetReady mocks base method.
func (m *MockBackend) SetReady(arg0 swapfactory.SwapFactorySwap) (common.Hash, *types.Receipt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetReady", arg0)
ret0, _ := ret[0].(common.Hash)
ret1, _ := ret[1].(*types.Receipt)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// SetReady indicates an expected call of SetReady.
func (mr *MockBackendMockRecorder) SetReady(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReady", reflect.TypeOf((*MockBackend)(nil).SetReady), arg0)
}
// SetSwapTimeout mocks base method.
func (m *MockBackend) SetSwapTimeout(arg0 time.Duration) {
m.ctrl.T.Helper()
@@ -415,6 +530,18 @@ func (mr *MockBackendMockRecorder) SetSwapTimeout(arg0 interface{}) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSwapTimeout", reflect.TypeOf((*MockBackend)(nil).SetSwapTimeout), arg0)
}
// SetXMRDepositAddress mocks base method.
func (m *MockBackend) SetXMRDepositAddress(arg0 mcrypto.Address) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetXMRDepositAddress", arg0)
}
// SetXMRDepositAddress indicates an expected call of SetXMRDepositAddress.
func (mr *MockBackendMockRecorder) SetXMRDepositAddress(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetXMRDepositAddress", reflect.TypeOf((*MockBackend)(nil).SetXMRDepositAddress), arg0)
}
// SwapManager mocks base method.
func (m *MockBackend) SwapManager() swap.Manager {
m.ctrl.T.Helper()
@@ -517,3 +644,17 @@ func (mr *MockBackendMockRecorder) WaitForReceipt(arg0, arg1 interface{}) *gomoc
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForReceipt", reflect.TypeOf((*MockBackend)(nil).WaitForReceipt), arg0, arg1)
}
// XMRDepositAddress mocks base method.
func (m *MockBackend) XMRDepositAddress() mcrypto.Address {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XMRDepositAddress")
ret0, _ := ret[0].(mcrypto.Address)
return ret0
}
// XMRDepositAddress indicates an expected call of XMRDepositAddress.
func (mr *MockBackendMockRecorder) XMRDepositAddress() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XMRDepositAddress", reflect.TypeOf((*MockBackend)(nil).XMRDepositAddress))
}

View File

@@ -44,7 +44,7 @@ func NewRecoveryState(b backend.Backend, basepath string, secret *mcrypto.Privat
s := &swapState{
ctx: ctx,
cancel: cancel,
backend: b,
Backend: b,
txOpts: txOpts,
privkeys: kp,
pubkeys: pubkp,

View File

@@ -23,7 +23,7 @@ func newTestRecoveryState(t *testing.T) *recoveryState {
require.NoError(t, err)
newSwap(t, s, [32]byte{}, sr, big.NewInt(1), duration)
rs, err := NewRecoveryState(inst.backend, "/tmp/test-infofile", s.privkeys.SpendKey(), s.backend.ContractAddr(),
rs, err := NewRecoveryState(inst.backend, "/tmp/test-infofile", s.privkeys.SpendKey(), s.ContractAddr(),
s.contractSwapID, s.contractSwap)
require.NoError(t, err)
@@ -35,7 +35,7 @@ func TestClaimOrRecover_Claim(t *testing.T) {
rs := newTestRecoveryState(t)
// set contract to Ready
_, err := rs.ss.contract.SetReady(rs.ss.txOpts, rs.ss.contractSwap)
_, err := rs.ss.Contract().SetReady(rs.ss.txOpts, rs.ss.contractSwap)
require.NoError(t, err)
// assert we can claim ether
@@ -53,7 +53,7 @@ func TestClaimOrRecover_Recover(t *testing.T) {
rs := newTestRecoveryState(t)
daemonClient := monero.NewClient(common.DefaultMoneroDaemonEndpoint)
addr, err := rs.ss.backend.GetAddress(0)
addr, err := rs.ss.GetAddress(0)
require.NoError(t, err)
_ = daemonClient.GenerateBlocks(addr.Address, 121)
@@ -64,7 +64,7 @@ func TestClaimOrRecover_Recover(t *testing.T) {
// call refund w/ XMRTaker's spend key
sc := rs.ss.getSecret()
_, err = rs.ss.contract.Refund(rs.ss.txOpts, rs.ss.contractSwap, sc)
_, err = rs.ss.Contract().Refund(rs.ss.txOpts, rs.ss.contractSwap, sc)
require.NoError(t, err)
// assert XMRMaker can reclaim his monero

View File

@@ -39,7 +39,7 @@ var (
)
type swapState struct {
backend backend.Backend
backend.Backend
ctx context.Context
cancel context.CancelFunc
@@ -58,10 +58,8 @@ type swapState struct {
pubkeys *mcrypto.PublicKeyPair
// swap contract and timeouts in it; set once contract is deployed
contract *swapfactory.SwapFactory
contractSwapID [32]byte
contractSwap swapfactory.SwapFactorySwap
contractAddr ethcommon.Address
t0, t1 time.Time
txOpts *bind.TransactOpts
@@ -104,7 +102,7 @@ func newSwapState(b backend.Backend, offer *types.Offer, om *offerManager, statu
s := &swapState{
ctx: ctx,
cancel: cancel,
backend: b,
Backend: b,
offer: offer,
offerManager: om,
infofile: infofile,
@@ -131,7 +129,7 @@ func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
PrivateViewKey: s.privkeys.ViewKey().Hex(),
DLEqProof: hex.EncodeToString(s.dleqProof.Proof()),
Secp256k1PublicKey: s.secp256k1Pub.String(),
EthAddress: s.backend.EthAddress().String(),
EthAddress: s.EthAddress().String(),
}, nil
}
@@ -179,7 +177,7 @@ func (s *swapState) exit() error {
defer func() {
// stop all running goroutines
s.cancel()
s.backend.SwapManager().CompleteOngoingSwap()
s.SwapManager().CompleteOngoingSwap()
if s.info.Status() != types.CompletedSuccess {
// re-add offer, as it wasn't taken successfully
@@ -271,19 +269,19 @@ func (s *swapState) reclaimMonero(skA *mcrypto.PrivateSpendKey) (mcrypto.Address
kpAB := mcrypto.NewPrivateKeyPair(skAB, vkAB)
// write keys to file in case something goes wrong
if err = pcommon.WriteSharedSwapKeyPairToFile(s.infofile, kpAB, s.backend.Env()); err != nil {
if err = pcommon.WriteSharedSwapKeyPairToFile(s.infofile, kpAB, s.Env()); err != nil {
return "", err
}
// TODO: check balance
return monero.CreateMoneroWallet("xmrmaker-swap-wallet", s.backend.Env(), s.backend, kpAB)
return monero.CreateMoneroWallet("xmrmaker-swap-wallet", s.Env(), s, kpAB)
}
func (s *swapState) filterForRefund() (*mcrypto.PrivateSpendKey, error) {
const refundedEvent = "Refunded"
logs, err := s.backend.FilterLogs(s.ctx, eth.FilterQuery{
Addresses: []ethcommon.Address{s.backend.ContractAddr()},
logs, err := s.FilterLogs(s.ctx, eth.FilterQuery{
Addresses: []ethcommon.Address{s.ContractAddr()},
Topics: [][]ethcommon.Hash{{refundedTopic}},
})
if err != nil {
@@ -327,7 +325,7 @@ func (s *swapState) filterForRefund() (*mcrypto.PrivateSpendKey, error) {
func (s *swapState) tryClaim() (ethcommon.Hash, error) {
untilT0 := time.Until(s.t0)
untilT1 := time.Until(s.t1)
stage, err := s.backend.Contract().Swaps(s.backend.CallOpts(), s.contractSwapID)
stage, err := s.Contract().Swaps(s.CallOpts(), s.contractSwapID)
if err != nil {
return ethcommon.Hash{}, err
}
@@ -369,7 +367,7 @@ func (s *swapState) generateAndSetKeys() error {
s.privkeys = keysAndProof.PrivateKeyPair
s.pubkeys = keysAndProof.PublicKeyPair
return pcommon.WriteKeysToFile(s.infofile, s.privkeys, s.backend.Env())
return pcommon.WriteKeysToFile(s.infofile, s.privkeys, s.Env())
}
func generateKeys() (*pcommon.KeysAndProof, error) {
@@ -393,10 +391,15 @@ func (s *swapState) setXMRTakerPublicKeys(sk *mcrypto.PublicKeyPair, secp256k1Pu
// setContract sets the contract in which XMRTaker has locked her ETH.
func (s *swapState) setContract(address ethcommon.Address) error {
var err error
// TODO: this overrides the backend contract, need to be careful
s.contractAddr = address
s.contract, err = s.backend.NewSwapFactory(address)
return err
// note: this overrides the backend contract
s.SetContractAddress(address)
contract, err := s.NewSwapFactory(address)
if err != nil {
return err
}
s.SetContract(contract)
return nil
}
func (s *swapState) setTimeouts(t0, t1 *big.Int) {
@@ -408,7 +411,7 @@ func (s *swapState) setTimeouts(t0, t1 *big.Int) {
// if the balance doesn't match what we're expecting to receive, or the public keys in the contract
// aren't what we expect, we error and abort the swap.
func (s *swapState) checkContract(txHash ethcommon.Hash) error {
receipt, err := s.backend.WaitForReceipt(s.ctx, txHash)
receipt, err := s.WaitForReceipt(s.ctx, txHash)
if err != nil {
return fmt.Errorf("failed to get receipt for New transaction: %w", err)
}
@@ -418,7 +421,7 @@ func (s *swapState) checkContract(txHash ethcommon.Hash) error {
return errCannotFindNewLog
}
event, err := s.contract.ParseNew(*receipt.Logs[0])
event, err := s.Contract().ParseNew(*receipt.Logs[0])
if err != nil {
return err
}
@@ -458,7 +461,7 @@ func (s *swapState) lockFunds(amount common.MoneroAmount) (mcrypto.Address, erro
kp := mcrypto.SumSpendAndViewKeys(s.xmrtakerPublicKeys, s.pubkeys)
log.Infof("going to lock XMR funds, amount(piconero)=%d", amount)
balance, err := s.backend.GetBalance(0)
balance, err := s.GetBalance(0)
if err != nil {
return "", err
}
@@ -466,25 +469,25 @@ func (s *swapState) lockFunds(amount common.MoneroAmount) (mcrypto.Address, erro
log.Debug("total XMR balance: ", balance.Balance)
log.Info("unlocked XMR balance: ", balance.UnlockedBalance)
address := kp.Address(s.backend.Env())
txResp, err := s.backend.Transfer(address, 0, uint(amount))
address := kp.Address(s.Env())
txResp, err := s.Transfer(address, 0, uint(amount))
if err != nil {
return "", err
}
log.Infof("locked XMR, txHash=%s fee=%d", txResp.TxHash, txResp.Fee)
xmrmakerAddr, err := s.backend.GetAddress(0)
xmrmakerAddr, err := s.GetAddress(0)
if err != nil {
return "", err
}
// if we're on a development --regtest node, generate some blocks
if s.backend.Env() == common.Development {
_ = s.backend.GenerateBlocks(xmrmakerAddr.Address, 2)
if s.Env() == common.Development {
_ = s.GenerateBlocks(xmrmakerAddr.Address, 2)
} else {
// otherwise, wait for new blocks
height, err := monero.WaitForBlocks(s.backend, 1)
height, err := monero.WaitForBlocks(s, 1)
if err != nil {
return "", err
}
@@ -492,7 +495,7 @@ func (s *swapState) lockFunds(amount common.MoneroAmount) (mcrypto.Address, erro
log.Infof("monero block height: %d", height)
}
if err := s.backend.Refresh(); err != nil {
if err := s.Refresh(); err != nil {
return "", err
}
@@ -502,9 +505,9 @@ func (s *swapState) lockFunds(amount common.MoneroAmount) (mcrypto.Address, erro
// claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract
func (s *swapState) claimFunds() (ethcommon.Hash, error) {
addr := s.backend.EthAddress()
addr := s.EthAddress()
balance, err := s.backend.BalanceAt(s.ctx, addr, nil)
balance, err := s.BalanceAt(s.ctx, addr, nil)
if err != nil {
return ethcommon.Hash{}, err
}
@@ -513,22 +516,18 @@ func (s *swapState) claimFunds() (ethcommon.Hash, error) {
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing XMRMaker's secret spend key
sc := s.getSecret()
tx, err := s.contract.Claim(s.txOpts, s.contractSwap, sc)
txHash, _, err := s.Claim(s.contractSwap, sc)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("sent claim tx, tx hash=%s", tx.Hash())
log.Infof("sent claim tx, tx hash=%s", txHash)
if _, err = s.backend.WaitForReceipt(s.ctx, tx.Hash()); err != nil {
return ethcommon.Hash{}, fmt.Errorf("failed to check claim transaction receipt: %w", err)
}
balance, err = s.backend.BalanceAt(s.ctx, addr, nil)
balance, err = s.BalanceAt(s.ctx, addr, nil)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance after claim: %v ETH", common.EtherAmount(*balance).AsEther())
return tx.Hash(), nil
return txHash, nil
}

View File

@@ -100,8 +100,8 @@ func newTestInstance(t *testing.T) (*Instance, *swapState) {
swapState, err := newSwapState(xmrmaker.backend, &types.Offer{}, xmrmaker.offerManager, nil, infofile,
common.MoneroAmount(33), desiredAmount)
require.NoError(t, err)
swapState.contract = xmrmaker.backend.Contract()
swapState.contractAddr = xmrmaker.backend.ContractAddr()
swapState.SetContract(xmrmaker.backend.Contract())
swapState.SetContractAddress(xmrmaker.backend.ContractAddr())
return xmrmaker, swapState
}
@@ -126,7 +126,7 @@ func newSwap(t *testing.T, ss *swapState, claimKey, refundKey [32]byte, amount *
claimKey = ss.secp256k1Pub.Keccak256()
}
txOpts, err := ss.backend.TxOpts()
txOpts, err := ss.TxOpts()
require.NoError(t, err)
// TODO: this is sus, update this when signing interfaces are updated
@@ -135,12 +135,12 @@ func newSwap(t *testing.T, ss *swapState, claimKey, refundKey [32]byte, amount *
txOpts.Value = nil
}()
ethAddr := ss.backend.EthAddress()
ethAddr := ss.EthAddress()
nonce := big.NewInt(0)
tx, err := ss.backend.Contract().NewSwap(txOpts, claimKey, refundKey, ethAddr, tm, nonce)
tx, err := ss.Contract().NewSwap(txOpts, claimKey, refundKey, ethAddr, tm, nonce)
require.NoError(t, err)
receipt, err := ss.backend.TransactionReceipt(context.Background(), tx.Hash())
receipt, err := ss.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
ss.contractSwapID, err = swapfactory.GetIDFromLog(receipt.Logs[0])
@@ -182,10 +182,8 @@ func TestSwapState_ClaimFunds(t *testing.T) {
claimKey := swapState.secp256k1Pub.Keccak256()
newSwap(t, swapState, claimKey,
[32]byte{}, big.NewInt(33), defaultTimeoutDuration)
swapState.contract = swapState.backend.Contract()
swapState.contractAddr = swapState.backend.ContractAddr()
_, err = swapState.contract.SetReady(swapState.txOpts, swapState.contractSwap)
_, err = swapState.Contract().SetReady(swapState.txOpts, swapState.contractSwap)
require.NoError(t, err)
txHash, err := swapState.claimFunds()
@@ -233,7 +231,7 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_ok(t *testing.T) {
require.NoError(t, err)
hash := newSwap(t, s, s.secp256k1Pub.Keccak256(), s.xmrtakerSecp256K1PublicKey.Keccak256(),
desiredAmount.BigInt(), duration)
addr := s.backend.ContractAddr()
addr := s.ContractAddr()
msg = &message.NotifyETHLocked{
Address: addr.String(),
@@ -247,8 +245,6 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_ok(t *testing.T) {
require.NotNil(t, resp)
require.Equal(t, message.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, &message.NotifyReady{}, s.nextExpectedMessage)
require.True(t, s.info.Status().IsOngoing())
@@ -280,7 +276,7 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_timeout(t *testing.T) {
require.NoError(t, err)
hash := newSwap(t, s, s.secp256k1Pub.Keccak256(), s.xmrtakerSecp256K1PublicKey.Keccak256(),
desiredAmount.BigInt(), duration)
addr := s.backend.ContractAddr()
addr := s.ContractAddr()
msg = &message.NotifyETHLocked{
Address: addr.String(),
@@ -294,8 +290,6 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_timeout(t *testing.T) {
require.NotNil(t, resp)
require.Equal(t, message.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, &message.NotifyReady{}, s.nextExpectedMessage)
@@ -307,7 +301,7 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_timeout(t *testing.T) {
}
}
require.NotNil(t, s.backend.Net().(*mockNet).msg)
require.NotNil(t, s.Net().(*mockNet).msg)
require.Equal(t, types.CompletedSuccess, s.info.Status())
}
@@ -322,7 +316,7 @@ func TestSwapState_HandleProtocolMessage_NotifyReady(t *testing.T) {
require.NoError(t, err)
newSwap(t, s, [32]byte{}, [32]byte{}, desiredAmount.BigInt(), duration)
_, err = s.contract.SetReady(s.txOpts, s.contractSwap)
_, err = s.Contract().SetReady(s.txOpts, s.contractSwap)
require.NoError(t, err)
msg := &message.NotifyReady{}
@@ -360,7 +354,7 @@ func TestSwapState_handleRefund(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret))
tx, err := s.contract.Refund(s.txOpts, s.contractSwap, sc)
tx, err := s.Contract().Refund(s.txOpts, s.contractSwap, sc)
require.NoError(t, err)
addr, err := s.handleRefund(tx.Hash().String())
@@ -393,7 +387,7 @@ func TestSwapState_HandleProtocolMessage_NotifyRefund(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret[:]))
tx, err := s.contract.Refund(s.txOpts, s.contractSwap, sc)
tx, err := s.Contract().Refund(s.txOpts, s.contractSwap, sc)
require.NoError(t, err)
msg := &message.NotifyRefund{
@@ -433,10 +427,10 @@ func TestSwapState_Exit_Reclaim(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret[:]))
tx, err := s.contract.Refund(s.txOpts, s.contractSwap, sc)
tx, err := s.Contract().Refund(s.txOpts, s.contractSwap, sc)
require.NoError(t, err)
receipt, err := s.backend.TransactionReceipt(s.ctx, tx.Hash())
receipt, err := s.TransactionReceipt(s.ctx, tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
require.Equal(t, 1, len(receipt.Logs[0].Topics))
@@ -446,7 +440,7 @@ func TestSwapState_Exit_Reclaim(t *testing.T) {
err = s.Exit()
require.NoError(t, err)
balance, err := s.backend.GetBalance(0)
balance, err := s.GetBalance(0)
require.NoError(t, err)
require.Equal(t, common.MoneroToPiconero(s.info.ProvidedAmount()).Uint64(), uint64(balance.Balance))
require.Equal(t, types.CompletedRefund, s.info.Status())

View File

@@ -29,7 +29,6 @@ type Instance struct {
basepath string
walletFile, walletPassword string
walletAddress mcrypto.Address
transferBack bool // transfer xmr back to original account
// non-nil if a swap is currently happening, nil otherwise
@@ -59,6 +58,7 @@ func NewInstance(cfg *Config) (*Instance, error) {
if err != nil {
return nil, err
}
cfg.Backend.SetXMRDepositAddress(address)
}
// TODO: check that XMRTaker's monero-wallet-cli endpoint has wallet-dir configured
@@ -67,7 +67,6 @@ func NewInstance(cfg *Config) (*Instance, error) {
basepath: cfg.Basepath,
walletFile: cfg.MoneroWalletFile,
walletPassword: cfg.MoneroWalletPassword,
walletAddress: address,
}, nil
}

View File

@@ -140,7 +140,7 @@ func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message
s.setXMRMakerKeys(sk, vk, secp256k1Pub)
txHash, err := s.lockETH(s.providedAmountInWei())
if err != nil {
return nil, fmt.Errorf("failed to deploy contract: %w", err)
return nil, fmt.Errorf("failed to lock ETH in contract: %w", err)
}
log.Info("locked ether in swap contract, waiting for XMR to be locked")
@@ -281,13 +281,12 @@ func (s *swapState) handleNotifyXMRLock(msg *message.NotifyXMRLock) (net.Message
}
close(s.xmrLockedCh)
log.Info("XMR was locked successfully, setting contract to ready...")
if err := s.ready(); err != nil {
return nil, fmt.Errorf("failed to call Ready: %w", err)
}
log.Info("XMR was locked successfully, setting contract to ready...")
go func() {
until := time.Until(s.t1)
@@ -332,6 +331,7 @@ func (s *swapState) handleNotifyXMRLock(msg *message.NotifyXMRLock) (net.Message
// handleNotifyClaimed handles XMRMaker's reveal after he calls Claim().
// it calls `createMoneroWallet` to create XMRTaker's wallet, allowing her to own the XMR.
func (s *swapState) handleNotifyClaimed(txHash string) (mcrypto.Address, error) {
log.Debugf("got NotifyClaimed, txHash=%s", txHash)
receipt, err := s.WaitForReceipt(s.ctx, ethcommon.HexToHash(txHash))
if err != nil {
return "", fmt.Errorf("failed check claim transaction receipt: %w", err)

View File

@@ -45,7 +45,7 @@ func (a *Instance) initiate(providesAmount common.EtherAmount, receivedAmount co
return errBalanceTooLow
}
a.swapState, err = newSwapState(a.backend, pcommon.GetSwapInfoFilepath(a.basepath), a.transferBack, a.walletAddress,
a.swapState, err = newSwapState(a.backend, pcommon.GetSwapInfoFilepath(a.basepath), a.transferBack,
providesAmount, receivedAmount, exchangeRate)
if err != nil {
return err

View File

@@ -27,11 +27,6 @@ type recoveryState struct {
// which has methods to either claim ether or reclaim monero from an initiated swap.
func NewRecoveryState(b backend.Backend, basepath string, secret *mcrypto.PrivateSpendKey,
contractSwapID [32]byte, contractSwap swapfactory.SwapFactorySwap) (*recoveryState, error) { //nolint:revive
txOpts, err := b.TxOpts()
if err != nil {
return nil, err
}
kp, err := secret.AsPrivateKeyPair()
if err != nil {
return nil, err
@@ -47,7 +42,6 @@ func NewRecoveryState(b backend.Backend, basepath string, secret *mcrypto.Privat
ctx: ctx,
cancel: cancel,
Backend: b,
txOpts: txOpts,
privkeys: kp,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),

View File

@@ -43,7 +43,10 @@ func TestClaimOrRefund_Claim(t *testing.T) {
// call swap.Claim()
sc := rs.ss.getSecret()
_, err = rs.ss.Contract().Claim(rs.ss.txOpts, rs.ss.contractSwap, sc)
txOpts, err := rs.ss.TxOpts()
require.NoError(t, err)
_, err = rs.ss.Contract().Claim(txOpts, rs.ss.contractSwap, sc)
require.NoError(t, err)
t.Log("XMRMaker claimed ETH...")

View File

@@ -23,7 +23,6 @@ import (
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swapfactory"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/fatih/color" //nolint:misspell
)
@@ -37,9 +36,8 @@ type swapState struct {
ctx context.Context
cancel context.CancelFunc
sync.Mutex
infofile string
transferBack bool
walletAddress mcrypto.Address
infofile string
transferBack bool
info *pswap.Info
statusCh chan types.Status
@@ -60,7 +58,6 @@ type swapState struct {
contractSwapID [32]byte
contractSwap swapfactory.SwapFactorySwap
t0, t1 time.Time
txOpts *bind.TransactOpts
// next expected network message
nextExpectedMessage net.Message
@@ -72,22 +69,17 @@ type swapState struct {
exited bool
}
func newSwapState(b backend.Backend, infofile string, transferBack bool, walletAddress mcrypto.Address,
func newSwapState(b backend.Backend, infofile string, transferBack bool,
providesAmount common.EtherAmount, receivedAmount common.MoneroAmount,
exchangeRate types.ExchangeRate) (*swapState, error) {
if b.Contract() == nil {
return nil, errNoSwapContractSet
}
if transferBack && walletAddress == "" {
if transferBack && b.XMRDepositAddress() == "" {
return nil, errMustProvideWalletAddress
}
txOpts, err := b.TxOpts()
if err != nil {
return nil, err
}
stage := types.ExpectingKeys
statusCh := make(chan types.Status, 16)
statusCh <- stage
@@ -97,6 +89,10 @@ func newSwapState(b backend.Backend, infofile string, transferBack bool, walletA
return nil, err
}
if b.ExternalSender() != nil {
transferBack = true // front-end must set final deposit address
}
ctx, cancel := context.WithCancel(b.Ctx())
s := &swapState{
ctx: ctx,
@@ -104,8 +100,6 @@ func newSwapState(b backend.Backend, infofile string, transferBack bool, walletA
Backend: b,
infofile: infofile,
transferBack: transferBack,
walletAddress: walletAddress,
txOpts: txOpts,
nextExpectedMessage: &net.SendKeysMessage{},
xmrLockedCh: make(chan struct{}),
claimedCh: make(chan struct{}),
@@ -388,23 +382,14 @@ func (s *swapState) lockETH(amount common.EtherAmount) (ethcommon.Hash, error) {
cmtXMRTaker := s.secp256k1Pub.Keccak256()
cmtXMRMaker := s.xmrmakerSecp256k1PublicKey.Keccak256()
s.txOpts.Value = amount.BigInt()
defer func() {
s.txOpts.Value = nil
}()
nonce := generateNonce()
tx, err := s.Contract().NewSwap(s.txOpts, cmtXMRMaker, cmtXMRTaker,
s.xmrmakerAddress, big.NewInt(int64(s.SwapTimeout().Seconds())), nonce)
txHash, receipt, err := s.NewSwap(cmtXMRMaker, cmtXMRTaker,
s.xmrmakerAddress, big.NewInt(int64(s.SwapTimeout().Seconds())), nonce, amount.BigInt())
if err != nil {
return ethcommon.Hash{}, fmt.Errorf("failed to instantiate swap on-chain: %w", err)
}
log.Debugf("instantiating swap on-chain: amount=%s txHash=%s", amount, tx.Hash())
receipt, err := s.WaitForReceipt(s.ctx, tx.Hash())
if err != nil {
return ethcommon.Hash{}, fmt.Errorf("failed to call new_swap in contract: %w", err)
}
log.Debugf("instantiated swap on-chain: amount=%s txHash=%s", amount, txHash)
if len(receipt.Logs) == 0 {
return ethcommon.Hash{}, errSwapInstantiationNoLogs
@@ -437,14 +422,14 @@ func (s *swapState) lockETH(amount common.EtherAmount) (ethcommon.Hash, error) {
return ethcommon.Hash{}, err
}
return tx.Hash(), nil
return txHash, nil
}
// ready calls the Ready() method on the Swap contract, indicating to XMRMaker he has until time t_1 to
// call Claim(). Ready() should only be called once XMRTaker sees XMRMaker lock his XMR.
// If time t_0 has passed, there is no point of calling Ready().
func (s *swapState) ready() error {
tx, err := s.Contract().SetReady(s.txOpts, s.contractSwap)
_, _, err := s.SetReady(s.contractSwap)
if err != nil {
if strings.Contains(err.Error(), revertSwapCompleted) && !s.info.Status().IsOngoing() {
return nil
@@ -453,10 +438,6 @@ func (s *swapState) ready() error {
return err
}
if _, err := s.WaitForReceipt(s.ctx, tx.Hash()); err != nil {
return fmt.Errorf("failed to call is_ready in swap contract: %w", err)
}
return nil
}
@@ -471,17 +452,13 @@ func (s *swapState) refund() (ethcommon.Hash, error) {
sc := s.getSecret()
log.Infof("attempting to call Refund()...")
tx, err := s.Contract().Refund(s.txOpts, s.contractSwap, sc)
txHash, _, err := s.Refund(s.contractSwap, sc)
if err != nil {
return ethcommon.Hash{}, err
}
if _, err := s.WaitForReceipt(s.ctx, tx.Hash()); err != nil {
return ethcommon.Hash{}, fmt.Errorf("failed to call Refund function in contract: %w", err)
}
s.clearNextExpectedMessage(types.CompletedRefund)
return tx.Hash(), nil
return txHash, nil
}
func (s *swapState) claimMonero(skB *mcrypto.PrivateSpendKey) (mcrypto.Address, error) {
@@ -509,14 +486,20 @@ func (s *swapState) claimMonero(skB *mcrypto.PrivateSpendKey) (mcrypto.Address,
}
log.Infof("monero claimed in account %s; transferring to original account %s",
addr, s.walletAddress)
addr, s.XMRDepositAddress())
err = mcrypto.ValidateAddress(string(s.XMRDepositAddress()))
if err != nil {
log.Errorf("failed to transfer to original account, address %s is invalid", addr)
return addr, nil
}
err = s.waitUntilBalanceUnlocks()
if err != nil {
return "", fmt.Errorf("failed to wait for balance to unlock: %w", err)
}
res, err := s.SweepAll(s.walletAddress, 0)
res, err := s.SweepAll(s.XMRDepositAddress(), 0)
if err != nil {
return "", fmt.Errorf("failed to send funds to original account: %w", err)
}
@@ -528,7 +511,7 @@ func (s *swapState) claimMonero(skB *mcrypto.PrivateSpendKey) (mcrypto.Address,
amount := res.AmountList[0]
log.Infof("transferred %v XMR to %s",
common.MoneroAmount(amount).AsMonero(),
s.walletAddress,
s.XMRDepositAddress(),
)
close(s.claimedCh)
@@ -544,7 +527,7 @@ func (s *swapState) waitUntilBalanceUnlocks() error {
log.Infof("checking if balance unlocked...")
if s.Env() == common.Development {
_ = s.GenerateBlocks(string(s.walletAddress), 64)
_ = s.GenerateBlocks(string(s.XMRDepositAddress()), 64)
_ = s.Refresh()
}

View File

@@ -103,7 +103,7 @@ func newXMRMakerBackend(t *testing.T) backend.Backend {
func newTestInstance(t *testing.T) *swapState {
b := newBackend(t)
swapState, err := newSwapState(b, infofile, false, "",
swapState, err := newSwapState(b, infofile, false,
common.NewEtherAmount(1), common.MoneroAmount(0), 1)
require.NoError(t, err)
return swapState
@@ -340,7 +340,9 @@ func TestSwapState_NotifyClaimed(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret))
tx, err := s.Contract().Claim(s.txOpts, s.contractSwap, sc)
txOpts, err := s.TxOpts()
require.NoError(t, err)
tx, err := s.Contract().Claim(txOpts, s.contractSwap, sc)
require.NoError(t, err)
// handled the claimed message should result in the monero wallet being created

View File

@@ -15,6 +15,7 @@ var (
errCannotRefund = errors.New("cannot refund if not the ETH provider")
// ws errors
errUnimplemented = errors.New("unimplemented")
errInvalidMethod = errors.New("invalid method")
errUnimplemented = errors.New("unimplemented")
errInvalidMethod = errors.New("invalid method")
errSignerNotRequired = errors.New("signer not required")
)

View File

@@ -8,7 +8,9 @@ import (
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/common/types"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/protocol/txsender"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/gorilla/handlers"
@@ -59,7 +61,7 @@ func NewServer(cfg *Config) (*Server, error) {
return &Server{
s: s,
wsServer: newWsServer(cfg.Ctx, cfg.ProtocolBackend.SwapManager(), ns),
wsServer: newWsServer(cfg.Ctx, cfg.ProtocolBackend.SwapManager(), ns, cfg.ProtocolBackend, cfg.ProtocolBackend.ExternalSender()), //nolint:lll
port: cfg.Port,
wsPort: cfg.WsPort,
}, nil
@@ -115,6 +117,9 @@ type ProtocolBackend interface {
SetGasPrice(uint64)
SetSwapTimeout(timeout time.Duration)
SwapManager() swap.Manager
ExternalSender() *txsender.ExternalSender
SetEthAddress(ethcommon.Address)
SetXMRDepositAddress(mcrypto.Address)
}
// XMRTaker ...

100
rpc/ws.go
View File

@@ -8,7 +8,10 @@ import (
"github.com/noot/atomic-swap/common/rpctypes"
"github.com/noot/atomic-swap/common/types"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/protocol/txsender"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/gorilla/websocket"
)
@@ -17,22 +20,41 @@ const (
subscribeMakeOffer = "net_makeOfferAndSubscribe"
subscribeTakeOffer = "net_takeOfferAndSubscribe"
subscribeSwapStatus = "swap_subscribeStatus"
subscribeSigner = "signer_subscribe"
)
var upgrader = websocket.Upgrader{}
type wsServer struct {
ctx context.Context
sm SwapManager
ns *NetService
var upgrader = websocket.Upgrader{
CheckOrigin: checkOriginFunc,
}
func newWsServer(ctx context.Context, sm SwapManager, ns *NetService) *wsServer {
return &wsServer{
ctx: ctx,
sm: sm,
ns: ns,
func checkOriginFunc(r *http.Request) bool {
return true
}
type wsServer struct {
ctx context.Context
sm SwapManager
ns *NetService
backend ProtocolBackend
txsOutCh <-chan *txsender.Transaction
txsInCh chan<- ethcommon.Hash
}
func newWsServer(ctx context.Context, sm SwapManager, ns *NetService, backend ProtocolBackend,
signer *txsender.ExternalSender) *wsServer {
s := &wsServer{
ctx: ctx,
sm: sm,
ns: ns,
backend: backend,
}
if signer != nil {
s.txsOutCh = signer.OngoingCh()
s.txsInCh = signer.IncomingCh()
}
return s
}
// ServeHTTP ...
@@ -69,6 +91,13 @@ func (s *wsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *wsServer) handleRequest(conn *websocket.Conn, req *rpctypes.Request) error {
switch req.Method {
case subscribeSigner:
var params *rpctypes.SignerRequest
if err := json.Unmarshal(req.Params, &params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
}
return s.handleSigner(s.ctx, conn, params.OfferID, params.EthAddress, params.XMRAddress)
case subscribeNewPeer:
return errUnimplemented
case "net_discover":
@@ -134,6 +163,55 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *rpctypes.Request) er
}
}
func (s *wsServer) handleSigner(ctx context.Context, conn *websocket.Conn, offerID, ethAddress, xmrAddr string) error {
if s.txsOutCh == nil {
return errSignerNotRequired
}
if err := mcrypto.ValidateAddress(xmrAddr); err != nil {
return err
}
s.backend.SetEthAddress(ethcommon.HexToAddress(ethAddress))
s.backend.SetXMRDepositAddress(mcrypto.Address(xmrAddr))
for {
select {
case <-ctx.Done():
return nil
case tx := <-s.txsOutCh:
log.Debugf("outbound tx: %v", tx)
resp := &rpctypes.SignerResponse{
OfferID: offerID,
To: tx.To.String(),
Data: tx.Data,
Value: tx.Value,
}
err := conn.WriteJSON(resp)
if err != nil {
return err
}
_, message, err := conn.ReadMessage()
if err != nil {
return err
}
var params *rpctypes.SignerTxSigned
if err := json.Unmarshal(message, &params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
}
if params.OfferID != offerID {
return fmt.Errorf("got unexpected offerID %s, expected %s", params.OfferID, offerID)
}
s.txsInCh <- ethcommon.HexToHash(params.TxHash)
}
}
}
func (s *wsServer) subscribeTakeOffer(ctx context.Context, conn *websocket.Conn,
id uint64, statusCh <-chan types.Status, infofile string) error {
resp := &rpctypes.TakeOfferResponse{

View File

@@ -9,9 +9,11 @@ import (
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/common/types"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/net"
"github.com/noot/atomic-swap/net/message"
"github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/protocol/txsender"
"github.com/noot/atomic-swap/rpcclient/wsclient"
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -131,6 +133,11 @@ func (*mockProtocolBackend) SetSwapTimeout(timeout time.Duration) {}
func (b *mockProtocolBackend) SwapManager() swap.Manager {
return b.sm
}
func (*mockProtocolBackend) ExternalSender() *txsender.ExternalSender {
return nil
}
func (*mockProtocolBackend) SetEthAddress(ethcommon.Address) {}
func (*mockProtocolBackend) SetXMRDepositAddress(mcrypto.Address) {}
func newServer(t *testing.T) *Server {
ctx, cancel := context.WithCancel(context.Background())

6398
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
"devDependencies": {
"@mdi/js": "^6.5.95",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.0.0",
"@smui/button": "^6.0.0-beta.13",
@@ -42,7 +43,9 @@
"typescript": "^4.0.0"
},
"dependencies": {
"@metamask/detect-provider": "^1.2.0",
"axios": "0.21.1",
"ethers": "^5.6.8",
"sirv-cli": "^2.0.0",
"svelte-material-ui": "^6.0.0-beta.13"
}

View File

@@ -6,6 +6,7 @@ import { terser } from 'rollup-plugin-terser'
import sveltePreprocess from 'svelte-preprocess'
import typescript from '@rollup/plugin-typescript'
import css from 'rollup-plugin-css-only'
import json from '@rollup/plugin-json'
const production = !process.env.ROLLUP_WATCH
@@ -43,6 +44,9 @@ export default {
file: 'public/build/bundle.js',
},
plugins: [
json({
compact: true
}),
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
compilerOptions: {

View File

@@ -4,6 +4,7 @@
import Button from '@smui/button'
import { peers, getPeers } from './stores/peerStore'
import { offers, refreshOffers } from './stores/offerStore'
import { connectAccount, currentAccount } from './stores/metamask'
import OffersTable from './components/OffersTable.svelte'
import StatCard from './components/StatCard.svelte'
import TakeDealDrawer from './components/TakeDealDialog.svelte'
@@ -11,6 +12,13 @@
const handleRefreshClick = () => {
getPeers()
}
let account = null;
const connectMetamask = () => {
connectAccount()
account = $currentAccount
}
</script>
<main>
@@ -27,6 +35,9 @@
<Cell class="refreshButton">
<Button on:click={handleRefreshClick}>Refresh</Button>
</Cell>
<Cell class="metamask">
<Button on:click={connectMetamask}>{account === null ? 'Connect Metamask' : `Metamask connected: ${account}`}</Button>
</Cell>
</InnerGrid>
<br />
<OffersTable />

View File

@@ -11,8 +11,13 @@
import { Svg } from '@smui/common/elements'
import CircularProgress from '@smui/circular-progress'
import HelperText from '@smui/textfield/helper-text'
import { currentAccount, sign } from "../stores/metamask"
import { onMount } from 'svelte'
const WS_ADDRESS = 'ws://127.0.0.1:8081'
let amountProvided: number | null = null
let xmrAddress = ""
let isSuccess = false
let isLoadingSwap = false
let error = ''
@@ -44,10 +49,47 @@
}
const handleSendTakeOffer = () => {
let offerID = $selectedOffer?.id
let webSocket = new WebSocket(WS_ADDRESS)
webSocket.onopen = () => {
console.log('opened')
console.log("sending ws signer msg")
let req = {
method: "signer_subscribe",
params: {
jsonRPC: "2.0",
id: "0",
offerID: offerID,
ethAddress: $currentAccount,
xmrAddress: xmrAddress,
}
}
webSocket.send(JSON.stringify(req))
console.log("sent ws signer msg", req)
}
webSocket.onmessage = async (msg) => {
console.log('message to sign:', msg.data)
let txHash = await sign(msg.data)
let out = {
offerID: offerID,
txHash: txHash,
}
webSocket.send(JSON.stringify(out))
}
webSocket.onclose = (e) => {
console.log('closed:', e)
}
webSocket.onerror = (e) => {
console.log('error', e)
}
isLoadingSwap = true
rpcRequest<NetTakeOfferSyncResult | undefined>('net_takeOfferSync', {
multiaddr: $selectedOffer?.peer,
offerID: $selectedOffer?.id,
offerID: offerID,
providesAmount: Number(amountProvided),
})
.then(({ result }) => {
@@ -60,6 +102,8 @@
swapError =
'Something went wrong. Swap funds refunded, please check the logs for more info'
}
webSocket.close()
})
.catch((e: Error) => {
console.error('error when swapping', e)
@@ -89,6 +133,7 @@
<Title class="title" id="mandatory-title">
Swap offer {$selectedOffer.id}
</Title>
<span>{$selectedOffer.peer}</span>
</div>
<Content id="mandatory-content">
<section class="container">
@@ -124,6 +169,14 @@
>
<HelperText slot="helper">{error}</HelperText>
</Textfield>
<Textfield
bind:value={xmrAddress}
variant="outlined"
label={"XMR address"}
invalid={!!error}
>
<HelperText slot="helper">{error}</HelperText>
</Textfield>
<Icon class="swapIcon" component={Svg} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiSwapVertical} />
</Icon>

96
ui/src/stores/metamask.ts Normal file
View File

@@ -0,0 +1,96 @@
import {providers, Contract, utils} from "ethers"
import detectEthereumProvider from "@metamask/detect-provider"
import { writable } from 'svelte/store';
import SwapFactory from "../../../ethereum/artifacts/contracts/SwapFactory.sol/SwapFactory.json"
ethereum.on('chainChanged', (_chainId) => window.location.reload());
ethereum.on('accountsChanged', handleAccountsChanged);
export const currentAccount = writable(null);
export const connectAccount = async () => {
const provider = (await detectEthereumProvider()) as any
if (provider) {
startApp(provider);
await provider.request({ method: "eth_accounts" }).then(handleAccountsChanged)
.catch((err) => {
// Some unexpected error.
// For backwards compatibility reasons, if no accounts are available,
// eth_accounts will return an empty array.
console.error(err);
});
await initialize()
} else {
console.error("Metamask is not installed")
}
}
function startApp(provider) {
// If the provider returned by detectEthereumProvider is not the same as
// window.ethereum, something is overwriting it, perhaps another wallet.
if (provider !== window.ethereum) {
console.error('Do you have multiple wallets installed?');
}
// Access the decentralized web!
}
// Note that this event is emitted on page load.
// If the array of accounts is non-empty, you're already
// connected.
ethereum.on('accountsChanged', handleAccountsChanged);
// For now, 'eth_accounts' will continue to always return an array
function handleAccountsChanged(accounts) {
if (accounts.length === 0) {
// MetaMask is locked or the user has not connected any accounts
console.log('Please connect to MetaMask.');
} else if (accounts[0] !== currentAccount) {
currentAccount.set(accounts[0]);
console.log(currentAccount)
console.log(accounts[0])
// Do any other work!
}
}
let ethersProvider
let chainId
let contract
let signer
let from
const initialize = async () => {
ethersProvider = new providers.Web3Provider(window.ethereum, 'any');
window.ethersProvider = ethersProvider
signer = ethersProvider.getSigner()
console.log("signer:", await signer.getAddress())
}
export const sign = async(msg) => {
let tx = JSON.parse(msg)
let value
if (tx.value != "") {
value = utils.parseEther(tx.value)
}
let params =
{
from: signer.getAddress(),
to: tx.to,
gasPrice: window.ethersProvider.getGasPrice(),
gasLimit: "200000",
value: value,
data: tx.data,
}
console.log("sending transaction:", params)
let res
try {
res = await signer.sendTransaction(params)
console.log(res)
} catch (e) {
console.error("tx failed", e)
return ""
}
return res.hash
}

View File

@@ -1,7 +1,6 @@
export const intToHexString = (input: number[]) => {
const hexArray = input.map((n) => {
const num = Number(n).toString(16).padStart(2, "0")
// console.log(n, num)
return num
})

File diff suppressed because it is too large Load Diff