create separate integration test CI job, implement net_makeOfferAndSubscribe, swap_getOffers, implement happy path integration test (#101)

This commit is contained in:
noot
2022-03-15 22:50:05 -04:00
committed by GitHub
parent f0021ce118
commit d24eb78434
45 changed files with 1205 additions and 432 deletions

View File

@@ -0,0 +1,48 @@
on:
pull_request:
branches:
- master
push:
branches:
- master
jobs:
integration-tests:
strategy:
matrix:
go-version: [1.17.x]
node-version: [16.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v2
# cache go build cache
- name: Cache go modules
uses: actions/cache@v2
with:
path: ${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-build
# cache go mod cache
- name: Cache go modules
uses: actions/cache@v2
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-mod
- name: Run build
run: make build
- name: Run integration tests
run: ./scripts/run-integration-tests.sh

View File

@@ -50,9 +50,6 @@ jobs:
- name: Run coverage
run: bash <(curl -s https://codecov.io/bash)
- name: Run integration tests
run: ./scripts/run-integration-tests.sh
- name: Run Hardhat tests
run: |
cd ethereum

3
.gitignore vendored
View File

@@ -51,4 +51,5 @@ Cargo.lock
# End of https://www.toptal.com/developers/gitignore/api/go,rust
*.key
log
log
*.log

View File

@@ -106,6 +106,11 @@ Firstly, we need Bob to make an offer and advertise it, so that Alice can take i
# Published offer with ID cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9
```
Alternatively, you can make the offer via websockets and get notified when the swap is taken:
```bash
./swapcli make --min-amount 0.1 --max-amount 1 --exchange-rate 0.05 --daemon-addr=ws://localhost:8082 --subscribe
```
Now, we can have Alice begin discovering peers who have offers advertised.
```bash
./swapcli discover --provides XMR --search-time 3
@@ -124,6 +129,11 @@ Now, we can tell Alice to initiate the protocol w/ the peer (Bob), the offer (co
# Initiated swap with ID=0
```
Alternatively, you can take the offer via websockets and get notified when the swap status updates:
```bash
./swapcli take --multiaddr /ip4/127.0.0.1/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7 --offer-id cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9 --provides-amount 0.05 --subscribe --daemon-addr=ws://localhost:8081
```
If all goes well, you should see Alice and Bob successfully exchange messages and execute the swap protocol. The result is that Alice now owns the private key to a Monero account (and is the only owner of that key) and Bob has the ETH transferred to him. On Alice's side, a Monero wallet will be generated in the `--wallet-dir` provided in the `monero-wallet-rpc` step for Alice.
To query the information for an ongoing swap, you can run:

View File

@@ -0,0 +1,32 @@
package client
import (
"encoding/json"
"github.com/noot/atomic-swap/common/rpcclient"
"github.com/noot/atomic-swap/common/types"
"github.com/noot/atomic-swap/rpc"
)
// GetOffers calls swap_getOffers.
func (c *Client) GetOffers() ([]*types.Offer, error) {
const (
method = "swap_getOffers"
)
resp, err := rpcclient.PostRPC(c.endpoint, method, "{}")
if err != nil {
return nil, err
}
if resp.Error != nil {
return nil, resp.Error
}
var res *rpc.GetOffersResponse
if err = json.Unmarshal(resp.Result, &res); err != nil {
return nil, err
}
return res.Offers, nil
}

View File

@@ -82,6 +82,10 @@ var (
Name: "exchange-rate",
Usage: "desired exchange rate of XMR:ETH, eg. --exchange-rate=0.1 means 10XMR = 1ETH",
},
&cli.BoolFlag{
Name: "subscribe",
Usage: "subscribe to push notifications about the swap's status",
},
daemonAddrFlag,
},
},
@@ -253,6 +257,32 @@ func runMake(ctx *cli.Context) error {
endpoint = defaultSwapdAddress
}
if ctx.Bool("subscribe") {
c, err := rpcclient.NewWsClient(context.Background(), endpoint)
if err != nil {
return err
}
id, takenCh, statusCh, err := c.MakeOfferAndSubscribe(min, max, types.ExchangeRate(exchangeRate))
if err != nil {
return err
}
fmt.Printf("Made offer with ID=%s\n", id)
taken := <-takenCh
fmt.Printf("Offer taken! Swap ID=%d\n", taken.ID)
for stage := range statusCh {
fmt.Printf("> Stage updated: %s\n", stage)
if !stage.IsOngoing() {
return nil
}
}
return nil
}
c := client.NewClient(endpoint)
id, err := c.MakeOffer(min, max, exchangeRate)
if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"math/big"
"github.com/noot/atomic-swap/common"
pcommon "github.com/noot/atomic-swap/protocol"
"github.com/noot/atomic-swap/swapfactory"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
@@ -37,7 +38,7 @@ func getOrDeploySwapFactory(address ethcommon.Address, env common.Environment, b
// store the contract address on disk
fp := fmt.Sprintf("%s/contractaddress", basepath)
if err = common.WriteContractAddressToFile(fp, address.String()); err != nil {
if err = pcommon.WriteContractAddressToFile(fp, address.String()); err != nil {
return nil, ethcommon.Address{}, fmt.Errorf("failed to write contract address to file: %w", err)
}
} else {

View File

@@ -335,7 +335,7 @@ func (d *daemon) make(c *cli.Context) error {
}
}()
log.Infof("started swapd with basepath %d",
log.Infof("started swapd with basepath %s",
cfg.Basepath,
)
return nil

View File

@@ -21,5 +21,5 @@ type SwapStateNet interface {
type SwapStateRPC interface {
SendKeysMessage() (*message.SendKeysMessage, error)
ID() uint64
//Status() types.Status
InfoFile() string
}

View File

@@ -7,22 +7,18 @@ import (
// Request represents a JSON-RPC request
type Request struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
ID uint64 `json:"id"`
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID uint64 `json:"id"`
}
// Response is the JSON format of a response
type Response struct {
// JSON-RPC Version
Version string `json:"jsonrpc"`
// Resulting values
Result json.RawMessage `json:"result"`
// Any generated errors
Error *Error `json:"error"`
// Request id
ID *json.RawMessage `json:"id"`
Version string `json:"jsonrpc"`
Result json.RawMessage `json:"result"`
Error *Error `json:"error"`
ID *json.RawMessage `json:"id"`
}
// ErrCode is a int type used for the rpc error codes
@@ -42,5 +38,5 @@ func (e *Error) Error() string {
// SubscribeSwapStatusResponse ...
type SubscribeSwapStatusResponse struct {
Stage string `json:"stage"`
Status string `json:"status"`
}

View File

@@ -3,7 +3,6 @@ package rpcclient
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/noot/atomic-swap/common/types"
@@ -44,16 +43,28 @@ func NewWsClient(ctx context.Context, endpoint string) (*wsClient, error) { ///n
}, nil
}
// SubscribeSwapStatusRequestParams ...
type SubscribeSwapStatusRequestParams struct {
ID uint64 `json:"id"`
}
// SubscribeSwapStatus returns a channel that is written to each time the swap's status updates.
// If there is no swap with the given ID, it returns an error.
func (c *wsClient) SubscribeSwapStatus(id uint64) (<-chan types.Status, error) {
params := &SubscribeSwapStatusRequestParams{
ID: id,
}
bz, err := json.Marshal(params)
if err != nil {
return nil, err
}
req := &Request{
JSONRPC: DefaultJSONRPCVersion,
Method: "swap_subscribeStatus",
Params: map[string]interface{}{
"id": id,
},
ID: 0,
Params: bz,
ID: 0,
}
if err := c.conn.WriteJSON(req); err != nil {
@@ -91,24 +102,45 @@ func (c *wsClient) SubscribeSwapStatus(id uint64) (<-chan types.Status, error) {
break
}
respCh <- types.NewStatus(status.Stage)
respCh <- types.NewStatus(status.Status)
}
}()
return respCh, nil
}
// SubscribeTakeOfferParams ...
// TODO: duplciate of rpc.TakeOfferRequest
type SubscribeTakeOfferParams struct {
Multiaddr string `json:"multiaddr"`
OfferID string `json:"offerID"`
ProvidesAmount float64 `json:"providesAmount"`
}
// TakeOfferResponse ...
type TakeOfferResponse struct {
ID uint64 `json:"id"`
InfoFile string `json:"infoFile"`
}
func (c *wsClient) TakeOfferAndSubscribe(multiaddr, offerID string,
providesAmount float64) (id uint64, ch <-chan types.Status, err error) {
params := &SubscribeTakeOfferParams{
Multiaddr: multiaddr,
OfferID: offerID,
ProvidesAmount: providesAmount,
}
bz, err := json.Marshal(params)
if err != nil {
return 0, nil, err
}
req := &Request{
JSONRPC: DefaultJSONRPCVersion,
Method: "net_takeOfferAndSubscribe",
Params: map[string]interface{}{
"multiaddr": multiaddr,
"offerID": offerID,
"providesAmount": providesAmount,
},
ID: 0,
Params: bz,
ID: 0,
}
if err = c.conn.WriteJSON(req); err != nil {
@@ -132,14 +164,9 @@ func (c *wsClient) TakeOfferAndSubscribe(multiaddr, offerID string,
}
log.Debugf("received message over websockets: %s", message)
var idResp map[string]uint64
var idResp *TakeOfferResponse
if err := json.Unmarshal(resp.Result, &idResp); err != nil {
return 0, nil, fmt.Errorf("failed to unmarshal response: %s", err)
}
id, ok := idResp["id"]
if !ok {
return 0, nil, errors.New("websocket response did not contain ID")
return 0, nil, fmt.Errorf("failed to unmarshal swap ID response: %s", err)
}
respCh := make(chan types.Status)
@@ -169,13 +196,147 @@ func (c *wsClient) TakeOfferAndSubscribe(multiaddr, offerID string,
log.Debugf("received message over websockets: %s", message)
var status *SubscribeSwapStatusResponse
if err := json.Unmarshal(resp.Result, &status); err != nil {
log.Warnf("failed to unmarshal swap status response: %s", err)
break
}
respCh <- types.NewStatus(status.Status)
}
}()
return idResp.ID, respCh, nil
}
// MakeOfferRequest ...
// TODO: duplicate of rpc.MakeOfferRequest
type MakeOfferRequest struct {
MinimumAmount float64 `json:"minimumAmount"`
MaximumAmount float64 `json:"maximumAmount"`
ExchangeRate types.ExchangeRate `json:"exchangeRate"`
}
// MakeOfferResponse ...
type MakeOfferResponse struct {
ID string `json:"offerID"`
InfoFile string `json:"infoFile"`
}
// MakeOfferTakenResponse contains the swap ID
type MakeOfferTakenResponse struct {
ID uint64 `json:"id"`
}
func (c *wsClient) MakeOfferAndSubscribe(min, max float64,
exchangeRate types.ExchangeRate) (string, <-chan *MakeOfferTakenResponse, <-chan types.Status, error) {
params := &MakeOfferRequest{
MinimumAmount: min,
MaximumAmount: max,
ExchangeRate: exchangeRate,
}
bz, err := json.Marshal(params)
if err != nil {
return "", nil, nil, err
}
req := &Request{
JSONRPC: DefaultJSONRPCVersion,
Method: "net_makeOfferAndSubscribe", // TODO: use const
Params: bz,
ID: 0,
}
if err = c.conn.WriteJSON(req); err != nil {
return "", nil, nil, err
}
// read ID from connection
_, message, err := c.conn.ReadMessage()
if err != nil {
return "", nil, nil, fmt.Errorf("failed to read websockets message: %s", err)
}
var resp *Response
err = json.Unmarshal(message, &resp)
if err != nil {
return "", nil, nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
if resp.Error != nil {
return "", nil, nil, fmt.Errorf("websocket server returned error: %w", resp.Error)
}
// read synchronous response (offer ID and infofile)
log.Debugf("received message over websockets: %s", message)
var respData *MakeOfferResponse
if err := json.Unmarshal(resp.Result, &respData); err != nil {
return "", nil, nil, fmt.Errorf("failed to unmarshal response: %s", err)
}
takenCh := make(chan *MakeOfferTakenResponse)
respCh := make(chan types.Status)
go func() {
defer close(respCh)
defer close(takenCh)
// read if swap was taken
_, message, err := c.conn.ReadMessage()
if err != nil {
log.Warnf("failed to read websockets message: %s", err)
return
}
var resp *Response
err = json.Unmarshal(message, &resp)
if err != nil {
log.Warnf("failed to unmarshal response: %s", err)
return
}
if resp.Error != nil {
log.Warnf("websocket server returned error: %s", resp.Error)
return
}
log.Debugf("received message over websockets: %s", message)
var taken *MakeOfferTakenResponse
if err := json.Unmarshal(resp.Result, &taken); err != nil {
log.Warnf("failed to unmarshal response: %s", err)
return
}
takenCh <- taken
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
log.Warnf("failed to read websockets message: %s", err)
break
}
var resp *Response
err = json.Unmarshal(message, &resp)
if err != nil {
log.Warnf("failed to unmarshal response: %s", err)
break
}
respCh <- types.NewStatus(status.Stage)
if resp.Error != nil {
log.Warnf("websocket server returned error: %s", resp.Error)
break
}
log.Debugf("received message over websockets: %s", message)
var status *SubscribeSwapStatusResponse
if err := json.Unmarshal(resp.Result, &status); err != nil {
log.Warnf("failed to unmarshal response: %s", err)
break
}
respCh <- types.NewStatus(status.Status)
}
}()
return id, respCh, nil
return respData.ID, takenCh, respCh, nil
}

View File

@@ -63,3 +63,10 @@ func (o *Offer) String() string {
o.ExchangeRate,
)
}
// OfferExtra represents extra data that is passed when an offer is made.
type OfferExtra struct {
IDCh chan uint64
StatusCh chan Status
InfoFile string
}

View File

@@ -3,11 +3,7 @@ package common
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"time"
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -58,36 +54,6 @@ func WaitForReceipt(ctx context.Context, ethclient *ethclient.Client, txHash eth
return nil, errors.New("failed to get receipt, timed out")
}
// WriteContractAddressToFile writes the contract address to a file in the given basepath
func WriteContractAddressToFile(basepath, addr string) error {
t := time.Now().Format("2006-Jan-2-15:04:05")
path := fmt.Sprintf("%s-%s.txt", basepath, t)
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
file, err := os.Create(filepath.Clean(path))
if err != nil {
return err
}
type addressFileFormat struct {
Address string
}
bz, err := json.Marshal(addressFileFormat{
Address: addr,
})
if err != nil {
return err
}
_, err = file.Write(bz)
return err
}
// EthereumPrivateKeyToAddress returns the address associated with a private key
func EthereumPrivateKeyToAddress(privkey *ecdsa.PrivateKey) ethcommon.Address {
pub := privkey.Public().(*ecdsa.PublicKey)

View File

@@ -3,7 +3,6 @@ package common
import (
"context"
"math/big"
"os"
"testing"
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -62,10 +61,3 @@ func TestWaitForReceipt(t *testing.T) {
require.NoError(t, err)
require.Equal(t, tx.Hash(), receipt.TxHash)
}
func TestWriteContractAddrssToFile(t *testing.T) {
addr := "0xabcd"
basepath := os.TempDir() + "/"
err := WriteContractAddressToFile(basepath, addr)
require.NoError(t, err)
}

View File

@@ -4,7 +4,6 @@ import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -81,15 +80,23 @@ func (kp *PrivateKeyPair) ViewKey() *PrivateViewKey {
return kp.vk
}
// Marshal JSON-marshals the private key pair, providing its PrivateSpendKey, PrivateViewKey, Address,
// PrivateKeyInfo ...
type PrivateKeyInfo struct {
PrivateSpendKey string
PrivateViewKey string
Address string
Environment string
}
// Info return the private key pair as PrivateKeyInfo, providing its PrivateSpendKey, PrivateViewKey, Address,
// and Environment. This is intended to be written to a file, which someone can use to regenerate the wallet.
func (kp *PrivateKeyPair) Marshal(env common.Environment) ([]byte, error) {
m := make(map[string]string)
m["PrivateSpendKey"] = kp.sk.Hex()
m["PrivateViewKey"] = kp.vk.Hex()
m["Address"] = string(kp.Address(env))
m["Environment"] = env.String()
return json.Marshal(m)
func (kp *PrivateKeyPair) Info(env common.Environment) *PrivateKeyInfo {
return &PrivateKeyInfo{
PrivateSpendKey: kp.sk.Hex(),
PrivateViewKey: kp.vk.Hex(),
Address: string(kp.Address(env)),
Environment: env.String(),
}
}
// PrivateSpendKey represents a monero private spend key

View File

@@ -2,7 +2,6 @@ package mcrypto
import (
"encoding/hex"
"encoding/json"
"testing"
"github.com/noot/atomic-swap/common"
@@ -60,22 +59,6 @@ func TestKeccak256(t *testing.T) {
require.Equal(t, res, res2[:])
}
func TestPrivateKeyPair_Marshal(t *testing.T) {
kp, err := GenerateKeys()
require.NoError(t, err)
bz, err := kp.Marshal(common.Mainnet)
require.NoError(t, err)
var res map[string]string
err = json.Unmarshal(bz, &res)
require.NoError(t, err)
require.Equal(t, kp.sk.Hex(), res["PrivateSpendKey"])
require.Equal(t, kp.vk.Hex(), res["PrivateViewKey"])
require.Equal(t, string(kp.Address(common.Mainnet)), res["Address"])
require.Equal(t, common.Mainnet.String(), res["Environment"])
}
func TestNewPrivateSpendKey(t *testing.T) {
kp, err := GenerateKeys()
require.NoError(t, err)

View File

@@ -1,34 +0,0 @@
package mcrypto
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/noot/atomic-swap/common"
)
// WriteKeysToFile writes the given private key pair to a file within the given path.
func WriteKeysToFile(basepath string, keys *PrivateKeyPair, env common.Environment) error {
t := time.Now().Format("2006-Jan-2-15:04:05")
path := fmt.Sprintf("%s-%s.key", basepath, t)
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
file, err := os.Create(filepath.Clean(path))
if err != nil {
return err
}
bz, err := keys.Marshal(env)
if err != nil {
return err
}
_, err = file.Write(bz)
return err
}

View File

@@ -1,18 +0,0 @@
package mcrypto
import (
"os"
"testing"
"github.com/noot/atomic-swap/common"
"github.com/stretchr/testify/require"
)
func TestWriteKeysToFile(t *testing.T) {
kp, err := GenerateKeys()
require.NoError(t, err)
err = WriteKeysToFile(os.TempDir()+"/", kp, common.Development)
require.NoError(t, err)
}

View File

@@ -211,12 +211,9 @@ Returns:
- `status`: the swap's status, one of `success`, `refunded`, or `aborted`.
Example:
```
```bash
curl -X POST http://127.0.0.1:5001 -d '{"jsonrpc":"2.0","id":"0","method":"swap_getPast","params":{"id": 0}}' -H 'Content-Type: application/json'
```
```
{"jsonrpc":"2.0","result":{"provided":"ETH","providedAmount":0.05,"receivedAmount":1,"exchangeRate":20,"status":"success"},"id":"0"}
# {"jsonrpc":"2.0","result":{"provided":"ETH","providedAmount":0.05,"receivedAmount":1,"exchangeRate":20,"status":"success"},"id":"0"}
```
## websocket subscriptions
@@ -231,13 +228,65 @@ Paramters:
- `id`: the swap ID.
Returns:
- `stage`: the swap's stage or exit status.
- `status`: the swap's status.
Example:
```
$ wscat -c ws://localhost:8081
```bash
wscat -c ws://localhost:8081
# Connected (press CTRL+C to quit)
# > {"jsonrpc":"2.0", "method":"swap_subscribeStatus", "params": {"id": 0}, "id": 0}
# < {"jsonrpc":"2.0","result":{"stage":"ContractDeployed"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"refunded"},"error":null,"id":null}
```
### `net_makeOfferAndSubscribe`
Make a swap offer and subscribe to updates on it. A notification will be pushed with the swap ID when the offer is taken, as well as status updates after that, until the swap has completed.
Parameters:
- `minimumAmount`: minimum amount to swap, in XMR.
- `maximumAmount`: maximum amount to swap, in XMR.
- `exchangeRate`: exchange rate of ETH-XMR for the swap, expressed in a fraction of XMR/ETH. For example, if you wish to trade 10 XMR for 1 ETH, the exchange rate would be 0.1.
Returns:
- `offerID`: ID of the swap offer.
- `id`: ID of the swap, when the offer is taken and a swap is initiated.
- `status`: the swap's status.
Example (including notifications when swap is taken):
```bash
wscat -c ws://localhost:8082
# Connected (press CTRL+C to quit)
# > {"jsonrpc":"2.0", "method":"net_makeOfferAndSubscribe", "params": {"minimumAmount": 0.1, "maximumAmount": 1, "exchangeRate": 0.05}, "id": 0}
# < {"jsonrpc":"2.0","result":{"offerID":"cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"id":0},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"ExpectingKeys"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"KeysExchanged"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"XMRLocked"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"Success"},"error":null,"id":null}
```
### `net_takeOfferAndSubscribe`
Take an advertised swap offer and subscribe to updates on it. This call will initiate and execute an atomic swap.
Parameters:
- `multiaddr`: multiaddress of the peer to swap with.
- `offerID`: ID of the swap offer.
- `providesAmount`: amount of ETH you will be providing. Must be between the offer's `minimumAmount * exchangeRate` and `maximumAmount * exchangeRate`. For example, if the offer has a minimum of 1 XMR and a maximum of 5 XMR and an exchange rate of 0.1, you must provide between 0.1 ETH and 0.5 ETH.
Returns:
- `id`: ID of the initiated swap.
- `status`: the swap's status.
Example:
```bash
wscat -c ws://localhost:8081
# Connected (press CTRL+C to quit)
# > {"jsonrpc":"2.0", "method":"net_takeOfferAndSubscribe", "params": {"multiaddr": "/ip4/192.168.0.101/tcp/9934/p2p/12D3KooWHLUrLnJtUbaGzTSi6azZavKhNgUZTtSiUZ9Uy12v1eZ7", "offerID": "cf4bf01a0775a0d13fa41b14516e4b89034300707a1754e0d99b65f6cb6fffb9", "providesAmount": 0.05}, "id": 0}
# < {"jsonrpc":"2.0","result":{"id":0},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"ExpectingKeys"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"ContractDeployed"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"ContractReady"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"Success"},"error":null,"id":null}
```

View File

@@ -11,6 +11,13 @@ type DaemonClient interface {
GenerateBlocks(address string, amount uint) error
}
// NewDaemonClient returns a new monerod client.
func NewDaemonClient(endpoint string) *client {
return &client{
endpoint: endpoint,
}
}
type generateBlocksRequest struct {
Address string `json:"wallet_address"`
AmountOfBlocks uint `json:"amount_of_blocks"`

View File

@@ -5,6 +5,7 @@ import (
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/common/types"
pcommon "github.com/noot/atomic-swap/protocol"
"github.com/fatih/color" //nolint:misspell
)
@@ -42,7 +43,7 @@ func (a *Instance) initiate(providesAmount common.EtherAmount) error {
return errors.New("balance lower than amount to be provided")
}
a.swapState, err = newSwapState(a, providesAmount)
a.swapState, err = newSwapState(a, pcommon.GetSwapInfoFilepath(a.basepath), providesAmount)
if err != nil {
return err
}

View File

@@ -12,6 +12,7 @@ import (
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/dleq"
pcommon "github.com/noot/atomic-swap/protocol"
"github.com/noot/atomic-swap/swapfactory"
)
@@ -54,6 +55,7 @@ func NewRecoveryState(a *Instance, secret *mcrypto.PrivateSpendKey,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
infofile: pcommon.GetSwapRecoveryFilepath(a.basepath),
}
rs := &recoveryState{

View File

@@ -33,6 +33,7 @@ type swapState struct {
ctx context.Context
cancel context.CancelFunc
sync.Mutex
infofile string
info *pswap.Info
statusCh chan types.Status
@@ -62,7 +63,7 @@ type swapState struct {
claimedCh chan struct{}
}
func newSwapState(a *Instance, providesAmount common.EtherAmount) (*swapState, error) {
func newSwapState(a *Instance, infofile string, providesAmount common.EtherAmount) (*swapState, error) {
txOpts, err := bind.NewKeyedTransactorWithChainID(a.ethPrivKey, a.chainID)
if err != nil {
return nil, err
@@ -84,6 +85,7 @@ func newSwapState(a *Instance, providesAmount common.EtherAmount) (*swapState, e
ctx: ctx,
cancel: cancel,
alice: a,
infofile: infofile,
txOpts: txOpts,
nextExpectedMessage: &net.SendKeysMessage{},
xmrLockedCh: make(chan struct{}),
@@ -92,6 +94,14 @@ func newSwapState(a *Instance, providesAmount common.EtherAmount) (*swapState, e
statusCh: statusCh,
}
if err := pcommon.WriteSwapIDToFile(infofile, info.ID()); err != nil {
return nil, err
}
if err := pcommon.WriteContractAddressToFile(s.infofile, a.contractAddr.String()); err != nil {
return nil, fmt.Errorf("failed to write contract address to file: %w", err)
}
return s, nil
}
@@ -109,6 +119,11 @@ func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
}, nil
}
// InfoFile returns the swap's infofile path
func (s *swapState) InfoFile() string {
return s.infofile
}
// ReceivedAmount returns the amount received, or expected to be received, at the end of the swap
func (s *swapState) ReceivedAmount() float64 {
return s.info.ReceivedAmount()
@@ -288,8 +303,7 @@ func (s *swapState) generateAndSetKeys() error {
s.privkeys = keysAndProof.PrivateKeyPair
s.pubkeys = keysAndProof.PublicKeyPair
fp := fmt.Sprintf("%s/%d/alice-secret", s.alice.basepath, s.info.ID())
return mcrypto.WriteKeysToFile(fp, s.privkeys, s.alice.env)
return pcommon.WriteKeysToFile(s.infofile, s.privkeys, s.alice.env)
}
// generateKeys generates Alice's monero spend and view keys (S_b, V_b), a secp256k1 public key,
@@ -402,8 +416,7 @@ func (s *swapState) claimMonero(skB *mcrypto.PrivateSpendKey) (mcrypto.Address,
kpAB := mcrypto.NewPrivateKeyPair(skAB, vkAB)
// write keys to file in case something goes wrong
fp := fmt.Sprintf("%s/%d/swap-secret", s.alice.basepath, s.info.ID())
if err := mcrypto.WriteKeysToFile(fp, kpAB, s.alice.env); err != nil {
if err := pcommon.WriteSharedSwapKeyPairToFile(s.infofile, kpAB, s.alice.env); err != nil {
return "", err
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/hex"
"math/big"
"os"
"testing"
"time"
@@ -25,6 +26,8 @@ import (
"github.com/stretchr/testify/require"
)
var infofile = os.TempDir() + "/test.keys"
var _ = logging.SetLogLevel("alice", "debug")
type mockNet struct {
@@ -68,7 +71,7 @@ func newTestAlice(t *testing.T) *Instance {
func newTestInstance(t *testing.T) (*Instance, *swapState) {
alice := newTestAlice(t)
swapState, err := newSwapState(alice, common.NewEtherAmount(1))
swapState, err := newSwapState(alice, infofile, common.NewEtherAmount(1))
require.NoError(t, err)
swapState.info.SetReceivedAmount(1)
return alice, swapState

View File

@@ -110,7 +110,7 @@ func NewInstance(cfg *Config) (*Instance, error) {
},
ethAddress: addr,
chainID: cfg.ChainID,
offerManager: newOfferManager(),
offerManager: newOfferManager(cfg.Basepath),
swapManager: cfg.SwapManager,
}, nil
}

View File

@@ -123,8 +123,7 @@ func (s *swapState) handleNotifyContractDeployed(msg *message.NotifyContractDepl
return nil, fmt.Errorf("failed to instantiate contract instance: %w", err)
}
fp := fmt.Sprintf("%s/%d/contractaddress", s.bob.basepath, s.ID())
if err := common.WriteContractAddressToFile(fp, msg.Address); err != nil {
if err := pcommon.WriteContractAddressToFile(s.infofile, msg.Address); err != nil {
return nil, fmt.Errorf("failed to write contract address to file: %w", err)
}

View File

@@ -15,7 +15,7 @@ func (b *Instance) Provides() types.ProvidesCoin {
return types.ProvidesXMR
}
func (b *Instance) initiate(offerID types.Hash, providesAmount common.MoneroAmount,
func (b *Instance) initiate(offer *types.Offer, offerExtra *types.OfferExtra, providesAmount common.MoneroAmount,
desiredAmount common.EtherAmount) error {
b.swapMu.Lock()
defer b.swapMu.Unlock()
@@ -34,7 +34,7 @@ func (b *Instance) initiate(offerID types.Hash, providesAmount common.MoneroAmou
return errors.New("balance lower than amount to be provided")
}
b.swapState, err = newSwapState(b, offerID, providesAmount, desiredAmount)
b.swapState, err = newSwapState(b, offer, offerExtra.StatusCh, offerExtra.InfoFile, providesAmount, desiredAmount)
if err != nil {
return err
}
@@ -58,7 +58,7 @@ func (b *Instance) HandleInitiateMessage(msg *net.SendKeysMessage) (net.SwapStat
return nil, nil, err
}
offer := b.offerManager.getOffer(id)
offer, offerExtra := b.offerManager.getAndDeleteOffer(id)
if offer == nil {
return nil, nil, errors.New("failed to find offer with given ID")
}
@@ -73,10 +73,13 @@ func (b *Instance) HandleInitiateMessage(msg *net.SendKeysMessage) (net.SwapStat
return nil, nil, errors.New("amount provided by taker is too low for offer")
}
if err = b.initiate(id, common.MoneroToPiconero(providedAmount), common.EtherToWei(msg.ProvidedAmount)); err != nil { //nolint:lll
if err = b.initiate(offer, offerExtra, common.MoneroToPiconero(providedAmount), common.EtherToWei(msg.ProvidedAmount)); err != nil { //nolint:lll
return nil, nil, err
}
offerExtra.IDCh <- b.swapState.info.ID()
close(offerExtra.IDCh)
if err = b.swapState.handleSendKeysMessage(msg); err != nil {
return nil, nil, err
}

View File

@@ -18,8 +18,12 @@ func TestBob_HandleInitiateMessage(t *testing.T) {
MaximumAmount: 0.002,
ExchangeRate: 0.1,
}
err := b.MakeOffer(offer)
extra, err := b.MakeOffer(offer)
require.NoError(t, err)
go func() {
<-extra.IDCh
}()
msg, _ := newTestAliceSendKeysMessage(t)
msg.OfferID = offer.GetID().String()
msg.ProvidedAmount = offer.MinimumAmount * float64(offer.ExchangeRate)

View File

@@ -5,44 +5,66 @@ import (
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/common/types"
pcommon "github.com/noot/atomic-swap/protocol"
)
type offerManager struct {
offers map[types.Hash]*types.Offer
type offerWithExtra struct {
offer *types.Offer
extra *types.OfferExtra
}
func newOfferManager() *offerManager {
type offerManager struct {
offers map[types.Hash]*offerWithExtra
basepath string
}
func newOfferManager(basepath string) *offerManager {
return &offerManager{
offers: make(map[types.Hash]*types.Offer),
offers: make(map[types.Hash]*offerWithExtra),
basepath: basepath,
}
}
func (om *offerManager) putOffer(o *types.Offer) {
om.offers[o.GetID()] = o
func (om *offerManager) putOffer(o *types.Offer) *types.OfferExtra {
extra := &types.OfferExtra{
IDCh: make(chan uint64, 1),
StatusCh: make(chan types.Status, 7),
InfoFile: pcommon.GetSwapInfoFilepath(om.basepath),
}
oe := &offerWithExtra{
offer: o,
extra: extra,
}
om.offers[o.GetID()] = oe
return extra
}
func (om *offerManager) getOffer(id types.Hash) *types.Offer {
return om.offers[id]
}
func (om *offerManager) getAndDeleteOffer(id types.Hash) (*types.Offer, *types.OfferExtra) {
offer, has := om.offers[id]
if !has {
return nil, nil
}
func (om *offerManager) deleteOffer(id types.Hash) {
delete(om.offers, id)
return offer.offer, offer.extra
}
// MakeOffer makes a new swap offer.
func (b *Instance) MakeOffer(o *types.Offer) error {
func (b *Instance) MakeOffer(o *types.Offer) (*types.OfferExtra, error) {
balance, err := b.client.GetBalance(0)
if err != nil {
return err
return nil, err
}
if common.MoneroAmount(balance.UnlockedBalance) < common.MoneroToPiconero(o.MaximumAmount) {
return errors.New("unlocked balance is less than maximum offer amount")
return nil, errors.New("unlocked balance is less than maximum offer amount")
}
b.offerManager.putOffer(o)
extra := b.offerManager.putOffer(o)
log.Infof("created new offer: %v", o)
return nil
return extra, nil
}
// GetOffers returns all current offers.
@@ -50,7 +72,7 @@ func (b *Instance) GetOffers() []*types.Offer {
offers := make([]*types.Offer, len(b.offerManager.offers))
i := 0
for _, o := range b.offerManager.offers {
offers[i] = o
offers[i] = o.offer
i++
}
return offers

View File

@@ -10,6 +10,7 @@ import (
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/dleq"
pcommon "github.com/noot/atomic-swap/protocol"
)
type recoveryState struct {
@@ -48,6 +49,7 @@ func NewRecoveryState(b *Instance, secret *mcrypto.PrivateSpendKey,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
infofile: pcommon.GetSwapRecoveryFilepath(b.basepath),
}
if err := s.setContract(contractAddr); err != nil {

View File

@@ -41,9 +41,10 @@ type swapState struct {
ctx context.Context
cancel context.CancelFunc
sync.Mutex
infofile string
info *pswap.Info
offerID types.Hash
offer *types.Offer
statusCh chan types.Status
// our keys for this session
@@ -69,11 +70,12 @@ type swapState struct {
// channels
readyCh chan struct{}
// address of reclaimed monero wallet, if the swap is refunded
// address of reclaimed monero wallet, if the swap is refunded77
moneroReclaimAddress mcrypto.Address
}
func newSwapState(b *Instance, offerID types.Hash, providesAmount common.MoneroAmount, desiredAmount common.EtherAmount) (*swapState, error) { //nolint:lll
func newSwapState(b *Instance, offer *types.Offer, statusCh chan types.Status, infofile string,
providesAmount common.MoneroAmount, desiredAmount common.EtherAmount) (*swapState, error) {
txOpts, err := bind.NewKeyedTransactorWithChainID(b.ethPrivKey, b.chainID)
if err != nil {
return nil, err
@@ -84,7 +86,9 @@ func newSwapState(b *Instance, offerID types.Hash, providesAmount common.MoneroA
exchangeRate := types.ExchangeRate(providesAmount.AsMonero() / desiredAmount.AsEther())
stage := types.ExpectingKeys
statusCh := make(chan types.Status, 7)
if statusCh == nil {
statusCh = make(chan types.Status, 7)
}
statusCh <- stage
info := pswap.NewInfo(types.ProvidesXMR, providesAmount.AsMonero(), desiredAmount.AsEther(),
exchangeRate, stage, statusCh)
@@ -97,7 +101,8 @@ func newSwapState(b *Instance, offerID types.Hash, providesAmount common.MoneroA
ctx: ctx,
cancel: cancel,
bob: b,
offerID: offerID,
offer: offer,
infofile: infofile,
nextExpectedMessage: &net.SendKeysMessage{},
readyCh: make(chan struct{}),
txOpts: txOpts,
@@ -105,6 +110,10 @@ func newSwapState(b *Instance, offerID types.Hash, providesAmount common.MoneroA
statusCh: statusCh,
}
if err := pcommon.WriteSwapIDToFile(infofile, info.ID()); err != nil {
return nil, err
}
return s, nil
}
@@ -124,6 +133,11 @@ func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
}, nil
}
// InfoFile returns the swap's infofile path
func (s *swapState) InfoFile() string {
return s.infofile
}
// ReceivedAmount returns the amount received, or expected to be received, at the end of the swap
func (s *swapState) ReceivedAmount() float64 {
return s.info.ReceivedAmount()
@@ -145,14 +159,16 @@ func (s *swapState) ProtocolExited() error {
s.cancel()
s.bob.swapState = nil
s.bob.swapManager.CompleteOngoingSwap()
if s.info.Status() != types.CompletedSuccess {
// re-add offer, as it wasn't taken successfully
s.bob.offerManager.putOffer(s.offer)
}
}()
if s.info.Status() == types.CompletedSuccess {
str := color.New(color.Bold).Sprintf("**swap completed successfully: id=%d**", s.ID())
log.Info(str)
// remove offer, as it's been taken
s.bob.offerManager.deleteOffer(s.offerID)
return nil
}
@@ -222,8 +238,7 @@ func (s *swapState) reclaimMonero(skA *mcrypto.PrivateSpendKey) (mcrypto.Address
kpAB := mcrypto.NewPrivateKeyPair(skAB, vkAB)
// write keys to file in case something goes wrong
fp := fmt.Sprintf("%s/%d/swap-secret", s.bob.basepath, s.ID())
if err = mcrypto.WriteKeysToFile(fp, kpAB, s.bob.env); err != nil {
if err = pcommon.WriteSharedSwapKeyPairToFile(s.infofile, kpAB, s.bob.env); err != nil {
return "", err
}
@@ -293,8 +308,7 @@ func (s *swapState) generateAndSetKeys() error {
s.privkeys = keysAndProof.PrivateKeyPair
s.pubkeys = keysAndProof.PublicKeyPair
fp := fmt.Sprintf("%s/%d/bob-secret", s.bob.basepath, s.ID())
return mcrypto.WriteKeysToFile(fp, s.privkeys, s.bob.env)
return pcommon.WriteKeysToFile(s.infofile, s.privkeys, s.bob.env)
}
func generateKeys() (*pcommon.KeysAndProof, error) {

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/hex"
"math/big"
"os"
"testing"
"time"
@@ -22,6 +23,8 @@ import (
"github.com/stretchr/testify/require"
)
var infofile = os.TempDir() + "/test.keys"
var (
_ = logging.SetLogLevel("bob", "debug")
testWallet = "test-wallet"
@@ -77,8 +80,7 @@ func newTestBob(t *testing.T) *Instance {
func newTestInstance(t *testing.T) (*Instance, *swapState) {
bob := newTestBob(t)
swapState, err := newSwapState(bob, types.Hash{}, common.MoneroAmount(33), desiredAmout)
swapState, err := newSwapState(bob, &types.Offer{}, nil, infofile, common.MoneroAmount(33), desiredAmout)
require.NoError(t, err)
return bob, swapState
}
@@ -420,34 +422,31 @@ func TestSwapState_ProtocolExited_Aborted(t *testing.T) {
func TestSwapState_ProtocolExited_Success(t *testing.T) {
b, s := newTestInstance(t)
offer := &types.Offer{
s.offer = &types.Offer{
Provides: types.ProvidesXMR,
MinimumAmount: 0.1,
MaximumAmount: 0.2,
ExchangeRate: 0.1,
}
b.MakeOffer(offer)
s.offerID = offer.GetID()
s.info.SetStatus(types.CompletedSuccess)
err := s.ProtocolExited()
require.NoError(t, err)
require.Nil(t, b.offerManager.getOffer(offer.GetID()))
require.Nil(t, b.offerManager.offers[s.offer.GetID()])
}
func TestSwapState_ProtocolExited_Refunded(t *testing.T) {
b, s := newTestInstance(t)
offer := &types.Offer{
s.offer = &types.Offer{
Provides: types.ProvidesXMR,
MinimumAmount: 0.1,
MaximumAmount: 0.2,
ExchangeRate: 0.1,
}
b.MakeOffer(offer)
s.offerID = offer.GetID()
b.MakeOffer(s.offer)
s.info.SetStatus(types.CompletedRefund)
err := s.ProtocolExited()
require.NoError(t, err)
require.NotNil(t, b.offerManager.getOffer(offer.GetID()))
require.NotNil(t, b.offerManager.offers[s.offer.GetID()])
}

View File

@@ -104,14 +104,16 @@ func NewInfo(provides types.ProvidesCoin, providedAmount, receivedAmount float64
// Manager tracks current and past swaps.
type Manager struct {
sync.RWMutex
ongoing *Info
past map[uint64]*Info
ongoing *Info
past map[uint64]*Info
offersTaken map[string]uint64 // map of offerID -> swapID
}
// NewManager ...
func NewManager() *Manager {
return &Manager{
past: make(map[uint64]*Info),
past: make(map[uint64]*Info),
offersTaken: make(map[string]uint64),
}
}

20
protocol/utils.go Normal file
View File

@@ -0,0 +1,20 @@
package protocol
import (
"fmt"
"time"
)
// GetSwapInfoFilepath returns an info file path with the current timestamp.
func GetSwapInfoFilepath(basepath string) string {
t := time.Now().Format("2006-Jan-2-15:04:05")
path := fmt.Sprintf("%s/info-%s.txt", basepath, t)
return path
}
// GetSwapRecoveryFilepath returns an info file path with the current timestamp.
func GetSwapRecoveryFilepath(basepath string) string {
t := time.Now().Format("2006-Jan-2-15:04:05")
path := fmt.Sprintf("%s/recovery-%s.txt", basepath, t)
return path
}

164
protocol/write.go Normal file
View File

@@ -0,0 +1,164 @@
package protocol
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/noot/atomic-swap/common"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
)
type infoFileContents struct {
ContractAddress string
SwapID uint64
PrivateKeyInfo *mcrypto.PrivateKeyInfo
SharedSwapPrivateKey *mcrypto.PrivateKeyInfo
}
// WriteContractAddressToFile writes the contract address to the given file
func WriteContractAddressToFile(infofile, addr string) error {
file, contents, err := setupFile(infofile)
if err != nil {
return err
}
contents.ContractAddress = addr
bz, err := json.Marshal(contents)
if err != nil {
return err
}
_, err = file.Write(bz)
return err
}
// WriteSwapIDToFile writes the swap ID to the given file
func WriteSwapIDToFile(infofile string, id uint64) error {
file, contents, err := setupFile(infofile)
if err != nil {
return err
}
contents.SwapID = id
bz, err := json.Marshal(contents)
if err != nil {
return err
}
_, err = file.Write(bz)
return err
}
// WriteKeysToFile writes the given private key pair to the given file
func WriteKeysToFile(infofile string, keys *mcrypto.PrivateKeyPair, env common.Environment) error {
file, contents, err := setupFile(infofile)
if err != nil {
return err
}
contents.PrivateKeyInfo = keys.Info(env)
bz, err := json.Marshal(contents)
if err != nil {
return err
}
_, err = file.Write(bz)
return err
}
// WriteSharedSwapKeyPairToFile writes the given private key pair to the given file
func WriteSharedSwapKeyPairToFile(infofile string, keys *mcrypto.PrivateKeyPair, env common.Environment) error {
file, contents, err := setupFile(infofile)
if err != nil {
return err
}
contents.SharedSwapPrivateKey = keys.Info(env)
bz, err := json.Marshal(contents)
if err != nil {
return err
}
_, err = file.Write(bz)
return err
}
func setupFile(infofile string) (*os.File, *infoFileContents, error) {
exists, err := exists(infofile)
if err != nil {
return nil, nil, err
}
var (
file *os.File
contents *infoFileContents
)
if !exists {
err = makeDir(filepath.Dir(infofile))
if err != nil {
return nil, nil, fmt.Errorf("failed to make directory %s: %w", filepath.Dir(infofile), err)
}
file, err = os.Create(filepath.Clean(infofile))
if err != nil {
return nil, nil, fmt.Errorf("failed to create file %s: %w", filepath.Clean(infofile), err)
}
} else {
file, err = os.OpenFile(filepath.Clean(infofile), os.O_RDWR, 0600)
if err != nil {
return nil, nil, err
}
bz, err := os.ReadFile(filepath.Clean(infofile))
if err != nil {
return nil, nil, err
}
if err = json.Unmarshal(bz, &contents); err != nil {
return nil, nil, err
}
if err = file.Truncate(0); err != nil {
return nil, nil, err
}
_, err = file.Seek(0, 0)
if err != nil {
return nil, nil, err
}
}
if contents == nil {
contents = &infoFileContents{}
}
return file, contents, nil
}
func makeDir(dir string) error {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
return nil
}
// exists returns whether the given file or directory exists
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

25
protocol/write_test.go Normal file
View File

@@ -0,0 +1,25 @@
package protocol
import (
"os"
"testing"
"github.com/noot/atomic-swap/common"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/stretchr/testify/require"
)
func TestWriteKeysToFile(t *testing.T) {
kp, err := mcrypto.GenerateKeys()
require.NoError(t, err)
err = WriteKeysToFile(os.TempDir()+"/test.keys", kp, common.Development)
require.NoError(t, err)
}
func TestWriteContractAddrssToFile(t *testing.T) {
addr := "0xabcd"
err := WriteContractAddressToFile(os.TempDir()+"/test.keys", addr)
require.NoError(t, err)
}

View File

@@ -132,30 +132,32 @@ type TakeOfferRequest struct {
// TakeOfferResponse ...
type TakeOfferResponse struct {
ID uint64 `json:"id"`
ID uint64 `json:"id"`
InfoFile string `json:"infoFile"`
}
// TakeOffer initiates a swap with the given peer by taking an offer they've made.
func (s *NetService) TakeOffer(_ *http.Request, req *TakeOfferRequest, resp *TakeOfferResponse) error {
id, _, err := s.takeOffer(req.Multiaddr, req.OfferID, req.ProvidesAmount)
id, _, infofile, err := s.takeOffer(req.Multiaddr, req.OfferID, req.ProvidesAmount)
if err != nil {
return err
}
resp.ID = id
resp.InfoFile = infofile
return nil
}
func (s *NetService) takeOffer(multiaddr, offerID string,
providesAmount float64) (uint64, <-chan types.Status, error) {
providesAmount float64) (uint64, <-chan types.Status, string, error) {
swapState, err := s.alice.InitiateProtocol(providesAmount)
if err != nil {
return 0, nil, err
return 0, nil, "", err
}
skm, err := swapState.SendKeysMessage()
if err != nil {
return 0, nil, err
return 0, nil, "", err
}
skm.OfferID = offerID
@@ -163,26 +165,27 @@ func (s *NetService) takeOffer(multiaddr, offerID string,
who, err := net.StringToAddrInfo(multiaddr)
if err != nil {
return 0, nil, err
return 0, nil, "", err
}
if err = s.net.Initiate(who, skm, swapState); err != nil {
_ = swapState.ProtocolExited()
return 0, nil, err
return 0, nil, "", err
}
info := s.sm.GetOngoingSwap()
if info == nil {
return 0, nil, errors.New("failed to get swap info after initiating")
return 0, nil, "", errors.New("failed to get swap info after initiating")
}
return swapState.ID(), info.StatusCh(), nil
return swapState.ID(), info.StatusCh(), swapState.InfoFile(), nil
}
// TakeOfferSyncResponse ...
type TakeOfferSyncResponse struct {
ID uint64 `json:"id"`
Status string `json:"status"`
ID uint64 `json:"id"`
InfoFile string `json:"infoFile"`
Status string `json:"status"`
}
// TakeOfferSync initiates a swap with the given peer by taking an offer they've made.
@@ -213,6 +216,7 @@ func (s *NetService) TakeOfferSync(_ *http.Request, req *TakeOfferRequest,
}
resp.ID = swapState.ID()
resp.InfoFile = swapState.InfoFile()
const checkSwapSleepDuration = time.Millisecond * 100
@@ -240,11 +244,24 @@ type MakeOfferRequest struct {
// MakeOfferResponse ...
type MakeOfferResponse struct {
ID string `json:"offerID"`
ID string `json:"offerID"`
InfoFile string `json:"infoFile"`
}
// MakeOffer creates and advertises a new swap offer.
func (s *NetService) MakeOffer(_ *http.Request, req *MakeOfferRequest, resp *MakeOfferResponse) error {
id, extra, err := s.makeOffer(req)
if err != nil {
return err
}
resp.ID = id
resp.InfoFile = extra.InfoFile
s.net.Advertise()
return nil
}
func (s *NetService) makeOffer(req *MakeOfferRequest) (string, *types.OfferExtra, error) {
o := &types.Offer{
Provides: types.ProvidesXMR,
MinimumAmount: req.MinimumAmount,
@@ -252,14 +269,12 @@ func (s *NetService) MakeOffer(_ *http.Request, req *MakeOfferRequest, resp *Mak
ExchangeRate: req.ExchangeRate,
}
if err := s.bob.MakeOffer(o); err != nil {
return err
offerExtra, err := s.bob.MakeOffer(o)
if err != nil {
return "", nil, err
}
resp.ID = o.GetID().String()
s.net.Advertise()
return nil
return o.GetID().String(), offerExtra, nil
}
// SetGasPriceRequest ...

View File

@@ -58,7 +58,7 @@ func NewServer(cfg *Config) (*Server, error) {
return &Server{
s: s,
wsServer: newWsServer(cfg.Ctx, cfg.SwapManager, cfg.Alice, cfg.Bob, ns),
wsServer: newWsServer(cfg.Ctx, cfg.SwapManager, ns),
port: cfg.Port,
wsPort: cfg.WsPort,
}, nil
@@ -120,8 +120,9 @@ type Alice interface {
// Bob ...
type Bob interface {
Protocol
MakeOffer(offer *types.Offer) error
MakeOffer(offer *types.Offer) (*types.OfferExtra, error)
SetMoneroWalletFile(file, password string) error
GetOffers() []*types.Offer
}
// SwapManager ...

View File

@@ -132,3 +132,14 @@ func (s *SwapService) GetStage(_ *http.Request, _ *interface{}, resp *GetStageRe
resp.Info = info.Status().Info()
return nil
}
// GetOffersResponse ...
type GetOffersResponse struct {
Offers []*types.Offer `json:"offers"`
}
// GetOffers returns the currently available offers.
func (s *SwapService) GetOffers(_ *http.Request, _ *interface{}, resp *GetOffersResponse) error {
resp.Offers = s.bob.GetOffers()
return nil
}

152
rpc/ws.go
View File

@@ -15,6 +15,7 @@ import (
const (
subscribeNewPeer = "net_subscribeNewPeer"
subscribeMakeOffer = "net_makeOfferAndSubscribe"
subscribeTakeOffer = "net_takeOfferAndSubscribe"
subscribeSwapStatus = "swap_subscribeStatus"
)
@@ -29,20 +30,16 @@ type (
)
type wsServer struct {
ctx context.Context
sm SwapManager
alice Alice
bob Bob
ns *NetService
ctx context.Context
sm SwapManager
ns *NetService
}
func newWsServer(ctx context.Context, sm SwapManager, a Alice, b Bob, ns *NetService) *wsServer {
func newWsServer(ctx context.Context, sm SwapManager, ns *NetService) *wsServer {
return &wsServer{
ctx: ctx,
sm: sm,
alice: a,
bob: b,
ns: ns,
ctx: ctx,
sm: sm,
ns: ns,
}
}
@@ -83,67 +80,49 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *Request) error {
case subscribeNewPeer:
return errors.New("unimplemented")
case subscribeSwapStatus:
idi, has := req.Params["id"] // TODO: make const
if !has {
return errors.New("params missing id field")
var params *rpcclient.SubscribeSwapStatusRequestParams
if err := json.Unmarshal(req.Params, &params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
}
id, ok := idi.(float64)
if !ok {
return fmt.Errorf("failed to cast id parameter to float64: got %T", idi)
}
return s.subscribeSwapStatus(s.ctx, conn, uint64(id))
return s.subscribeSwapStatus(s.ctx, conn, params.ID)
case subscribeTakeOffer:
maddri, has := req.Params["multiaddr"]
if !has {
return errors.New("params missing multiaddr field")
var params *rpcclient.SubscribeTakeOfferParams
if err := json.Unmarshal(req.Params, &params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
}
maddr, ok := maddri.(string)
if !ok {
return fmt.Errorf("failed to cast multiaddr parameter to string: got %T", maddri)
}
offerIDi, has := req.Params["offerID"]
if !has {
return errors.New("params missing offerID field")
}
offerID, ok := offerIDi.(string)
if !ok {
return fmt.Errorf("failed to cast multiaddr parameter to string: got %T", offerIDi)
}
providesi, has := req.Params["providesAmount"]
if !has {
return errors.New("params missing providesAmount field")
}
providesAmount, ok := providesi.(float64)
if !ok {
return fmt.Errorf("failed to cast providesAmount parameter to float64: got %T", providesi)
}
id, ch, err := s.ns.takeOffer(maddr, offerID, providesAmount)
id, ch, infofile, err := s.ns.takeOffer(params.Multiaddr, params.OfferID, params.ProvidesAmount)
if err != nil {
return err
}
return s.subscribeTakeOffer(s.ctx, conn, id, ch)
return s.subscribeTakeOffer(s.ctx, conn, id, ch, infofile)
case subscribeMakeOffer:
var params *MakeOfferRequest
if err := json.Unmarshal(req.Params, &params); err != nil {
return fmt.Errorf("failed to unmarshal parameters: %w", err)
}
offerID, offerExtra, err := s.ns.makeOffer(params)
if err != nil {
return err
}
return s.subscribeMakeOffer(s.ctx, conn, offerID, offerExtra)
default:
return errors.New("invalid method")
}
}
func (s *wsServer) subscribeTakeOffer(ctx context.Context, conn *websocket.Conn,
id uint64, statusCh <-chan types.Status) error {
// firstly write swap ID
idMsg := map[string]uint64{
"id": id,
id uint64, statusCh <-chan types.Status, infofile string) error {
resp := &TakeOfferResponse{
ID: id,
InfoFile: infofile,
}
if err := writeResponse(conn, idMsg); err != nil {
if err := writeResponse(conn, resp); err != nil {
return err
}
@@ -155,7 +134,64 @@ func (s *wsServer) subscribeTakeOffer(ctx context.Context, conn *websocket.Conn,
}
resp := &SubscribeSwapStatusResponse{
Stage: status.String(),
Status: status.String(),
}
if err := writeResponse(conn, resp); err != nil {
return err
}
case <-ctx.Done():
return nil
}
}
}
func (s *wsServer) subscribeMakeOffer(ctx context.Context, conn *websocket.Conn,
offerID string, offerExtra *types.OfferExtra) error {
// firstly write offer ID
resp := &MakeOfferResponse{
ID: offerID,
InfoFile: offerExtra.InfoFile,
}
if err := writeResponse(conn, resp); err != nil {
return err
}
// then check for swap ID to be sent when swap is initiated
var taken bool
for {
if taken {
break
}
select {
case id := <-offerExtra.IDCh:
idMsg := map[string]uint64{
"id": id,
}
if err := writeResponse(conn, idMsg); err != nil {
return err
}
taken = true
case <-ctx.Done():
return nil
}
}
// finally, read the swap's status
for {
select {
case status, ok := <-offerExtra.StatusCh:
if !ok {
return nil
}
resp := &SubscribeSwapStatusResponse{
Status: status.String(),
}
if err := writeResponse(conn, resp); err != nil {
@@ -185,7 +221,7 @@ func (s *wsServer) subscribeSwapStatus(ctx context.Context, conn *websocket.Conn
}
resp := &SubscribeSwapStatusResponse{
Stage: status.String(),
Status: status.String(),
}
if err := writeResponse(conn, resp); err != nil {
@@ -204,7 +240,7 @@ func (s *wsServer) writeSwapExitStatus(conn *websocket.Conn, id uint64) error {
}
resp := &SubscribeSwapStatusResponse{
Stage: info.Status().String(),
Status: info.Status().String(),
}
if err := writeResponse(conn, resp); err != nil {

201
rpc/ws_test.go Normal file
View File

@@ -0,0 +1,201 @@
package rpc
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/common/rpcclient"
"github.com/noot/atomic-swap/common/types"
"github.com/noot/atomic-swap/net"
"github.com/noot/atomic-swap/net/message"
"github.com/noot/atomic-swap/protocol/swap"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/stretchr/testify/require"
)
const (
testSwapID uint64 = 77
testMultiaddr = "/ip4/192.168.0.102/tcp/9933/p2p/12D3KooWAYn1T8Lu122Pav4zAogjpeU61usLTNZpLRNh9gCqY6X2"
)
var (
testTImeout = time.Second * 5
defaultRPCPort uint16 = 3001
defaultWSPort uint16 = 4002
)
func defaultWSEndpoint() string {
return fmt.Sprintf("ws://localhost:%d", defaultWSPort)
}
type mockNet struct{}
func (*mockNet) Addresses() []string {
return nil
}
func (*mockNet) Advertise() {}
func (*mockNet) Discover(provides types.ProvidesCoin, searchTime time.Duration) ([]peer.AddrInfo, error) {
return nil, nil
}
func (*mockNet) Query(who peer.AddrInfo) (*net.QueryResponse, error) {
return nil, nil
}
func (*mockNet) Initiate(who peer.AddrInfo, msg *net.SendKeysMessage, s common.SwapState) error {
return nil
}
type mockSwapManager struct{}
func (*mockSwapManager) GetPastIDs() []uint64 {
return []uint64{}
}
func (*mockSwapManager) GetPastSwap(id uint64) *swap.Info {
return &swap.Info{}
}
func (*mockSwapManager) GetOngoingSwap() *swap.Info {
statusCh := make(chan types.Status, 1)
statusCh <- types.CompletedSuccess
return swap.NewInfo(
types.ProvidesETH,
1,
1,
1,
types.CompletedSuccess,
statusCh,
)
}
type mockAlice struct{}
func (*mockAlice) Provides() types.ProvidesCoin {
return types.ProvidesETH
}
func (*mockAlice) SetGasPrice(gasPrice uint64) {}
func (*mockAlice) GetOngoingSwapState() common.SwapState {
return new(mockSwapState)
}
func (*mockAlice) InitiateProtocol(providesAmount float64) (common.SwapState, error) {
return new(mockSwapState), nil
}
func (*mockAlice) Refund() (ethcommon.Hash, error) {
return ethcommon.Hash{}, nil
}
type mockSwapState struct{}
func (*mockSwapState) HandleProtocolMessage(msg message.Message) (resp message.Message, done bool, err error) {
return nil, true, nil
}
func (*mockSwapState) ProtocolExited() error {
return nil
}
func (*mockSwapState) SendKeysMessage() (*message.SendKeysMessage, error) {
return &message.SendKeysMessage{}, nil
}
func (*mockSwapState) ID() uint64 {
return testSwapID
}
func (*mockSwapState) InfoFile() string {
return os.TempDir() + "test.infofile"
}
func newServer(t *testing.T) *Server {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(func() {
cancel()
})
defaultRPCPort++
defaultWSPort++
cfg := &Config{
Ctx: ctx,
Port: defaultRPCPort,
WsPort: defaultWSPort,
Net: new(mockNet),
SwapManager: new(mockSwapManager),
Alice: new(mockAlice),
}
s, err := NewServer(cfg)
require.NoError(t, err)
errCh := s.Start()
go func() {
err := <-errCh
require.NoError(t, err)
}()
time.Sleep(time.Millisecond * 300) // let server start up
return s
}
func TestSubscribeSwapStatus(t *testing.T) {
_ = newServer(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(func() {
cancel()
})
c, err := rpcclient.NewWsClient(ctx, defaultWSEndpoint())
require.NoError(t, err)
ch, err := c.SubscribeSwapStatus(testSwapID)
require.NoError(t, err)
select {
case status := <-ch:
require.Equal(t, types.CompletedSuccess, status)
case <-time.After(testTImeout):
t.Fatal("test timed out")
}
}
// TODO: add unit test
// func TestSubscribeMakeOffer(t *testing.T) {
// _ = newServer(t)
// ctx, cancel := context.WithCancel(context.Background())
// t.Cleanup(func() {
// cancel()
// })
// c, err := rpcclient.NewWsClient(ctx, defaultWSEndpoint())
// require.NoError(t, err)
// id, ch, err := c.MakeOfferAndSubscribe(0.1, 1, 0.05)
// require.NoError(t, err)
// require.Equal(t, id, testSwapID)
// select {
// case status := <-ch:
// require.Equal(t, types.CompletedSuccess, status)
// case <-time.After(testTImeout):
// t.Fatal("test timed out")
// }
// }
func TestSubscribeTakeOffer(t *testing.T) {
_ = newServer(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(func() {
cancel()
})
c, err := rpcclient.NewWsClient(ctx, defaultWSEndpoint())
require.NoError(t, err)
id, ch, err := c.TakeOfferAndSubscribe(testMultiaddr, "", 1)
require.NoError(t, err)
require.Equal(t, id, testSwapID)
select {
case status := <-ch:
require.Equal(t, types.CompletedSuccess, status)
case <-time.After(testTImeout):
t.Fatal("test timed out")
}
}

View File

@@ -31,15 +31,28 @@ GANACHE_CLI_PID=$!
# wait for servers to start
sleep 10
# run unit tests
echo "running unit tests..."
go test ./tests -v
# start alice and bob swapd instances
echo "starting alice, logs in ./tests/alice.log"
bash scripts/build.sh
./swapd --dev-alice --libp2p-key=./tests/alice.key &> ./tests/alice.log &
ALICE_PID=$!
sleep 3
echo "starting bob, logs in ./tests/bob.log"
./swapd --dev-bob --bootnodes /ip4/127.0.0.1/tcp/9933/p2p/12D3KooWAYn1T8Lu122Pav4zAogjpeU61usLTNZpLRNh9gCqY6X2 --wallet-file test-wallet &> ./tests/bob.log &
BOB_PID=$!
sleep 3
# run tests
echo "running integration tests..."
TESTS=integration go test ./tests -v
OK=$?
# kill processes
kill $MONERO_WALLET_CLI_BOB_PID
kill $MONERO_WALLET_CLI_ALICE_PID
kill $GANACHE_CLI_PID
kill $ALICE_PID
kill $BOB_PID
# rm -rf ./alice-test-keys
# rm -rf ./bob-test-keys
exit $OK

0
scripts/setup-env.sh Normal file → Executable file
View File

1
tests/alice.key Normal file
View File

@@ -0,0 +1 @@
b0d2d5dac3cc02d8db12e4a9703d6175cd4e9531027268b0566f50b85ca2e9410add5985d336d208fcb66c429f3abab7ae4d42f94714b54ea6fb8558d3a72df9

View File

@@ -1,27 +1,36 @@
package tests
import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"sync"
"testing"
"time"
"github.com/noot/atomic-swap/cmd/client/client"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/common/rpcclient"
"github.com/noot/atomic-swap/common/types"
"github.com/noot/atomic-swap/monero"
"github.com/stretchr/testify/require"
)
const (
defaultAliceTestLibp2pKey = "alice.key"
defaultAliceDaemonEndpoint = "http://localhost:5001"
defaultBobDaemonEndpoint = "http://localhost:5002"
defaultDiscoverTimeout = 2 // 2 seconds
testsEnv = "TESTS"
integrationMode = "integration"
generateBlocksEnv = "GENERATEBLOCKS"
bobProvideAmount = float64(44.4)
defaultAliceDaemonEndpoint = "http://localhost:5001"
defaultAliceDaemonWSEndpoint = "ws://localhost:8081"
defaultBobDaemonEndpoint = "http://localhost:5002"
defaultBobDaemonWSEndpoint = "ws://localhost:8082"
defaultDiscoverTimeout = 2 // 2 seconds
bobProvideAmount = float64(1.0)
exchangeRate = float64(0.05)
)
@@ -31,123 +40,32 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
cmd := exec.Command("../scripts/build.sh")
out, err := cmd.CombinedOutput()
if os.Getenv(testsEnv) != integrationMode {
os.Exit(0)
}
c := monero.NewClient(common.DefaultBobMoneroEndpoint)
d := monero.NewDaemonClient(common.DefaultMoneroDaemonEndpoint)
bobAddr, err := c.GetAddress(0)
if err != nil {
panic(fmt.Sprintf("%s\n%s", out, err))
panic(err)
}
if os.Getenv(generateBlocksEnv) != "false" {
fmt.Println("> Generating blocks for test setup...")
_ = d.GenerateBlocks(bobAddr.Address, 512)
err = c.Refresh()
if err != nil {
panic(err)
}
fmt.Println("> Completed generating blocks.")
}
os.Exit(m.Run())
}
func startSwapDaemon(t *testing.T, done <-chan struct{}, args ...string) {
cmd := exec.Command("../swapd", args...)
wg := new(sync.WaitGroup)
wg.Add(2)
type errOut struct {
err error
out string
}
errCh := make(chan *errOut)
go func() {
out, err := cmd.CombinedOutput()
if err != nil {
errCh <- &errOut{
err: err,
out: string(out),
}
}
wg.Done()
}()
go func() {
defer wg.Done()
select {
case <-done:
_ = cmd.Process.Kill()
_ = cmd.Wait()
// drain errCh
<-errCh
return
case err := <-errCh:
fmt.Println("program exited early: ", err.err)
fmt.Println("output: ", err.out)
}
}()
t.Cleanup(func() {
wg.Wait()
})
time.Sleep(time.Second * 2)
}
func startAlice(t *testing.T, done <-chan struct{}) []string {
startSwapDaemon(t, done, "--dev-alice",
"--libp2p-key", defaultAliceTestLibp2pKey,
)
c := client.NewClient(defaultAliceDaemonEndpoint)
addrs, err := c.Addresses()
require.NoError(t, err)
require.GreaterOrEqual(t, len(addrs), 1)
return addrs
}
func startBob(t *testing.T, done <-chan struct{}, aliceMultiaddr string) {
startSwapDaemon(t, done, "--dev-bob",
"--bootnodes", aliceMultiaddr,
"--wallet-file", "test-wallet",
)
}
// charlie doesn't provide any coin or participate in any swap.
// he is just a node running the p2p protocol.
func startCharlie(t *testing.T, done <-chan struct{}, aliceMultiaddr string) {
startSwapDaemon(t, done,
"--libp2p-port", "9955",
"--rpc-port", "5003",
"--bootnodes", aliceMultiaddr)
}
func startNodes(t *testing.T) {
done := make(chan struct{})
addrs := startAlice(t, done)
startBob(t, done, addrs[0])
startCharlie(t, done, addrs[0])
t.Cleanup(func() {
close(done)
})
}
func TestStartAlice(t *testing.T) {
done := make(chan struct{})
_ = startAlice(t, done)
close(done)
}
func TestStartBob(t *testing.T) {
done := make(chan struct{})
addrs := startAlice(t, done)
startBob(t, done, addrs[0])
close(done)
}
func TestStartCharlie(t *testing.T) {
done := make(chan struct{})
addrs := startAlice(t, done)
startCharlie(t, done, addrs[0])
close(done)
}
func TestAlice_Discover(t *testing.T) {
startNodes(t)
bc := client.NewClient(defaultBobDaemonEndpoint)
_, err := bc.MakeOffer(bobProvideAmount, bobProvideAmount, exchangeRate)
require.NoError(t, err)
@@ -160,7 +78,6 @@ func TestAlice_Discover(t *testing.T) {
}
func TestBob_Discover(t *testing.T) {
startNodes(t)
c := client.NewClient(defaultBobDaemonEndpoint)
providers, err := c.Discover(types.ProvidesETH, defaultDiscoverTimeout)
require.NoError(t, err)
@@ -168,7 +85,6 @@ func TestBob_Discover(t *testing.T) {
}
func TestAlice_Query(t *testing.T) {
startNodes(t)
bc := client.NewClient(defaultBobDaemonEndpoint)
_, err := bc.MakeOffer(bobProvideAmount, bobProvideAmount, exchangeRate)
require.NoError(t, err)
@@ -182,27 +98,98 @@ func TestAlice_Query(t *testing.T) {
resp, err := c.Query(providers[0][0])
require.NoError(t, err)
require.Equal(t, 1, len(resp.Offers))
require.GreaterOrEqual(t, len(resp.Offers), 1)
require.Equal(t, bobProvideAmount, resp.Offers[0].MinimumAmount)
require.Equal(t, bobProvideAmount, resp.Offers[0].MaximumAmount)
require.Equal(t, exchangeRate, float64(resp.Offers[0].ExchangeRate))
}
func TestAlice_TakeOffer(t *testing.T) {
startNodes(t)
func TestTakeOffer_HappyPath(t *testing.T) {
const testTimeout = time.Second * 5
bc := client.NewClient(defaultBobDaemonEndpoint)
offerID, err := bc.MakeOffer(0.1, bobProvideAmount, exchangeRate)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bwsc, err := rpcclient.NewWsClient(ctx, defaultBobDaemonWSEndpoint)
require.NoError(t, err)
c := client.NewClient(defaultAliceDaemonEndpoint)
offerID, takenCh, statusCh, err := bwsc.MakeOfferAndSubscribe(0.1, bobProvideAmount,
types.ExchangeRate(exchangeRate))
require.NoError(t, err)
bc := client.NewClient(defaultBobDaemonEndpoint)
offersBefore, err := bc.GetOffers()
require.NoError(t, err)
bobIDCh := make(chan uint64, 1)
errCh := make(chan error, 2)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer close(errCh)
defer wg.Done()
select {
case taken := <-takenCh:
require.NotNil(t, taken)
t.Log("swap ID:", taken.ID)
bobIDCh <- taken.ID
case <-time.After(testTimeout):
errCh <- errors.New("make offer subscription timed out")
}
for status := range statusCh {
fmt.Println("> Bob got status:", status)
if status.IsOngoing() {
continue
}
if status != types.CompletedSuccess {
errCh <- fmt.Errorf("swap did not complete successfully: got %s", status)
}
return
}
}()
c := client.NewClient(defaultAliceDaemonEndpoint)
wsc, err := rpcclient.NewWsClient(ctx, defaultAliceDaemonWSEndpoint)
require.NoError(t, err)
// TODO: implement discovery over websockets
providers, err := c.Discover(types.ProvidesXMR, defaultDiscoverTimeout)
require.NoError(t, err)
require.Equal(t, 1, len(providers))
require.GreaterOrEqual(t, len(providers[0]), 2)
id, err := c.TakeOffer(providers[0][0], offerID, 0.1)
id, takerStatusCh, err := wsc.TakeOfferAndSubscribe(providers[0][0], offerID, 0.05)
require.NoError(t, err)
require.Equal(t, uint64(0), id)
go func() {
defer wg.Done()
for status := range takerStatusCh {
fmt.Println("> Alice got status:", status)
if status.IsOngoing() {
continue
}
if status != types.CompletedSuccess {
errCh <- fmt.Errorf("swap did not complete successfully: got %s", status)
}
return
}
}()
wg.Wait()
err = <-errCh
require.NoError(t, err)
bobSwapID := <-bobIDCh
require.Equal(t, id, bobSwapID)
offersAfter, err := bc.GetOffers()
require.NoError(t, err)
require.Equal(t, 1, len(offersBefore)-len(offersAfter))
}