github.com/neatlab/neatio@v1.7.3-0.20220425043230-d903e92fcc75/chain/contracts/chequebook/cheque.go (about)

     1  package chequebook
     2  
     3  //go:generate abigen --sol contract/chequebook.sol --exc contract/mortal.sol:mortal,contract/owned.sol:owned --pkg contract --out contract/chequebook.go
     4  //go:generate go run ./gencode.go
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/ecdsa"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io/ioutil"
    13  	"math/big"
    14  	"os"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/neatlab/neatio/chain/accounts/abi/bind"
    19  	"github.com/neatlab/neatio/chain/contracts/chequebook/contract"
    20  	"github.com/neatlab/neatio/chain/core/types"
    21  	"github.com/neatlab/neatio/chain/log"
    22  	"github.com/neatlab/neatio/utilities/common"
    23  	"github.com/neatlab/neatio/utilities/common/hexutil"
    24  	"github.com/neatlab/neatio/utilities/crypto"
    25  )
    26  
    27  
    28  var (
    29  	gasToCash = uint64(2000000)
    30  )
    31  
    32  type Backend interface {
    33  	bind.ContractBackend
    34  	TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
    35  	BalanceAt(ctx context.Context, address common.Address, blockNum *big.Int) (*big.Int, error)
    36  }
    37  
    38  type Cheque struct {
    39  	Contract    common.Address 
    40  	Beneficiary common.Address
    41  	Amount      *big.Int 
    42  	Sig         []byte 
    43  }
    44  
    45  func (self *Cheque) String() string {
    46  	return fmt.Sprintf("contract: %s, beneficiary: %s, amount: %v, signature: %x", self.Contract.String(), self.Beneficiary.String(), self.Amount, self.Sig)
    47  }
    48  
    49  type Params struct {
    50  	ContractCode, ContractAbi string
    51  }
    52  
    53  var ContractParams = &Params{contract.ChequebookBin, contract.ChequebookABI}
    54  
    55  type Chequebook struct {
    56  	path     string
    57  	prvKey   *ecdsa.PrivateKey
    58  	lock     sync.Mutex
    59  	backend  Backend
    60  	quit     chan bool
    61  	owner    common.Address
    62  	contract *contract.Chequebook
    63  	session  *contract.ChequebookSession
    64  
    65  	balance      *big.Int
    66  	contractAddr common.Address            
    67  	sent         map[common.Address]*big.Int
    68  
    69  	txhash    string
    70  	threshold *big.Int
    71  	buffer    *big.Int
    72  
    73  	log log.Logger
    74  }
    75  
    76  func (self *Chequebook) String() string {
    77  	return fmt.Sprintf("contract: %s, owner: %s, balance: %v, signer: %x", self.contractAddr.String(), self.owner.String(), self.balance, self.prvKey.PublicKey)
    78  }
    79  
    80  func NewChequebook(path string, contractAddr common.Address, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) {
    81  	balance := new(big.Int)
    82  	sent := make(map[common.Address]*big.Int)
    83  
    84  	chbook, err := contract.NewChequebook(contractAddr, backend)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	transactOpts := bind.NewKeyedTransactor(prvKey)
    89  	session := &contract.ChequebookSession{
    90  		Contract:     chbook,
    91  		TransactOpts: *transactOpts,
    92  	}
    93  
    94  	self = &Chequebook{
    95  		prvKey:       prvKey,
    96  		balance:      balance,
    97  		contractAddr: contractAddr,
    98  		sent:         sent,
    99  		path:         path,
   100  		backend:      backend,
   101  		owner:        transactOpts.From,
   102  		contract:     chbook,
   103  		session:      session,
   104  		log:          log.New("contract", contractAddr),
   105  	}
   106  
   107  	if (contractAddr != common.Address{}) {
   108  		self.setBalanceFromBlockChain()
   109  		self.log.Trace("New chequebook initialised", "owner", self.owner, "balance", self.balance)
   110  	}
   111  	return
   112  }
   113  
   114  func (self *Chequebook) setBalanceFromBlockChain() {
   115  	balance, err := self.backend.BalanceAt(context.TODO(), self.contractAddr, nil)
   116  	if err != nil {
   117  		log.Error("Failed to retrieve chequebook balance", "err", err)
   118  	} else {
   119  		self.balance.Set(balance)
   120  	}
   121  }
   122  
   123  func LoadChequebook(path string, prvKey *ecdsa.PrivateKey, backend Backend, checkBalance bool) (self *Chequebook, err error) {
   124  	var data []byte
   125  	data, err = ioutil.ReadFile(path)
   126  	if err != nil {
   127  		return
   128  	}
   129  	self, _ = NewChequebook(path, common.Address{}, prvKey, backend)
   130  
   131  	err = json.Unmarshal(data, self)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	if checkBalance {
   136  		self.setBalanceFromBlockChain()
   137  	}
   138  	log.Trace("Loaded chequebook from disk", "path", path)
   139  
   140  	return
   141  }
   142  
   143  type chequebookFile struct {
   144  	Balance  string
   145  	Contract string
   146  	Owner    string
   147  	Sent     map[string]string
   148  }
   149  
   150  func (self *Chequebook) UnmarshalJSON(data []byte) error {
   151  	var file chequebookFile
   152  	err := json.Unmarshal(data, &file)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	_, ok := self.balance.SetString(file.Balance, 10)
   157  	if !ok {
   158  		return fmt.Errorf("cumulative amount sent: unable to convert string to big integer: %v", file.Balance)
   159  	}
   160  	self.contractAddr = common.HexToAddress(file.Contract)
   161  	for addr, sent := range file.Sent {
   162  		self.sent[common.HexToAddress(addr)], ok = new(big.Int).SetString(sent, 10)
   163  		if !ok {
   164  			return fmt.Errorf("beneficiary %v cumulative amount sent: unable to convert string to big integer: %v", addr, sent)
   165  		}
   166  	}
   167  	return nil
   168  }
   169  
   170  func (self *Chequebook) MarshalJSON() ([]byte, error) {
   171  	var file = &chequebookFile{
   172  		Balance: self.balance.String(),
   173  		Contract: self.contractAddr.String(),
   174  		Owner: self.owner.String(),
   175  		Sent:  make(map[string]string),
   176  	}
   177  	for addr, sent := range self.sent {
   178  		file.Sent[addr.String()] = sent.String()
   179  	}
   180  	return json.Marshal(file)
   181  }
   182  
   183  func (self *Chequebook) Save() (err error) {
   184  	data, err := json.MarshalIndent(self, "", " ")
   185  	if err != nil {
   186  		return err
   187  	}
   188  	self.log.Trace("Saving chequebook to disk", self.path)
   189  
   190  	return ioutil.WriteFile(self.path, data, os.ModePerm)
   191  }
   192  
   193  func (self *Chequebook) Stop() {
   194  	defer self.lock.Unlock()
   195  	self.lock.Lock()
   196  	if self.quit != nil {
   197  		close(self.quit)
   198  		self.quit = nil
   199  	}
   200  }
   201  
   202  func (self *Chequebook) Issue(beneficiary common.Address, amount *big.Int) (ch *Cheque, err error) {
   203  	defer self.lock.Unlock()
   204  	self.lock.Lock()
   205  
   206  	if amount.Sign() <= 0 {
   207  		return nil, fmt.Errorf("amount must be greater than zero (%v)", amount)
   208  	}
   209  	if self.balance.Cmp(amount) < 0 {
   210  		err = fmt.Errorf("insufficient funds to issue cheque for amount: %v. balance: %v", amount, self.balance)
   211  	} else {
   212  		var sig []byte
   213  		sent, found := self.sent[beneficiary]
   214  		if !found {
   215  			sent = new(big.Int)
   216  			self.sent[beneficiary] = sent
   217  		}
   218  		sum := new(big.Int).Set(sent)
   219  		sum.Add(sum, amount)
   220  
   221  		sig, err = crypto.Sign(sigHash(self.contractAddr, beneficiary, sum), self.prvKey)
   222  		if err == nil {
   223  			ch = &Cheque{
   224  				Contract:    self.contractAddr,
   225  				Beneficiary: beneficiary,
   226  				Amount:      sum,
   227  				Sig:         sig,
   228  			}
   229  			sent.Set(sum)
   230  			self.balance.Sub(self.balance, amount)
   231  		}
   232  	}
   233  
   234  	if self.threshold != nil {
   235  		if self.balance.Cmp(self.threshold) < 0 {
   236  			send := new(big.Int).Sub(self.buffer, self.balance)
   237  			self.deposit(send)
   238  		}
   239  	}
   240  
   241  	return
   242  }
   243  
   244  func (self *Chequebook) Cash(ch *Cheque) (txhash string, err error) {
   245  	return ch.Cash(self.session)
   246  }
   247  
   248  func sigHash(contract, beneficiary common.Address, sum *big.Int) []byte {
   249  	bigamount := sum.Bytes()
   250  	if len(bigamount) > 32 {
   251  		return nil
   252  	}
   253  	var amount32 [32]byte
   254  	copy(amount32[32-len(bigamount):32], bigamount)
   255  	input := append(contract.Bytes(), beneficiary.Bytes()...)
   256  	input = append(input, amount32[:]...)
   257  	return crypto.Keccak256(input)
   258  }
   259  
   260  func (self *Chequebook) Balance() *big.Int {
   261  	defer self.lock.Unlock()
   262  	self.lock.Lock()
   263  	return new(big.Int).Set(self.balance)
   264  }
   265  
   266  func (self *Chequebook) Owner() common.Address {
   267  	return self.owner
   268  }
   269  
   270  func (self *Chequebook) Address() common.Address {
   271  	return self.contractAddr
   272  }
   273  
   274  func (self *Chequebook) Deposit(amount *big.Int) (string, error) {
   275  	defer self.lock.Unlock()
   276  	self.lock.Lock()
   277  	return self.deposit(amount)
   278  }
   279  
   280  func (self *Chequebook) deposit(amount *big.Int) (string, error) {
   281  	depositTransactor := bind.NewKeyedTransactor(self.prvKey)
   282  	depositTransactor.Value = amount
   283  	chbookRaw := &contract.ChequebookRaw{Contract: self.contract}
   284  	tx, err := chbookRaw.Transfer(depositTransactor)
   285  	if err != nil {
   286  		self.log.Warn("Failed to fund chequebook", "amount", amount, "balance", self.balance, "target", self.buffer, "err", err)
   287  		return "", err
   288  	}
   289  	self.balance.Add(self.balance, amount)
   290  	self.log.Trace("Deposited funds to chequebook", "amount", amount, "balance", self.balance, "target", self.buffer)
   291  	return tx.Hash().Hex(), nil
   292  }
   293  
   294  func (self *Chequebook) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) {
   295  	defer self.lock.Unlock()
   296  	self.lock.Lock()
   297  	self.threshold = threshold
   298  	self.buffer = buffer
   299  	self.autoDeposit(interval)
   300  }
   301  
   302  func (self *Chequebook) autoDeposit(interval time.Duration) {
   303  	if self.quit != nil {
   304  		close(self.quit)
   305  		self.quit = nil
   306  	}
   307  	if interval == time.Duration(0) || self.threshold != nil && self.buffer != nil && self.threshold.Cmp(self.buffer) >= 0 {
   308  		return
   309  	}
   310  
   311  	ticker := time.NewTicker(interval)
   312  	self.quit = make(chan bool)
   313  	quit := self.quit
   314  
   315  	go func() {
   316  		for {
   317  			select {
   318  			case <-quit:
   319  				return
   320  			case <-ticker.C:
   321  				self.lock.Lock()
   322  				if self.balance.Cmp(self.buffer) < 0 {
   323  					amount := new(big.Int).Sub(self.buffer, self.balance)
   324  					txhash, err := self.deposit(amount)
   325  					if err == nil {
   326  						self.txhash = txhash
   327  					}
   328  				}
   329  				self.lock.Unlock()
   330  			}
   331  		}
   332  	}()
   333  }
   334  
   335  type Outbox struct {
   336  	chequeBook  *Chequebook
   337  	beneficiary common.Address
   338  }
   339  
   340  func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox {
   341  	return &Outbox{chbook, beneficiary}
   342  }
   343  
   344  func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) {
   345  	self.chequeBook.AutoDeposit(interval, threshold, buffer)
   346  }
   347  
   348  func (self *Outbox) Stop() {}
   349  
   350  func (self *Outbox) String() string {
   351  	return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().String(), self.beneficiary.String(), self.chequeBook.Balance())
   352  }
   353  
   354  type Inbox struct {
   355  	lock        sync.Mutex
   356  	contract    common.Address              
   357  	beneficiary common.Address              
   358  	sender      common.Address              
   359  	signer      *ecdsa.PublicKey         
   360  	txhash      string                   
   361  	session     *contract.ChequebookSession 
   362  	quit        chan bool                   
   363  	maxUncashed *big.Int                  
   364  	cashed      *big.Int                  
   365  	cheque      *Cheque              
   366  	log         log.Logger       
   367  }
   368  
   369  func NewInbox(prvKey *ecdsa.PrivateKey, contractAddr, beneficiary common.Address, signer *ecdsa.PublicKey, abigen bind.ContractBackend) (self *Inbox, err error) {
   370  	if signer == nil {
   371  		return nil, fmt.Errorf("signer is null")
   372  	}
   373  	chbook, err := contract.NewChequebook(contractAddr, abigen)
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  	transactOpts := bind.NewKeyedTransactor(prvKey)
   378  	transactOpts.GasLimit = gasToCash
   379  	session := &contract.ChequebookSession{
   380  		Contract:     chbook,
   381  		TransactOpts: *transactOpts,
   382  	}
   383  	sender := transactOpts.From
   384  
   385  	self = &Inbox{
   386  		contract:    contractAddr,
   387  		beneficiary: beneficiary,
   388  		sender:      sender,
   389  		signer:      signer,
   390  		session:     session,
   391  		cashed:      new(big.Int).Set(common.Big0),
   392  		log:         log.New("contract", contractAddr),
   393  	}
   394  	self.log.Trace("New chequebook inbox initialized", "beneficiary", self.beneficiary, "signer", hexutil.Bytes(crypto.FromECDSAPub(signer)))
   395  	return
   396  }
   397  
   398  func (self *Inbox) String() string {
   399  	return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.contract.String(), self.beneficiary.String(), self.cheque.Amount)
   400  }
   401  
   402  func (self *Inbox) Stop() {
   403  	defer self.lock.Unlock()
   404  	self.lock.Lock()
   405  	if self.quit != nil {
   406  		close(self.quit)
   407  		self.quit = nil
   408  	}
   409  }
   410  
   411  func (self *Inbox) Cash() (txhash string, err error) {
   412  	if self.cheque != nil {
   413  		txhash, err = self.cheque.Cash(self.session)
   414  		self.log.Trace("Cashing in chequebook cheque", "amount", self.cheque.Amount, "beneficiary", self.beneficiary)
   415  		self.cashed = self.cheque.Amount
   416  	}
   417  	return
   418  }
   419  
   420  func (self *Inbox) AutoCash(cashInterval time.Duration, maxUncashed *big.Int) {
   421  	defer self.lock.Unlock()
   422  	self.lock.Lock()
   423  	self.maxUncashed = maxUncashed
   424  	self.autoCash(cashInterval)
   425  }
   426  
   427  func (self *Inbox) autoCash(cashInterval time.Duration) {
   428  	if self.quit != nil {
   429  		close(self.quit)
   430  		self.quit = nil
   431  	}
   432  	if cashInterval == time.Duration(0) || self.maxUncashed != nil && self.maxUncashed.Sign() == 0 {
   433  		return
   434  	}
   435  
   436  	ticker := time.NewTicker(cashInterval)
   437  	self.quit = make(chan bool)
   438  	quit := self.quit
   439  
   440  	go func() {
   441  		for {
   442  			select {
   443  			case <-quit:
   444  				return
   445  			case <-ticker.C:
   446  				self.lock.Lock()
   447  				if self.cheque != nil && self.cheque.Amount.Cmp(self.cashed) != 0 {
   448  					txhash, err := self.Cash()
   449  					if err == nil {
   450  						self.txhash = txhash
   451  					}
   452  				}
   453  				self.lock.Unlock()
   454  			}
   455  		}
   456  	}()
   457  }
   458  
   459  func (self *Cheque) Verify(signerKey *ecdsa.PublicKey, contract, beneficiary common.Address, sum *big.Int) (*big.Int, error) {
   460  	log.Trace("Verifying chequebook cheque", "cheque", self, "sum", sum)
   461  	if sum == nil {
   462  		return nil, fmt.Errorf("invalid amount")
   463  	}
   464  
   465  	if self.Beneficiary != beneficiary {
   466  		return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.String(), beneficiary.String())
   467  	}
   468  	if self.Contract != contract {
   469  		return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.String(), contract.String())
   470  	}
   471  
   472  	amount := new(big.Int).Set(self.Amount)
   473  	if sum != nil {
   474  		amount.Sub(amount, sum)
   475  		if amount.Sign() <= 0 {
   476  			return nil, fmt.Errorf("incorrect amount: %v <= 0", amount)
   477  		}
   478  	}
   479  
   480  	pubKey, err := crypto.SigToPub(sigHash(self.Contract, beneficiary, self.Amount), self.Sig)
   481  	if err != nil {
   482  		return nil, fmt.Errorf("invalid signature: %v", err)
   483  	}
   484  	if !bytes.Equal(crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) {
   485  		return nil, fmt.Errorf("signer mismatch: %x != %x", crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey))
   486  	}
   487  	return amount, nil
   488  }
   489  
   490  func sig2vrs(sig []byte) (v byte, r, s [32]byte) {
   491  	v = sig[64] + 27
   492  	copy(r[:], sig[:32])
   493  	copy(s[:], sig[32:64])
   494  	return
   495  }
   496  
   497  func (self *Cheque) Cash(session *contract.ChequebookSession) (string, error) {
   498  	v, r, s := sig2vrs(self.Sig)
   499  	tx, err := session.Cash(self.Beneficiary, self.Amount, v, r, s)
   500  	if err != nil {
   501  		return "", err
   502  	}
   503  	return tx.Hash().Hex(), nil
   504  }
   505  
   506  func ValidateCode(ctx context.Context, b Backend, address common.Address) (ok bool, err error) {
   507  	code, err := b.CodeAt(ctx, address, nil)
   508  	if err != nil {
   509  		return false, err
   510  	}
   511  	return bytes.Equal(code, common.FromHex(contract.ContractDeployedCode)), nil
   512  }