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 }