From 4d300e4dece56535f56ccc32330340ce89e42581 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=9ETH=CE=9E=D0=AFSPH=CE=9E=D0=AF=CE=9E?=
 <{viktor.tron,nagydani,zsfelfoldi}@gmail.com>
Date: Mon, 29 Aug 2016 21:18:00 +0200
Subject: swarm: plan bee for content storage and distribution on web3

This change imports the Swarm protocol codebase. Compared to the 'swarm'
branch, a few mostly cosmetic changes had to be made:

* The various redundant log message prefixes are gone.
* All files now have LGPLv3 license headers.
* Minor code changes were needed to please go vet and make the tests
  pass on Windows.
* Further changes were required to adapt to the go-ethereum develop
  branch and its new Go APIs.

Some code has not (yet) been brought over:

* swarm/cmd/bzzhash: will reappear as cmd/bzzhash later
* swarm/cmd/bzzup.sh: will be reimplemented in cmd/bzzup
* swarm/cmd/makegenesis: will reappear somehow
* swarm/examples/album: will move to a separate repository
* swarm/examples/filemanager: ditto
* swarm/examples/files: will not be merged
* swarm/test/*: will not be merged
* swarm/services/swear: will reappear as contracts/swear when needed
---
 swarm/services/swap/swap.go           | 283 ++++++++++++++++++++++++++++++++++
 swarm/services/swap/swap/swap.go      | 254 ++++++++++++++++++++++++++++++
 swarm/services/swap/swap/swap_test.go | 194 +++++++++++++++++++++++
 3 files changed, 731 insertions(+)
 create mode 100644 swarm/services/swap/swap.go
 create mode 100644 swarm/services/swap/swap/swap.go
 create mode 100644 swarm/services/swap/swap/swap_test.go

(limited to 'swarm/services')

diff --git a/swarm/services/swap/swap.go b/swarm/services/swap/swap.go
new file mode 100644
index 000000000..f72036d72
--- /dev/null
+++ b/swarm/services/swap/swap.go
@@ -0,0 +1,283 @@
+// Copyright 2016 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package swap
+
+import (
+	"crypto/ecdsa"
+	"fmt"
+	"math/big"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/accounts/abi/bind"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/contracts/chequebook"
+	"github.com/ethereum/go-ethereum/contracts/chequebook/contract"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/logger"
+	"github.com/ethereum/go-ethereum/logger/glog"
+	"github.com/ethereum/go-ethereum/swarm/services/swap/swap"
+	"golang.org/x/net/context"
+)
+
+// SwAP       Swarm Accounting Protocol with
+// SWAP^2     Strategies of Withholding Automatic Payments
+// SWAP^3     Accreditation: payment via credit SWAP
+// using chequebook pkg for delayed payments
+// default parameters
+
+var (
+	autoCashInterval     = 300 * time.Second           // default interval for autocash
+	autoCashThreshold    = big.NewInt(50000000000000)  // threshold that triggers autocash (wei)
+	autoDepositInterval  = 300 * time.Second           // default interval for autocash
+	autoDepositThreshold = big.NewInt(50000000000000)  // threshold that triggers autodeposit (wei)
+	autoDepositBuffer    = big.NewInt(100000000000000) // buffer that is surplus for fork protection etc (wei)
+	buyAt                = big.NewInt(20000000000)     // maximum chunk price host is willing to pay (wei)
+	sellAt               = big.NewInt(20000000000)     // minimum chunk price host requires (wei)
+	payAt                = 100                         // threshold that triggers payment {request} (units)
+	dropAt               = 10000                       // threshold that triggers disconnect (units)
+)
+
+const (
+	chequebookDeployRetries = 5
+	chequebookDeployDelay   = 1 * time.Second // delay between retries
+)
+
+type SwapParams struct {
+	*swap.Params
+	*PayProfile
+}
+
+type SwapProfile struct {
+	*swap.Profile
+	*PayProfile
+}
+
+type PayProfile struct {
+	PublicKey   string         // check against signature of promise
+	Contract    common.Address // address of chequebook contract
+	Beneficiary common.Address // recipient address for swarm sales revenue
+	privateKey  *ecdsa.PrivateKey
+	publicKey   *ecdsa.PublicKey
+	owner       common.Address
+	chbook      *chequebook.Chequebook
+	lock        sync.RWMutex
+}
+
+func DefaultSwapParams(contract common.Address, prvkey *ecdsa.PrivateKey) *SwapParams {
+	pubkey := &prvkey.PublicKey
+	return &SwapParams{
+		PayProfile: &PayProfile{
+			PublicKey:   common.ToHex(crypto.FromECDSAPub(pubkey)),
+			Contract:    contract,
+			Beneficiary: crypto.PubkeyToAddress(*pubkey),
+			privateKey:  prvkey,
+			publicKey:   pubkey,
+			owner:       crypto.PubkeyToAddress(*pubkey),
+		},
+		Params: &swap.Params{
+			Profile: &swap.Profile{
+				BuyAt:  buyAt,
+				SellAt: sellAt,
+				PayAt:  uint(payAt),
+				DropAt: uint(dropAt),
+			},
+			Strategy: &swap.Strategy{
+				AutoCashInterval:     autoCashInterval,
+				AutoCashThreshold:    autoCashThreshold,
+				AutoDepositInterval:  autoDepositInterval,
+				AutoDepositThreshold: autoDepositThreshold,
+				AutoDepositBuffer:    autoDepositBuffer,
+			},
+		},
+	}
+}
+
+// swap constructor, parameters
+// * global chequebook, assume deployed service and
+// * the balance is at buffer.
+// swap.Add(n) called in netstore
+// n > 0 called when sending chunks = receiving retrieve requests
+//                 OR sending cheques.
+// n < 0  called when receiving chunks = receiving delivery responses
+//                 OR receiving cheques.
+
+func NewSwap(local *SwapParams, remote *SwapProfile, backend chequebook.Backend, proto swap.Protocol) (self *swap.Swap, err error) {
+	var (
+		ctx = context.TODO()
+		ok  bool
+		in  *chequebook.Inbox
+		out *chequebook.Outbox
+	)
+
+	// check if remote chequebook is valid
+	// insolvent chequebooks suicide so will signal as invalid
+	// TODO: monitoring a chequebooks events
+	ok, err = chequebook.ValidateCode(ctx, backend, remote.Contract)
+	if !ok {
+		glog.V(logger.Info).Infof("invalid contract %v for peer %v: %v)", remote.Contract.Hex()[:8], proto, err)
+	} else {
+		// remote contract valid, create inbox
+		in, err = chequebook.NewInbox(local.privateKey, remote.Contract, local.Beneficiary, crypto.ToECDSAPub(common.FromHex(remote.PublicKey)), backend)
+		if err != nil {
+			glog.V(logger.Warn).Infof("unable to set up inbox for chequebook contract %v for peer %v: %v)", remote.Contract.Hex()[:8], proto, err)
+		}
+	}
+
+	// check if local chequebook contract is valid
+	ok, err = chequebook.ValidateCode(ctx, backend, local.Contract)
+	if !ok {
+		glog.V(logger.Warn).Infof("unable to set up outbox for peer %v:  chequebook contract (owner: %v): %v)", proto, local.owner.Hex(), err)
+	} else {
+		out = chequebook.NewOutbox(local.Chequebook(), remote.Beneficiary)
+	}
+
+	pm := swap.Payment{
+		In:    in,
+		Out:   out,
+		Buys:  out != nil,
+		Sells: in != nil,
+	}
+	self, err = swap.New(local.Params, pm, proto)
+	if err != nil {
+		return
+	}
+	// remote profile given (first) in handshake
+	self.SetRemote(remote.Profile)
+	var buy, sell string
+	if self.Buys {
+		buy = "purchase from peer enabled at " + remote.SellAt.String() + " wei/chunk"
+	} else {
+		buy = "purchase from peer disabled"
+	}
+	if self.Sells {
+		sell = "selling to peer enabled at " + local.SellAt.String() + " wei/chunk"
+	} else {
+		sell = "selling to peer disabled"
+	}
+	glog.V(logger.Warn).Infof("SWAP arrangement with <%v>: %v; %v)", proto, buy, sell)
+
+	return
+}
+
+func (self *SwapParams) Chequebook() *chequebook.Chequebook {
+	defer self.lock.Unlock()
+	self.lock.Lock()
+	return self.chbook
+}
+
+func (self *SwapParams) PrivateKey() *ecdsa.PrivateKey {
+	return self.privateKey
+}
+
+// func (self *SwapParams) PublicKey() *ecdsa.PublicKey {
+// 	return self.publicKey
+// }
+
+func (self *SwapParams) SetKey(prvkey *ecdsa.PrivateKey) {
+	self.privateKey = prvkey
+	self.publicKey = &prvkey.PublicKey
+}
+
+// setChequebook(path, backend) wraps the
+// chequebook initialiser and sets up autoDeposit to cover spending.
+func (self *SwapParams) SetChequebook(ctx context.Context, backend chequebook.Backend, path string) error {
+	self.lock.Lock()
+	contract := self.Contract
+	self.lock.Unlock()
+
+	valid, err := chequebook.ValidateCode(ctx, backend, contract)
+	if err != nil {
+		return err
+	} else if valid {
+		return self.newChequebookFromContract(path, backend)
+	}
+	return self.deployChequebook(ctx, backend, path)
+}
+
+func (self *SwapParams) deployChequebook(ctx context.Context, backend chequebook.Backend, path string) error {
+	opts := bind.NewKeyedTransactor(self.privateKey)
+	opts.Value = self.AutoDepositBuffer
+	opts.Context = ctx
+
+	glog.V(logger.Info).Infof("Deploying new chequebook (owner: %v)", opts.From.Hex())
+	contract, err := deployChequebookLoop(opts, backend)
+	if err != nil {
+		glog.V(logger.Error).Infof("unable to deploy new chequebook: %v", err)
+		return err
+	}
+	glog.V(logger.Info).Infof("new chequebook deployed at %v (owner: %v)", contract.Hex(), opts.From.Hex())
+
+	// need to save config at this point
+	self.lock.Lock()
+	self.Contract = contract
+	err = self.newChequebookFromContract(path, backend)
+	self.lock.Unlock()
+	if err != nil {
+		glog.V(logger.Warn).Infof("error initialising cheque book (owner: %v): %v", opts.From.Hex(), err)
+	}
+	return err
+}
+
+// repeatedly tries to deploy a chequebook.
+func deployChequebookLoop(opts *bind.TransactOpts, backend chequebook.Backend) (addr common.Address, err error) {
+	var tx *types.Transaction
+	for try := 0; try < chequebookDeployRetries; try++ {
+		if try > 0 {
+			time.Sleep(chequebookDeployDelay)
+		}
+		if _, tx, _, err = contract.DeployChequebook(opts, backend); err != nil {
+			glog.V(logger.Warn).Infof("can't send chequebook deploy tx (try %d): %v", try, err)
+			continue
+		}
+		if addr, err = bind.WaitDeployed(opts.Context, backend, tx); err != nil {
+			glog.V(logger.Warn).Infof("chequebook deploy error (try %d): %v", try, err)
+			continue
+		}
+		return addr, nil
+	}
+	return addr, err
+}
+
+// initialise the chequebook from a persisted json file or create a new one
+// caller holds the lock
+func (self *SwapParams) newChequebookFromContract(path string, backend chequebook.Backend) error {
+	hexkey := common.Bytes2Hex(self.Contract.Bytes())
+	err := os.MkdirAll(filepath.Join(path, "chequebooks"), os.ModePerm)
+	if err != nil {
+		return fmt.Errorf("unable to create directory for chequebooks: %v", err)
+	}
+
+	chbookpath := filepath.Join(path, "chequebooks", hexkey+".json")
+	self.chbook, err = chequebook.LoadChequebook(chbookpath, self.privateKey, backend, true)
+
+	if err != nil {
+		self.chbook, err = chequebook.NewChequebook(chbookpath, self.Contract, self.privateKey, backend)
+		if err != nil {
+			glog.V(logger.Warn).Infof("unable to initialise chequebook (owner: %v): %v", self.owner.Hex(), err)
+			return fmt.Errorf("unable to initialise chequebook (owner: %v): %v", self.owner.Hex(), err)
+		}
+	}
+
+	self.chbook.AutoDeposit(self.AutoDepositInterval, self.AutoDepositThreshold, self.AutoDepositBuffer)
+	glog.V(logger.Info).Infof("auto deposit ON for %v -> %v: interval = %v, threshold = %v, buffer = %v)", crypto.PubkeyToAddress(*(self.publicKey)).Hex()[:8], self.Contract.Hex()[:8], self.AutoDepositInterval, self.AutoDepositThreshold, self.AutoDepositBuffer)
+
+	return nil
+}
diff --git a/swarm/services/swap/swap/swap.go b/swarm/services/swap/swap/swap.go
new file mode 100644
index 000000000..9d5da7c3a
--- /dev/null
+++ b/swarm/services/swap/swap/swap.go
@@ -0,0 +1,254 @@
+// Copyright 2016 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package swap
+
+import (
+	"fmt"
+	"math/big"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/logger"
+	"github.com/ethereum/go-ethereum/logger/glog"
+)
+
+// SwAP Swarm Accounting Protocol with
+//      Swift Automatic  Payments
+// a peer to peer micropayment system
+
+// public swap profile
+// public parameters for SWAP, serializable config struct passed in handshake
+type Profile struct {
+	BuyAt  *big.Int // accepted max price for chunk
+	SellAt *big.Int // offered sale price for chunk
+	PayAt  uint     // threshold that triggers payment request
+	DropAt uint     // threshold that triggers disconnect
+}
+
+// Strategy encapsulates parameters relating to
+// automatic deposit and automatic cashing
+type Strategy struct {
+	AutoCashInterval     time.Duration // default interval for autocash
+	AutoCashThreshold    *big.Int      // threshold that triggers autocash (wei)
+	AutoDepositInterval  time.Duration // default interval for autocash
+	AutoDepositThreshold *big.Int      // threshold that triggers autodeposit (wei)
+	AutoDepositBuffer    *big.Int      // buffer that is surplus for fork protection etc (wei)
+}
+
+// Params extends the public profile with private parameters relating to
+// automatic deposit and automatic cashing
+type Params struct {
+	*Profile
+	*Strategy
+}
+
+// Promise
+// 3rd party Provable Promise of Payment
+// issued by outPayment
+// serialisable to send with Protocol
+type Promise interface{}
+
+// interface for the peer protocol for testing or external alternative payment
+type Protocol interface {
+	Pay(int, Promise) // units, payment proof
+	Drop()
+	String() string
+}
+
+// interface for the (delayed) ougoing payment system with autodeposit
+type OutPayment interface {
+	Issue(amount *big.Int) (promise Promise, err error)
+	AutoDeposit(interval time.Duration, threshold, buffer *big.Int)
+	Stop()
+}
+
+// interface for the (delayed) incoming payment system with autocash
+type InPayment interface {
+	Receive(promise Promise) (*big.Int, error)
+	AutoCash(cashInterval time.Duration, maxUncashed *big.Int)
+	Stop()
+}
+
+// swap is the swarm accounting protocol instance
+// * pairwise accounting and payments
+type Swap struct {
+	lock    sync.Mutex // mutex for balance access
+	balance int        // units of chunk/retrieval request
+	local   *Params    // local peer's swap parameters
+	remote  *Profile   // remote peer's swap profile
+	proto   Protocol   // peer communication protocol
+	Payment
+}
+
+type Payment struct {
+	Out         OutPayment // outgoing payment handler
+	In          InPayment  // incoming  payment handler
+	Buys, Sells bool
+}
+
+// swap constructor
+func New(local *Params, pm Payment, proto Protocol) (self *Swap, err error) {
+
+	self = &Swap{
+		local:   local,
+		Payment: pm,
+		proto:   proto,
+	}
+
+	self.SetParams(local)
+
+	return
+}
+
+// entry point for setting remote swap profile (e.g from handshake or other message)
+func (self *Swap) SetRemote(remote *Profile) {
+	defer self.lock.Unlock()
+	self.lock.Lock()
+
+	self.remote = remote
+	if self.Sells && (remote.BuyAt.Cmp(common.Big0) <= 0 || self.local.SellAt.Cmp(common.Big0) <= 0 || remote.BuyAt.Cmp(self.local.SellAt) < 0) {
+		self.Out.Stop()
+		self.Sells = false
+	}
+	if self.Buys && (remote.SellAt.Cmp(common.Big0) <= 0 || self.local.BuyAt.Cmp(common.Big0) <= 0 || self.local.BuyAt.Cmp(self.remote.SellAt) < 0) {
+		self.In.Stop()
+		self.Buys = false
+	}
+
+	glog.V(logger.Debug).Infof("<%v> remote profile set: pay at: %v, drop at: %v, buy at: %v, sell at: %v", self.proto, remote.PayAt, remote.DropAt, remote.BuyAt, remote.SellAt)
+
+}
+
+// to set strategy dynamically
+func (self *Swap) SetParams(local *Params) {
+	defer self.lock.Unlock()
+	self.lock.Lock()
+	self.local = local
+	self.setParams(local)
+}
+
+// caller holds the lock
+
+func (self *Swap) setParams(local *Params) {
+
+	if self.Sells {
+		self.In.AutoCash(local.AutoCashInterval, local.AutoCashThreshold)
+		glog.V(logger.Info).Infof("<%v> set autocash to every %v, max uncashed limit: %v", self.proto, local.AutoCashInterval, local.AutoCashThreshold)
+	} else {
+		glog.V(logger.Info).Infof("<%v> autocash off (not selling)", self.proto)
+	}
+	if self.Buys {
+		self.Out.AutoDeposit(local.AutoDepositInterval, local.AutoDepositThreshold, local.AutoDepositBuffer)
+		glog.V(logger.Info).Infof("<%v> set autodeposit to every %v, pay at: %v, buffer: %v", self.proto, local.AutoDepositInterval, local.AutoDepositThreshold, local.AutoDepositBuffer)
+	} else {
+		glog.V(logger.Info).Infof("<%v> autodeposit off (not buying)", self.proto)
+	}
+}
+
+// Add(n)
+// n > 0 called when promised/provided n units of service
+// n < 0 called when used/requested n units of service
+func (self *Swap) Add(n int) error {
+	defer self.lock.Unlock()
+	self.lock.Lock()
+	self.balance += n
+	if !self.Sells && self.balance > 0 {
+		glog.V(logger.Detail).Infof("<%v> remote peer cannot have debt (balance: %v)", self.proto, self.balance)
+		self.proto.Drop()
+		return fmt.Errorf("[SWAP] <%v> remote peer cannot have debt (balance: %v)", self.proto, self.balance)
+	}
+	if !self.Buys && self.balance < 0 {
+		glog.V(logger.Detail).Infof("<%v> we cannot have debt (balance: %v)", self.proto, self.balance)
+		return fmt.Errorf("[SWAP] <%v> we cannot have debt (balance: %v)", self.proto, self.balance)
+	}
+	if self.balance >= int(self.local.DropAt) {
+		glog.V(logger.Detail).Infof("<%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", self.proto, self.balance, self.local.DropAt)
+		self.proto.Drop()
+		return fmt.Errorf("[SWAP] <%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", self.proto, self.balance, self.local.DropAt)
+	} else if self.balance <= -int(self.remote.PayAt) {
+		self.send()
+	}
+	return nil
+}
+
+func (self *Swap) Balance() int {
+	defer self.lock.Unlock()
+	self.lock.Lock()
+	return self.balance
+}
+
+// send(units) is called when payment is due
+// In case of insolvency no promise is issued and sent, safe against fraud
+// No return value: no error = payment is opportunistic = hang in till dropped
+func (self *Swap) send() {
+	if self.local.BuyAt != nil && self.balance < 0 {
+		amount := big.NewInt(int64(-self.balance))
+		amount.Mul(amount, self.remote.SellAt)
+		promise, err := self.Out.Issue(amount)
+		if err != nil {
+			glog.V(logger.Warn).Infof("<%v> cannot issue cheque (amount: %v, channel: %v): %v", self.proto, amount, self.Out, err)
+		} else {
+			glog.V(logger.Warn).Infof("<%v> cheque issued (amount: %v, channel: %v)", self.proto, amount, self.Out)
+			self.proto.Pay(-self.balance, promise)
+			self.balance = 0
+		}
+	}
+}
+
+// receive(units, promise) is called by the protocol when a payment msg is received
+// returns error if promise is invalid.
+func (self *Swap) Receive(units int, promise Promise) error {
+	if units <= 0 {
+		return fmt.Errorf("invalid units: %v <= 0", units)
+	}
+
+	price := new(big.Int).SetInt64(int64(units))
+	price.Mul(price, self.local.SellAt)
+
+	amount, err := self.In.Receive(promise)
+
+	if err != nil {
+		err = fmt.Errorf("invalid promise: %v", err)
+	} else if price.Cmp(amount) != 0 {
+		// verify amount = units * unit sale price
+		return fmt.Errorf("invalid amount: %v = %v * %v (units sent in msg * agreed sale unit price) != %v (signed in cheque)", price, units, self.local.SellAt, amount)
+	}
+	if err != nil {
+		glog.V(logger.Detail).Infof("<%v> invalid promise (amount: %v, channel: %v): %v", self.proto, amount, self.In, err)
+		return err
+	}
+
+	// credit remote peer with units
+	self.Add(-units)
+	glog.V(logger.Detail).Infof("<%v> received promise (amount: %v, channel: %v): %v", self.proto, amount, self.In, promise)
+
+	return nil
+}
+
+// stop() causes autocash loop to terminate.
+// Called after protocol handle loop terminates.
+func (self *Swap) Stop() {
+	defer self.lock.Unlock()
+	self.lock.Lock()
+	if self.Buys {
+		self.Out.Stop()
+	}
+	if self.Sells {
+		self.In.Stop()
+	}
+}
diff --git a/swarm/services/swap/swap/swap_test.go b/swarm/services/swap/swap/swap_test.go
new file mode 100644
index 000000000..222e0770f
--- /dev/null
+++ b/swarm/services/swap/swap/swap_test.go
@@ -0,0 +1,194 @@
+// Copyright 2016 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package swap
+
+import (
+	"math/big"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+)
+
+type testInPayment struct {
+	received         []*testPromise
+	autocashInterval time.Duration
+	autocashLimit    *big.Int
+}
+
+type testPromise struct {
+	amount *big.Int
+}
+
+func (self *testInPayment) Receive(promise Promise) (*big.Int, error) {
+	p := promise.(*testPromise)
+	self.received = append(self.received, p)
+	return p.amount, nil
+}
+
+func (self *testInPayment) AutoCash(interval time.Duration, limit *big.Int) {
+	self.autocashInterval = interval
+	self.autocashLimit = limit
+}
+
+func (self *testInPayment) Cash() (string, error) { return "", nil }
+
+func (self *testInPayment) Stop() {}
+
+type testOutPayment struct {
+	deposits             []*big.Int
+	autodepositInterval  time.Duration
+	autodepositThreshold *big.Int
+	autodepositBuffer    *big.Int
+}
+
+func (self *testOutPayment) Issue(amount *big.Int) (promise Promise, err error) {
+	return &testPromise{amount}, nil
+}
+
+func (self *testOutPayment) Deposit(amount *big.Int) (string, error) {
+	self.deposits = append(self.deposits, amount)
+	return "", nil
+}
+
+func (self *testOutPayment) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) {
+	self.autodepositInterval = interval
+	self.autodepositThreshold = threshold
+	self.autodepositBuffer = buffer
+}
+
+func (self *testOutPayment) Stop() {}
+
+type testProtocol struct {
+	drop     bool
+	amounts  []int
+	promises []*testPromise
+}
+
+func (self *testProtocol) Drop() {
+	self.drop = true
+}
+
+func (self *testProtocol) String() string {
+	return ""
+}
+
+func (self *testProtocol) Pay(amount int, promise Promise) {
+	p := promise.(*testPromise)
+	self.promises = append(self.promises, p)
+	self.amounts = append(self.amounts, amount)
+}
+
+func TestSwap(t *testing.T) {
+
+	strategy := &Strategy{
+		AutoCashInterval:     1 * time.Second,
+		AutoCashThreshold:    big.NewInt(20),
+		AutoDepositInterval:  1 * time.Second,
+		AutoDepositThreshold: big.NewInt(20),
+		AutoDepositBuffer:    big.NewInt(40),
+	}
+
+	local := &Params{
+		Profile: &Profile{
+			PayAt:  5,
+			DropAt: 10,
+			BuyAt:  common.Big3,
+			SellAt: common.Big2,
+		},
+		Strategy: strategy,
+	}
+
+	in := &testInPayment{}
+	out := &testOutPayment{}
+	proto := &testProtocol{}
+
+	swap, _ := New(local, Payment{In: in, Out: out, Buys: true, Sells: true}, proto)
+
+	if in.autocashInterval != strategy.AutoCashInterval {
+		t.Fatalf("autocash interval not properly set, expect %v, got %v", strategy.AutoCashInterval, in.autocashInterval)
+	}
+	if out.autodepositInterval != strategy.AutoDepositInterval {
+		t.Fatalf("autodeposit interval not properly set, expect %v, got %v", strategy.AutoDepositInterval, out.autodepositInterval)
+	}
+
+	remote := &Profile{
+		PayAt:  3,
+		DropAt: 10,
+		BuyAt:  common.Big2,
+		SellAt: common.Big3,
+	}
+	swap.SetRemote(remote)
+
+	swap.Add(9)
+	if proto.drop {
+		t.Fatalf("not expected peer to be dropped")
+	}
+	swap.Add(1)
+	if !proto.drop {
+		t.Fatalf("expected peer to be dropped")
+	}
+	if !proto.drop {
+		t.Fatalf("expected peer to be dropped")
+	}
+	proto.drop = false
+
+	swap.Receive(10, &testPromise{big.NewInt(20)})
+	if swap.balance != 0 {
+		t.Fatalf("expected zero balance, got %v", swap.balance)
+	}
+
+	if len(proto.amounts) != 0 {
+		t.Fatalf("expected zero balance, got %v", swap.balance)
+	}
+
+	swap.Add(-2)
+	if len(proto.amounts) > 0 {
+		t.Fatalf("expected no payments yet, got %v", proto.amounts)
+	}
+
+	swap.Add(-1)
+	if len(proto.amounts) != 1 {
+		t.Fatalf("expected one payment, got %v", len(proto.amounts))
+	}
+
+	if proto.amounts[0] != 3 {
+		t.Fatalf("expected payment for %v units, got %v", proto.amounts[0], 3)
+	}
+
+	exp := new(big.Int).Mul(big.NewInt(int64(proto.amounts[0])), remote.SellAt)
+	if proto.promises[0].amount.Cmp(exp) != 0 {
+		t.Fatalf("expected payment amount %v, got %v", exp, proto.promises[0].amount)
+	}
+
+	swap.SetParams(&Params{
+		Profile: &Profile{
+			PayAt:  5,
+			DropAt: 10,
+			BuyAt:  common.Big3,
+			SellAt: common.Big2,
+		},
+		Strategy: &Strategy{
+			AutoCashInterval:     2 * time.Second,
+			AutoCashThreshold:    big.NewInt(40),
+			AutoDepositInterval:  2 * time.Second,
+			AutoDepositThreshold: big.NewInt(40),
+			AutoDepositBuffer:    big.NewInt(60),
+		},
+	})
+
+}
-- 
cgit v1.2.3