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