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 }