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 - name: Run coverage
run: bash <(curl -s https://codecov.io/bash) run: bash <(curl -s https://codecov.io/bash)
- name: Run integration tests
run: ./scripts/run-integration-tests.sh
- name: Run Hardhat tests - name: Run Hardhat tests
run: | run: |
cd ethereum cd ethereum

3
.gitignore vendored
View File

@@ -51,4 +51,5 @@ Cargo.lock
# End of https://www.toptal.com/developers/gitignore/api/go,rust # End of https://www.toptal.com/developers/gitignore/api/go,rust
*.key *.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 # 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. Now, we can have Alice begin discovering peers who have offers advertised.
```bash ```bash
./swapcli discover --provides XMR --search-time 3 ./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 # 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. 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: 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", Name: "exchange-rate",
Usage: "desired exchange rate of XMR:ETH, eg. --exchange-rate=0.1 means 10XMR = 1ETH", 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, daemonAddrFlag,
}, },
}, },
@@ -253,6 +257,32 @@ func runMake(ctx *cli.Context) error {
endpoint = defaultSwapdAddress 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) c := client.NewClient(endpoint)
id, err := c.MakeOffer(min, max, exchangeRate) id, err := c.MakeOffer(min, max, exchangeRate)
if err != nil { if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"math/big" "math/big"
"github.com/noot/atomic-swap/common" "github.com/noot/atomic-swap/common"
pcommon "github.com/noot/atomic-swap/protocol"
"github.com/noot/atomic-swap/swapfactory" "github.com/noot/atomic-swap/swapfactory"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "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 // store the contract address on disk
fp := fmt.Sprintf("%s/contractaddress", basepath) 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) return nil, ethcommon.Address{}, fmt.Errorf("failed to write contract address to file: %w", err)
} }
} else { } 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, cfg.Basepath,
) )
return nil return nil

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package rpcclient
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/noot/atomic-swap/common/types" "github.com/noot/atomic-swap/common/types"
@@ -44,16 +43,28 @@ func NewWsClient(ctx context.Context, endpoint string) (*wsClient, error) { ///n
}, nil }, nil
} }
// SubscribeSwapStatusRequestParams ...
type SubscribeSwapStatusRequestParams struct {
ID uint64 `json:"id"`
}
// SubscribeSwapStatus returns a channel that is written to each time the swap's status updates. // 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. // If there is no swap with the given ID, it returns an error.
func (c *wsClient) SubscribeSwapStatus(id uint64) (<-chan types.Status, 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{ req := &Request{
JSONRPC: DefaultJSONRPCVersion, JSONRPC: DefaultJSONRPCVersion,
Method: "swap_subscribeStatus", Method: "swap_subscribeStatus",
Params: map[string]interface{}{ Params: bz,
"id": id, ID: 0,
},
ID: 0,
} }
if err := c.conn.WriteJSON(req); err != nil { if err := c.conn.WriteJSON(req); err != nil {
@@ -91,24 +102,45 @@ func (c *wsClient) SubscribeSwapStatus(id uint64) (<-chan types.Status, error) {
break break
} }
respCh <- types.NewStatus(status.Stage) respCh <- types.NewStatus(status.Status)
} }
}() }()
return respCh, nil 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, func (c *wsClient) TakeOfferAndSubscribe(multiaddr, offerID string,
providesAmount float64) (id uint64, ch <-chan types.Status, err error) { 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{ req := &Request{
JSONRPC: DefaultJSONRPCVersion, JSONRPC: DefaultJSONRPCVersion,
Method: "net_takeOfferAndSubscribe", Method: "net_takeOfferAndSubscribe",
Params: map[string]interface{}{ Params: bz,
"multiaddr": multiaddr, ID: 0,
"offerID": offerID,
"providesAmount": providesAmount,
},
ID: 0,
} }
if err = c.conn.WriteJSON(req); err != nil { 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) 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 { if err := json.Unmarshal(resp.Result, &idResp); err != nil {
return 0, nil, fmt.Errorf("failed to unmarshal response: %s", err) return 0, nil, fmt.Errorf("failed to unmarshal swap ID response: %s", err)
}
id, ok := idResp["id"]
if !ok {
return 0, nil, errors.New("websocket response did not contain ID")
} }
respCh := make(chan types.Status) respCh := make(chan types.Status)
@@ -169,13 +196,147 @@ func (c *wsClient) TakeOfferAndSubscribe(multiaddr, offerID string,
log.Debugf("received message over websockets: %s", message) log.Debugf("received message over websockets: %s", message)
var status *SubscribeSwapStatusResponse var status *SubscribeSwapStatusResponse
if err := json.Unmarshal(resp.Result, &status); err != nil { 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) log.Warnf("failed to unmarshal response: %s", err)
break 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, 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 ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"encoding/json"
"errors" "errors"
"fmt"
"os"
"path/filepath"
"time" "time"
ethcommon "github.com/ethereum/go-ethereum/common" 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") 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 // EthereumPrivateKeyToAddress returns the address associated with a private key
func EthereumPrivateKeyToAddress(privkey *ecdsa.PrivateKey) ethcommon.Address { func EthereumPrivateKeyToAddress(privkey *ecdsa.PrivateKey) ethcommon.Address {
pub := privkey.Public().(*ecdsa.PublicKey) pub := privkey.Public().(*ecdsa.PublicKey)

View File

@@ -3,7 +3,6 @@ package common
import ( import (
"context" "context"
"math/big" "math/big"
"os"
"testing" "testing"
ethcommon "github.com/ethereum/go-ethereum/common" ethcommon "github.com/ethereum/go-ethereum/common"
@@ -62,10 +61,3 @@ func TestWaitForReceipt(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tx.Hash(), receipt.TxHash) 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/rand"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -81,15 +80,23 @@ func (kp *PrivateKeyPair) ViewKey() *PrivateViewKey {
return kp.vk 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. // 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) { func (kp *PrivateKeyPair) Info(env common.Environment) *PrivateKeyInfo {
m := make(map[string]string) return &PrivateKeyInfo{
m["PrivateSpendKey"] = kp.sk.Hex() PrivateSpendKey: kp.sk.Hex(),
m["PrivateViewKey"] = kp.vk.Hex() PrivateViewKey: kp.vk.Hex(),
m["Address"] = string(kp.Address(env)) Address: string(kp.Address(env)),
m["Environment"] = env.String() Environment: env.String(),
return json.Marshal(m) }
} }
// PrivateSpendKey represents a monero private spend key // PrivateSpendKey represents a monero private spend key

View File

@@ -2,7 +2,6 @@ package mcrypto
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json"
"testing" "testing"
"github.com/noot/atomic-swap/common" "github.com/noot/atomic-swap/common"
@@ -60,22 +59,6 @@ func TestKeccak256(t *testing.T) {
require.Equal(t, res, res2[:]) 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) { func TestNewPrivateSpendKey(t *testing.T) {
kp, err := GenerateKeys() kp, err := GenerateKeys()
require.NoError(t, err) 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`. - `status`: the swap's status, one of `success`, `refunded`, or `aborted`.
Example: 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' 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 ## websocket subscriptions
@@ -231,13 +228,65 @@ Paramters:
- `id`: the swap ID. - `id`: the swap ID.
Returns: Returns:
- `stage`: the swap's stage or exit status. - `status`: the swap's status.
Example: Example:
``` ```bash
$ wscat -c ws://localhost:8081 wscat -c ws://localhost:8081
# Connected (press CTRL+C to quit) # Connected (press CTRL+C to quit)
# > {"jsonrpc":"2.0", "method":"swap_subscribeStatus", "params": {"id": 0}, "id": 0} # > {"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":"ContractDeployed"},"error":null,"id":null}
# < {"jsonrpc":"2.0","result":{"stage":"refunded"},"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 GenerateBlocks(address string, amount uint) error
} }
// NewDaemonClient returns a new monerod client.
func NewDaemonClient(endpoint string) *client {
return &client{
endpoint: endpoint,
}
}
type generateBlocksRequest struct { type generateBlocksRequest struct {
Address string `json:"wallet_address"` Address string `json:"wallet_address"`
AmountOfBlocks uint `json:"amount_of_blocks"` 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"
"github.com/noot/atomic-swap/common/types" "github.com/noot/atomic-swap/common/types"
pcommon "github.com/noot/atomic-swap/protocol"
"github.com/fatih/color" //nolint:misspell "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") 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 { if err != nil {
return err return err
} }

View File

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

View File

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

View File

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

View File

@@ -110,7 +110,7 @@ func NewInstance(cfg *Config) (*Instance, error) {
}, },
ethAddress: addr, ethAddress: addr,
chainID: cfg.ChainID, chainID: cfg.ChainID,
offerManager: newOfferManager(), offerManager: newOfferManager(cfg.Basepath),
swapManager: cfg.SwapManager, swapManager: cfg.SwapManager,
}, nil }, 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) return nil, fmt.Errorf("failed to instantiate contract instance: %w", err)
} }
fp := fmt.Sprintf("%s/%d/contractaddress", s.bob.basepath, s.ID()) if err := pcommon.WriteContractAddressToFile(s.infofile, msg.Address); err != nil {
if err := common.WriteContractAddressToFile(fp, msg.Address); err != nil {
return nil, fmt.Errorf("failed to write contract address to file: %w", err) 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 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 { desiredAmount common.EtherAmount) error {
b.swapMu.Lock() b.swapMu.Lock()
defer b.swapMu.Unlock() 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") 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 { if err != nil {
return err return err
} }
@@ -58,7 +58,7 @@ func (b *Instance) HandleInitiateMessage(msg *net.SendKeysMessage) (net.SwapStat
return nil, nil, err return nil, nil, err
} }
offer := b.offerManager.getOffer(id) offer, offerExtra := b.offerManager.getAndDeleteOffer(id)
if offer == nil { if offer == nil {
return nil, nil, errors.New("failed to find offer with given ID") 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") 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 return nil, nil, err
} }
offerExtra.IDCh <- b.swapState.info.ID()
close(offerExtra.IDCh)
if err = b.swapState.handleSendKeysMessage(msg); err != nil { if err = b.swapState.handleSendKeysMessage(msg); err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -18,8 +18,12 @@ func TestBob_HandleInitiateMessage(t *testing.T) {
MaximumAmount: 0.002, MaximumAmount: 0.002,
ExchangeRate: 0.1, ExchangeRate: 0.1,
} }
err := b.MakeOffer(offer) extra, err := b.MakeOffer(offer)
require.NoError(t, err) require.NoError(t, err)
go func() {
<-extra.IDCh
}()
msg, _ := newTestAliceSendKeysMessage(t) msg, _ := newTestAliceSendKeysMessage(t)
msg.OfferID = offer.GetID().String() msg.OfferID = offer.GetID().String()
msg.ProvidedAmount = offer.MinimumAmount * float64(offer.ExchangeRate) 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"
"github.com/noot/atomic-swap/common/types" "github.com/noot/atomic-swap/common/types"
pcommon "github.com/noot/atomic-swap/protocol"
) )
type offerManager struct { type offerWithExtra struct {
offers map[types.Hash]*types.Offer 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{ return &offerManager{
offers: make(map[types.Hash]*types.Offer), offers: make(map[types.Hash]*offerWithExtra),
basepath: basepath,
} }
} }
func (om *offerManager) putOffer(o *types.Offer) { func (om *offerManager) putOffer(o *types.Offer) *types.OfferExtra {
om.offers[o.GetID()] = o 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 { func (om *offerManager) getAndDeleteOffer(id types.Hash) (*types.Offer, *types.OfferExtra) {
return om.offers[id] offer, has := om.offers[id]
} if !has {
return nil, nil
}
func (om *offerManager) deleteOffer(id types.Hash) {
delete(om.offers, id) delete(om.offers, id)
return offer.offer, offer.extra
} }
// MakeOffer makes a new swap offer. // 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) balance, err := b.client.GetBalance(0)
if err != nil { if err != nil {
return err return nil, err
} }
if common.MoneroAmount(balance.UnlockedBalance) < common.MoneroToPiconero(o.MaximumAmount) { 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) log.Infof("created new offer: %v", o)
return nil return extra, nil
} }
// GetOffers returns all current offers. // GetOffers returns all current offers.
@@ -50,7 +72,7 @@ func (b *Instance) GetOffers() []*types.Offer {
offers := make([]*types.Offer, len(b.offerManager.offers)) offers := make([]*types.Offer, len(b.offerManager.offers))
i := 0 i := 0
for _, o := range b.offerManager.offers { for _, o := range b.offerManager.offers {
offers[i] = o offers[i] = o.offer
i++ i++
} }
return offers return offers

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"math/big" "math/big"
"os"
"testing" "testing"
"time" "time"
@@ -22,6 +23,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var infofile = os.TempDir() + "/test.keys"
var ( var (
_ = logging.SetLogLevel("bob", "debug") _ = logging.SetLogLevel("bob", "debug")
testWallet = "test-wallet" testWallet = "test-wallet"
@@ -77,8 +80,7 @@ func newTestBob(t *testing.T) *Instance {
func newTestInstance(t *testing.T) (*Instance, *swapState) { func newTestInstance(t *testing.T) (*Instance, *swapState) {
bob := newTestBob(t) bob := newTestBob(t)
swapState, err := newSwapState(bob, &types.Offer{}, nil, infofile, common.MoneroAmount(33), desiredAmout)
swapState, err := newSwapState(bob, types.Hash{}, common.MoneroAmount(33), desiredAmout)
require.NoError(t, err) require.NoError(t, err)
return bob, swapState return bob, swapState
} }
@@ -420,34 +422,31 @@ func TestSwapState_ProtocolExited_Aborted(t *testing.T) {
func TestSwapState_ProtocolExited_Success(t *testing.T) { func TestSwapState_ProtocolExited_Success(t *testing.T) {
b, s := newTestInstance(t) b, s := newTestInstance(t)
offer := &types.Offer{ s.offer = &types.Offer{
Provides: types.ProvidesXMR, Provides: types.ProvidesXMR,
MinimumAmount: 0.1, MinimumAmount: 0.1,
MaximumAmount: 0.2, MaximumAmount: 0.2,
ExchangeRate: 0.1, ExchangeRate: 0.1,
} }
b.MakeOffer(offer)
s.offerID = offer.GetID()
s.info.SetStatus(types.CompletedSuccess) s.info.SetStatus(types.CompletedSuccess)
err := s.ProtocolExited() err := s.ProtocolExited()
require.NoError(t, err) 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) { func TestSwapState_ProtocolExited_Refunded(t *testing.T) {
b, s := newTestInstance(t) b, s := newTestInstance(t)
offer := &types.Offer{ s.offer = &types.Offer{
Provides: types.ProvidesXMR, Provides: types.ProvidesXMR,
MinimumAmount: 0.1, MinimumAmount: 0.1,
MaximumAmount: 0.2, MaximumAmount: 0.2,
ExchangeRate: 0.1, ExchangeRate: 0.1,
} }
b.MakeOffer(offer) b.MakeOffer(s.offer)
s.offerID = offer.GetID()
s.info.SetStatus(types.CompletedRefund) s.info.SetStatus(types.CompletedRefund)
err := s.ProtocolExited() err := s.ProtocolExited()
require.NoError(t, err) 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. // Manager tracks current and past swaps.
type Manager struct { type Manager struct {
sync.RWMutex sync.RWMutex
ongoing *Info ongoing *Info
past map[uint64]*Info past map[uint64]*Info
offersTaken map[string]uint64 // map of offerID -> swapID
} }
// NewManager ... // NewManager ...
func NewManager() *Manager { func NewManager() *Manager {
return &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 ... // TakeOfferResponse ...
type TakeOfferResponse struct { 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. // 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 { 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 { if err != nil {
return err return err
} }
resp.ID = id resp.ID = id
resp.InfoFile = infofile
return nil return nil
} }
func (s *NetService) takeOffer(multiaddr, offerID string, 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) swapState, err := s.alice.InitiateProtocol(providesAmount)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, "", err
} }
skm, err := swapState.SendKeysMessage() skm, err := swapState.SendKeysMessage()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, "", err
} }
skm.OfferID = offerID skm.OfferID = offerID
@@ -163,26 +165,27 @@ func (s *NetService) takeOffer(multiaddr, offerID string,
who, err := net.StringToAddrInfo(multiaddr) who, err := net.StringToAddrInfo(multiaddr)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, "", err
} }
if err = s.net.Initiate(who, skm, swapState); err != nil { if err = s.net.Initiate(who, skm, swapState); err != nil {
_ = swapState.ProtocolExited() _ = swapState.ProtocolExited()
return 0, nil, err return 0, nil, "", err
} }
info := s.sm.GetOngoingSwap() info := s.sm.GetOngoingSwap()
if info == nil { 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 ... // TakeOfferSyncResponse ...
type TakeOfferSyncResponse struct { type TakeOfferSyncResponse struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
Status string `json:"status"` InfoFile string `json:"infoFile"`
Status string `json:"status"`
} }
// TakeOfferSync initiates a swap with the given peer by taking an offer they've made. // 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.ID = swapState.ID()
resp.InfoFile = swapState.InfoFile()
const checkSwapSleepDuration = time.Millisecond * 100 const checkSwapSleepDuration = time.Millisecond * 100
@@ -240,11 +244,24 @@ type MakeOfferRequest struct {
// MakeOfferResponse ... // MakeOfferResponse ...
type MakeOfferResponse struct { type MakeOfferResponse struct {
ID string `json:"offerID"` ID string `json:"offerID"`
InfoFile string `json:"infoFile"`
} }
// MakeOffer creates and advertises a new swap offer. // MakeOffer creates and advertises a new swap offer.
func (s *NetService) MakeOffer(_ *http.Request, req *MakeOfferRequest, resp *MakeOfferResponse) error { 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{ o := &types.Offer{
Provides: types.ProvidesXMR, Provides: types.ProvidesXMR,
MinimumAmount: req.MinimumAmount, MinimumAmount: req.MinimumAmount,
@@ -252,14 +269,12 @@ func (s *NetService) MakeOffer(_ *http.Request, req *MakeOfferRequest, resp *Mak
ExchangeRate: req.ExchangeRate, ExchangeRate: req.ExchangeRate,
} }
if err := s.bob.MakeOffer(o); err != nil { offerExtra, err := s.bob.MakeOffer(o)
return err if err != nil {
return "", nil, err
} }
resp.ID = o.GetID().String() return o.GetID().String(), offerExtra, nil
s.net.Advertise()
return nil
} }
// SetGasPriceRequest ... // SetGasPriceRequest ...

View File

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

View File

@@ -132,3 +132,14 @@ func (s *SwapService) GetStage(_ *http.Request, _ *interface{}, resp *GetStageRe
resp.Info = info.Status().Info() resp.Info = info.Status().Info()
return nil 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 ( const (
subscribeNewPeer = "net_subscribeNewPeer" subscribeNewPeer = "net_subscribeNewPeer"
subscribeMakeOffer = "net_makeOfferAndSubscribe"
subscribeTakeOffer = "net_takeOfferAndSubscribe" subscribeTakeOffer = "net_takeOfferAndSubscribe"
subscribeSwapStatus = "swap_subscribeStatus" subscribeSwapStatus = "swap_subscribeStatus"
) )
@@ -29,20 +30,16 @@ type (
) )
type wsServer struct { type wsServer struct {
ctx context.Context ctx context.Context
sm SwapManager sm SwapManager
alice Alice ns *NetService
bob Bob
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{ return &wsServer{
ctx: ctx, ctx: ctx,
sm: sm, sm: sm,
alice: a, ns: ns,
bob: b,
ns: ns,
} }
} }
@@ -83,67 +80,49 @@ func (s *wsServer) handleRequest(conn *websocket.Conn, req *Request) error {
case subscribeNewPeer: case subscribeNewPeer:
return errors.New("unimplemented") return errors.New("unimplemented")
case subscribeSwapStatus: case subscribeSwapStatus:
idi, has := req.Params["id"] // TODO: make const var params *rpcclient.SubscribeSwapStatusRequestParams
if !has { if err := json.Unmarshal(req.Params, &params); err != nil {
return errors.New("params missing id field") return fmt.Errorf("failed to unmarshal parameters: %w", err)
} }
id, ok := idi.(float64) return s.subscribeSwapStatus(s.ctx, conn, params.ID)
if !ok {
return fmt.Errorf("failed to cast id parameter to float64: got %T", idi)
}
return s.subscribeSwapStatus(s.ctx, conn, uint64(id))
case subscribeTakeOffer: case subscribeTakeOffer:
maddri, has := req.Params["multiaddr"] var params *rpcclient.SubscribeTakeOfferParams
if !has { if err := json.Unmarshal(req.Params, &params); err != nil {
return errors.New("params missing multiaddr field") return fmt.Errorf("failed to unmarshal parameters: %w", err)
} }
maddr, ok := maddri.(string) id, ch, infofile, err := s.ns.takeOffer(params.Multiaddr, params.OfferID, params.ProvidesAmount)
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)
if err != nil { if err != nil {
return err 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: default:
return errors.New("invalid method") return errors.New("invalid method")
} }
} }
func (s *wsServer) subscribeTakeOffer(ctx context.Context, conn *websocket.Conn, func (s *wsServer) subscribeTakeOffer(ctx context.Context, conn *websocket.Conn,
id uint64, statusCh <-chan types.Status) error { id uint64, statusCh <-chan types.Status, infofile string) error {
// firstly write swap ID resp := &TakeOfferResponse{
idMsg := map[string]uint64{ ID: id,
"id": id, InfoFile: infofile,
} }
if err := writeResponse(conn, idMsg); err != nil { if err := writeResponse(conn, resp); err != nil {
return err return err
} }
@@ -155,7 +134,64 @@ func (s *wsServer) subscribeTakeOffer(ctx context.Context, conn *websocket.Conn,
} }
resp := &SubscribeSwapStatusResponse{ 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 { if err := writeResponse(conn, resp); err != nil {
@@ -185,7 +221,7 @@ func (s *wsServer) subscribeSwapStatus(ctx context.Context, conn *websocket.Conn
} }
resp := &SubscribeSwapStatusResponse{ resp := &SubscribeSwapStatusResponse{
Stage: status.String(), Status: status.String(),
} }
if err := writeResponse(conn, resp); err != nil { if err := writeResponse(conn, resp); err != nil {
@@ -204,7 +240,7 @@ func (s *wsServer) writeSwapExitStatus(conn *websocket.Conn, id uint64) error {
} }
resp := &SubscribeSwapStatusResponse{ resp := &SubscribeSwapStatusResponse{
Stage: info.Status().String(), Status: info.Status().String(),
} }
if err := writeResponse(conn, resp); err != nil { 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 # wait for servers to start
sleep 10 sleep 10
# run unit tests # start alice and bob swapd instances
echo "running unit tests..." echo "starting alice, logs in ./tests/alice.log"
go test ./tests -v 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=$? OK=$?
# kill processes # kill processes
kill $MONERO_WALLET_CLI_BOB_PID kill $MONERO_WALLET_CLI_BOB_PID
kill $MONERO_WALLET_CLI_ALICE_PID kill $MONERO_WALLET_CLI_ALICE_PID
kill $GANACHE_CLI_PID kill $GANACHE_CLI_PID
kill $ALICE_PID
kill $BOB_PID
# rm -rf ./alice-test-keys # rm -rf ./alice-test-keys
# rm -rf ./bob-test-keys # rm -rf ./bob-test-keys
exit $OK 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 package tests
import ( import (
"context"
"errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"os/exec"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/noot/atomic-swap/cmd/client/client" "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/common/types"
"github.com/noot/atomic-swap/monero"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const ( const (
defaultAliceTestLibp2pKey = "alice.key" testsEnv = "TESTS"
defaultAliceDaemonEndpoint = "http://localhost:5001" integrationMode = "integration"
defaultBobDaemonEndpoint = "http://localhost:5002" generateBlocksEnv = "GENERATEBLOCKS"
defaultDiscoverTimeout = 2 // 2 seconds
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) exchangeRate = float64(0.05)
) )
@@ -31,123 +40,32 @@ func TestMain(m *testing.M) {
os.Exit(0) os.Exit(0)
} }
cmd := exec.Command("../scripts/build.sh") if os.Getenv(testsEnv) != integrationMode {
out, err := cmd.CombinedOutput() os.Exit(0)
}
c := monero.NewClient(common.DefaultBobMoneroEndpoint)
d := monero.NewDaemonClient(common.DefaultMoneroDaemonEndpoint)
bobAddr, err := c.GetAddress(0)
if err != nil { 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()) 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) { func TestAlice_Discover(t *testing.T) {
startNodes(t)
bc := client.NewClient(defaultBobDaemonEndpoint) bc := client.NewClient(defaultBobDaemonEndpoint)
_, err := bc.MakeOffer(bobProvideAmount, bobProvideAmount, exchangeRate) _, err := bc.MakeOffer(bobProvideAmount, bobProvideAmount, exchangeRate)
require.NoError(t, err) require.NoError(t, err)
@@ -160,7 +78,6 @@ func TestAlice_Discover(t *testing.T) {
} }
func TestBob_Discover(t *testing.T) { func TestBob_Discover(t *testing.T) {
startNodes(t)
c := client.NewClient(defaultBobDaemonEndpoint) c := client.NewClient(defaultBobDaemonEndpoint)
providers, err := c.Discover(types.ProvidesETH, defaultDiscoverTimeout) providers, err := c.Discover(types.ProvidesETH, defaultDiscoverTimeout)
require.NoError(t, err) require.NoError(t, err)
@@ -168,7 +85,6 @@ func TestBob_Discover(t *testing.T) {
} }
func TestAlice_Query(t *testing.T) { func TestAlice_Query(t *testing.T) {
startNodes(t)
bc := client.NewClient(defaultBobDaemonEndpoint) bc := client.NewClient(defaultBobDaemonEndpoint)
_, err := bc.MakeOffer(bobProvideAmount, bobProvideAmount, exchangeRate) _, err := bc.MakeOffer(bobProvideAmount, bobProvideAmount, exchangeRate)
require.NoError(t, err) require.NoError(t, err)
@@ -182,27 +98,98 @@ func TestAlice_Query(t *testing.T) {
resp, err := c.Query(providers[0][0]) resp, err := c.Query(providers[0][0])
require.NoError(t, err) 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].MinimumAmount)
require.Equal(t, bobProvideAmount, resp.Offers[0].MaximumAmount) require.Equal(t, bobProvideAmount, resp.Offers[0].MaximumAmount)
require.Equal(t, exchangeRate, float64(resp.Offers[0].ExchangeRate)) require.Equal(t, exchangeRate, float64(resp.Offers[0].ExchangeRate))
} }
func TestAlice_TakeOffer(t *testing.T) { func TestTakeOffer_HappyPath(t *testing.T) {
startNodes(t) const testTimeout = time.Second * 5
bc := client.NewClient(defaultBobDaemonEndpoint) ctx, cancel := context.WithCancel(context.Background())
offerID, err := bc.MakeOffer(0.1, bobProvideAmount, exchangeRate) defer cancel()
bwsc, err := rpcclient.NewWsClient(ctx, defaultBobDaemonWSEndpoint)
require.NoError(t, err) 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) providers, err := c.Discover(types.ProvidesXMR, defaultDiscoverTimeout)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(providers)) require.Equal(t, 1, len(providers))
require.GreaterOrEqual(t, len(providers[0]), 2) 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.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))
} }