mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-08 21:58:07 -05:00
create separate integration test CI job, implement net_makeOfferAndSubscribe, swap_getOffers, implement happy path integration test (#101)
This commit is contained in:
48
.github/workflows/integration-tests.yaml
vendored
Normal file
48
.github/workflows/integration-tests.yaml
vendored
Normal 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
|
||||
3
.github/workflows/unit-tests.yml
vendored
3
.github/workflows/unit-tests.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -51,4 +51,5 @@ Cargo.lock
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,rust
|
||||
|
||||
*.key
|
||||
log
|
||||
log
|
||||
*.log
|
||||
10
README.md
10
README.md
@@ -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:
|
||||
|
||||
32
cmd/client/client/get_offers.go
Normal file
32
cmd/client/client/get_offers.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,5 +21,5 @@ type SwapStateNet interface {
|
||||
type SwapStateRPC interface {
|
||||
SendKeysMessage() (*message.SendKeysMessage, error)
|
||||
ID() uint64
|
||||
//Status() types.Status
|
||||
InfoFile() string
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
65
docs/rpc.md
65
docs/rpc.md
@@ -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}
|
||||
```
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()])
|
||||
}
|
||||
|
||||
@@ -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
20
protocol/utils.go
Normal 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
164
protocol/write.go
Normal 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
25
protocol/write_test.go
Normal 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)
|
||||
}
|
||||
51
rpc/net.go
51
rpc/net.go
@@ -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 ...
|
||||
|
||||
@@ -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 ...
|
||||
|
||||
11
rpc/swap.go
11
rpc/swap.go
@@ -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
152
rpc/ws.go
@@ -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, ¶ms); 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, ¶ms); 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, ¶ms); 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
201
rpc/ws_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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
0
scripts/setup-env.sh
Normal file → Executable file
1
tests/alice.key
Normal file
1
tests/alice.key
Normal file
@@ -0,0 +1 @@
|
||||
b0d2d5dac3cc02d8db12e4a9703d6175cd4e9531027268b0566f50b85ca2e9410add5985d336d208fcb66c429f3abab7ae4d42f94714b54ea6fb8558d3a72df9
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user