├── .env ├── .gitignore ├── README.md ├── abi ├── __init__.py ├── btcb_abi.py ├── erc20_abi.py ├── optimism_gas_oracle_abi.py └── stargate_router_abi.py ├── base └── errors.py ├── btcb ├── __init__.py ├── btcb.py └── constants.py ├── config.py ├── exchange ├── __init__.py ├── binance │ ├── binance.py │ └── constants.py ├── exchange.py ├── factory.py └── okex │ ├── constants.py │ └── okex.py ├── logger.py ├── logic ├── __init__.py ├── account_thread.py ├── btcb_states.py ├── stargate_states.py └── state.py ├── lz.py ├── network ├── __init__.py ├── arbitrum │ ├── arbitrum.py │ └── constants.py ├── avalanche │ ├── avalanche.py │ └── constants.py ├── balance_helper.py ├── bsc │ ├── bsc.py │ └── constants.py ├── ethereum │ ├── constants.py │ └── ethereum.py ├── fantom │ ├── constants.py │ └── fantom.py ├── network.py ├── optimism │ ├── constants.py │ └── optimism.py └── polygon │ ├── constants.py │ └── polygon.py ├── private_keys.txt ├── requirements.txt ├── stargate ├── __init__.py ├── constants.py └── stargate.py └── utility ├── __init__.py ├── stablecoin.py └── wallet.py /.env: -------------------------------------------------------------------------------- 1 | # RPC 2 | ETHEREUM_RPC=https://eth.llamarpc.com 3 | OPTIMISM_RPC=https://opt-mainnet.g.alchemy.com/v2/demo 4 | ARBITRUM_RPC=https://arb-mainnet-public.unifra.io 5 | BSC_RPC=https://rpc.ankr.com/bsc 6 | POLYGON_RPC=https://rpc.ankr.com/polygon 7 | AVALANCHE_RPC=https://rpc.ankr.com/avalanche 8 | FANTOM_RPC=https://rpc.ankr.com/fantom 9 | 10 | # 0.01 - 1% 11 | STARGATE_SLIPPAGE=0.01 12 | # Minimal balance to make bridge 13 | STARGATE_MIN_STABLECOIN_BALANCE=30 14 | BTCB_MIN_BALANCE=0.0001 15 | 16 | # Keys 17 | BINANCE_API_KEY=key 18 | BINANCE_SECRET_KEY=key 19 | 20 | OKEX_API_KEY=key 21 | OKEX_SECRET_KEY=key 22 | OKEX_PASSWORD=key 23 | 24 | # Utility 25 | DEFAULT_PRIVATE_KEYS_FILE_PATH=private_keys.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .nox/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | *.cover 39 | *.py,cover 40 | .hypothesis/ 41 | .pytest_cache/ 42 | cover/ 43 | 44 | # Cache 45 | .mypy_cache/ 46 | 47 | # IDE files 48 | .idea/ 49 | 50 | # OS specific files 51 | .DS_Store 52 | 53 | # Environments 54 | #.env 55 | .venv 56 | env/ 57 | venv/ 58 | ENV/ 59 | env.bak/ 60 | venv.bak/ 61 | 62 | private_keys.txt 63 | logs/ 64 | generated_keys/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # layerzero-bridger 2 | 3 | #### The layerzero-bridger is an app with the command-line interface that provides various functionalities for working with LayerZero bridges. It allows you to generate private keys, withdraw funds from exchanges, and execute bridges multiple times in a random sequence. 4 | 5 | ## Features 6 | 7 | - Support for all popular EVM networks - Ethereum, Arbitrum, Optimism, Polygon, Fantom, Avalanche, BSC 8 | - Scanning of networks for stablecoins/BTC.b on balance 9 | - Bridge via Stargate, BTC.b 10 | - Complete randomization of paths and timings. No patterns 11 | - Simultaneous operation of multiple accounts in different threads 12 | - Automatic refuel from Binance and Okex exchanges (withdrawal of the native token to pay gas fees) 13 | 14 | ### Usage 15 | 16 | 1. Install Python 3.9.2 (another version is possible, but I can't vouch for it) 17 | 2. Go to the directory with the repository (you will probably have a different path): 18 | 19 | ``` 20 | cd layerzero-bridger 21 | ``` 22 | 23 | 3. Initialize the virtual environment and install the dependencies: 24 | 25 | ```shell 26 | python3 -m venv venv 27 | source venv/bin/activate 28 | pip install -r requirements.txt 29 | ``` 30 | 4. Read the documentation: 31 | 32 | ```shell 33 | python3 lz.py -h 34 | ``` 35 | 36 | 5. Run one of the supported commands: 37 | ```shell 38 | python3 lz.py generate [] 39 | python3 lz.py withdraw [--min_time=] [--max_time=] [--keys=] [--exchange=] 40 | python3 lz.py run [--keys=] [--refuel=] [--limit=] 41 | ``` 42 | -------------------------------------------------------------------------------- /abi/__init__.py: -------------------------------------------------------------------------------- 1 | from abi.erc20_abi import ERC20_ABI 2 | from abi.stargate_router_abi import STARGATE_ROUTER_ABI 3 | from abi.btcb_abi import BTCB_ABI 4 | from abi.optimism_gas_oracle_abi import OPTIMISM_GAS_ORACLE_ABI 5 | -------------------------------------------------------------------------------- /abi/btcb_abi.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | BTCB_ABI = json.loads(''' 4 | [ 5 | { 6 | "inputs": [ 7 | { 8 | "internalType": "address", 9 | "name": "_lzEndpoint", 10 | "type": "address" 11 | } 12 | ], 13 | "stateMutability": "nonpayable", 14 | "type": "constructor" 15 | }, 16 | { 17 | "anonymous": false, 18 | "inputs": [ 19 | { 20 | "indexed": true, 21 | "internalType": "address", 22 | "name": "owner", 23 | "type": "address" 24 | }, 25 | { 26 | "indexed": true, 27 | "internalType": "address", 28 | "name": "spender", 29 | "type": "address" 30 | }, 31 | { 32 | "indexed": false, 33 | "internalType": "uint256", 34 | "name": "value", 35 | "type": "uint256" 36 | } 37 | ], 38 | "name": "Approval", 39 | "type": "event" 40 | }, 41 | { 42 | "anonymous": false, 43 | "inputs": [ 44 | { 45 | "indexed": true, 46 | "internalType": "uint16", 47 | "name": "_srcChainId", 48 | "type": "uint16" 49 | }, 50 | { 51 | "indexed": false, 52 | "internalType": "bytes", 53 | "name": "_srcAddress", 54 | "type": "bytes" 55 | }, 56 | { 57 | "indexed": false, 58 | "internalType": "uint64", 59 | "name": "_nonce", 60 | "type": "uint64" 61 | }, 62 | { 63 | "indexed": false, 64 | "internalType": "bytes32", 65 | "name": "_hash", 66 | "type": "bytes32" 67 | } 68 | ], 69 | "name": "CallOFTReceivedSuccess", 70 | "type": "event" 71 | }, 72 | { 73 | "anonymous": false, 74 | "inputs": [ 75 | { 76 | "indexed": false, 77 | "internalType": "uint16", 78 | "name": "_srcChainId", 79 | "type": "uint16" 80 | }, 81 | { 82 | "indexed": false, 83 | "internalType": "bytes", 84 | "name": "_srcAddress", 85 | "type": "bytes" 86 | }, 87 | { 88 | "indexed": false, 89 | "internalType": "uint64", 90 | "name": "_nonce", 91 | "type": "uint64" 92 | }, 93 | { 94 | "indexed": false, 95 | "internalType": "bytes", 96 | "name": "_payload", 97 | "type": "bytes" 98 | }, 99 | { 100 | "indexed": false, 101 | "internalType": "bytes", 102 | "name": "_reason", 103 | "type": "bytes" 104 | } 105 | ], 106 | "name": "MessageFailed", 107 | "type": "event" 108 | }, 109 | { 110 | "anonymous": false, 111 | "inputs": [ 112 | { 113 | "indexed": false, 114 | "internalType": "address", 115 | "name": "_address", 116 | "type": "address" 117 | } 118 | ], 119 | "name": "NonContractAddress", 120 | "type": "event" 121 | }, 122 | { 123 | "anonymous": false, 124 | "inputs": [ 125 | { 126 | "indexed": true, 127 | "internalType": "address", 128 | "name": "previousOwner", 129 | "type": "address" 130 | }, 131 | { 132 | "indexed": true, 133 | "internalType": "address", 134 | "name": "newOwner", 135 | "type": "address" 136 | } 137 | ], 138 | "name": "OwnershipTransferred", 139 | "type": "event" 140 | }, 141 | { 142 | "anonymous": false, 143 | "inputs": [ 144 | { 145 | "indexed": true, 146 | "internalType": "uint16", 147 | "name": "_srcChainId", 148 | "type": "uint16" 149 | }, 150 | { 151 | "indexed": true, 152 | "internalType": "address", 153 | "name": "_to", 154 | "type": "address" 155 | }, 156 | { 157 | "indexed": false, 158 | "internalType": "uint256", 159 | "name": "_amount", 160 | "type": "uint256" 161 | } 162 | ], 163 | "name": "ReceiveFromChain", 164 | "type": "event" 165 | }, 166 | { 167 | "anonymous": false, 168 | "inputs": [ 169 | { 170 | "indexed": false, 171 | "internalType": "uint16", 172 | "name": "_srcChainId", 173 | "type": "uint16" 174 | }, 175 | { 176 | "indexed": false, 177 | "internalType": "bytes", 178 | "name": "_srcAddress", 179 | "type": "bytes" 180 | }, 181 | { 182 | "indexed": false, 183 | "internalType": "uint64", 184 | "name": "_nonce", 185 | "type": "uint64" 186 | }, 187 | { 188 | "indexed": false, 189 | "internalType": "bytes32", 190 | "name": "_payloadHash", 191 | "type": "bytes32" 192 | } 193 | ], 194 | "name": "RetryMessageSuccess", 195 | "type": "event" 196 | }, 197 | { 198 | "anonymous": false, 199 | "inputs": [ 200 | { 201 | "indexed": true, 202 | "internalType": "uint16", 203 | "name": "_dstChainId", 204 | "type": "uint16" 205 | }, 206 | { 207 | "indexed": true, 208 | "internalType": "address", 209 | "name": "_from", 210 | "type": "address" 211 | }, 212 | { 213 | "indexed": true, 214 | "internalType": "bytes32", 215 | "name": "_toAddress", 216 | "type": "bytes32" 217 | }, 218 | { 219 | "indexed": false, 220 | "internalType": "uint256", 221 | "name": "_amount", 222 | "type": "uint256" 223 | } 224 | ], 225 | "name": "SendToChain", 226 | "type": "event" 227 | }, 228 | { 229 | "anonymous": false, 230 | "inputs": [ 231 | { 232 | "indexed": false, 233 | "internalType": "uint16", 234 | "name": "feeBp", 235 | "type": "uint16" 236 | } 237 | ], 238 | "name": "SetDefaultFeeBp", 239 | "type": "event" 240 | }, 241 | { 242 | "anonymous": false, 243 | "inputs": [ 244 | { 245 | "indexed": false, 246 | "internalType": "uint16", 247 | "name": "dstchainId", 248 | "type": "uint16" 249 | }, 250 | { 251 | "indexed": false, 252 | "internalType": "bool", 253 | "name": "enabled", 254 | "type": "bool" 255 | }, 256 | { 257 | "indexed": false, 258 | "internalType": "uint16", 259 | "name": "feeBp", 260 | "type": "uint16" 261 | } 262 | ], 263 | "name": "SetFeeBp", 264 | "type": "event" 265 | }, 266 | { 267 | "anonymous": false, 268 | "inputs": [ 269 | { 270 | "indexed": false, 271 | "internalType": "address", 272 | "name": "feeOwner", 273 | "type": "address" 274 | } 275 | ], 276 | "name": "SetFeeOwner", 277 | "type": "event" 278 | }, 279 | { 280 | "anonymous": false, 281 | "inputs": [ 282 | { 283 | "indexed": false, 284 | "internalType": "uint16", 285 | "name": "_dstChainId", 286 | "type": "uint16" 287 | }, 288 | { 289 | "indexed": false, 290 | "internalType": "uint16", 291 | "name": "_type", 292 | "type": "uint16" 293 | }, 294 | { 295 | "indexed": false, 296 | "internalType": "uint256", 297 | "name": "_minDstGas", 298 | "type": "uint256" 299 | } 300 | ], 301 | "name": "SetMinDstGas", 302 | "type": "event" 303 | }, 304 | { 305 | "anonymous": false, 306 | "inputs": [ 307 | { 308 | "indexed": false, 309 | "internalType": "address", 310 | "name": "precrime", 311 | "type": "address" 312 | } 313 | ], 314 | "name": "SetPrecrime", 315 | "type": "event" 316 | }, 317 | { 318 | "anonymous": false, 319 | "inputs": [ 320 | { 321 | "indexed": false, 322 | "internalType": "uint16", 323 | "name": "_remoteChainId", 324 | "type": "uint16" 325 | }, 326 | { 327 | "indexed": false, 328 | "internalType": "bytes", 329 | "name": "_path", 330 | "type": "bytes" 331 | } 332 | ], 333 | "name": "SetTrustedRemote", 334 | "type": "event" 335 | }, 336 | { 337 | "anonymous": false, 338 | "inputs": [ 339 | { 340 | "indexed": false, 341 | "internalType": "uint16", 342 | "name": "_remoteChainId", 343 | "type": "uint16" 344 | }, 345 | { 346 | "indexed": false, 347 | "internalType": "bytes", 348 | "name": "_remoteAddress", 349 | "type": "bytes" 350 | } 351 | ], 352 | "name": "SetTrustedRemoteAddress", 353 | "type": "event" 354 | }, 355 | { 356 | "anonymous": false, 357 | "inputs": [ 358 | { 359 | "indexed": false, 360 | "internalType": "bool", 361 | "name": "_useCustomAdapterParams", 362 | "type": "bool" 363 | } 364 | ], 365 | "name": "SetUseCustomAdapterParams", 366 | "type": "event" 367 | }, 368 | { 369 | "anonymous": false, 370 | "inputs": [ 371 | { 372 | "indexed": true, 373 | "internalType": "address", 374 | "name": "from", 375 | "type": "address" 376 | }, 377 | { 378 | "indexed": true, 379 | "internalType": "address", 380 | "name": "to", 381 | "type": "address" 382 | }, 383 | { 384 | "indexed": false, 385 | "internalType": "uint256", 386 | "name": "value", 387 | "type": "uint256" 388 | } 389 | ], 390 | "name": "Transfer", 391 | "type": "event" 392 | }, 393 | { 394 | "inputs": [], 395 | "name": "BP_DENOMINATOR", 396 | "outputs": [ 397 | { 398 | "internalType": "uint256", 399 | "name": "", 400 | "type": "uint256" 401 | } 402 | ], 403 | "stateMutability": "view", 404 | "type": "function" 405 | }, 406 | { 407 | "inputs": [], 408 | "name": "NO_EXTRA_GAS", 409 | "outputs": [ 410 | { 411 | "internalType": "uint256", 412 | "name": "", 413 | "type": "uint256" 414 | } 415 | ], 416 | "stateMutability": "view", 417 | "type": "function" 418 | }, 419 | { 420 | "inputs": [], 421 | "name": "PT_SEND", 422 | "outputs": [ 423 | { 424 | "internalType": "uint8", 425 | "name": "", 426 | "type": "uint8" 427 | } 428 | ], 429 | "stateMutability": "view", 430 | "type": "function" 431 | }, 432 | { 433 | "inputs": [], 434 | "name": "PT_SEND_AND_CALL", 435 | "outputs": [ 436 | { 437 | "internalType": "uint8", 438 | "name": "", 439 | "type": "uint8" 440 | } 441 | ], 442 | "stateMutability": "view", 443 | "type": "function" 444 | }, 445 | { 446 | "inputs": [ 447 | { 448 | "internalType": "address", 449 | "name": "owner", 450 | "type": "address" 451 | }, 452 | { 453 | "internalType": "address", 454 | "name": "spender", 455 | "type": "address" 456 | } 457 | ], 458 | "name": "allowance", 459 | "outputs": [ 460 | { 461 | "internalType": "uint256", 462 | "name": "", 463 | "type": "uint256" 464 | } 465 | ], 466 | "stateMutability": "view", 467 | "type": "function" 468 | }, 469 | { 470 | "inputs": [ 471 | { 472 | "internalType": "address", 473 | "name": "spender", 474 | "type": "address" 475 | }, 476 | { 477 | "internalType": "uint256", 478 | "name": "amount", 479 | "type": "uint256" 480 | } 481 | ], 482 | "name": "approve", 483 | "outputs": [ 484 | { 485 | "internalType": "bool", 486 | "name": "", 487 | "type": "bool" 488 | } 489 | ], 490 | "stateMutability": "nonpayable", 491 | "type": "function" 492 | }, 493 | { 494 | "inputs": [ 495 | { 496 | "internalType": "address", 497 | "name": "account", 498 | "type": "address" 499 | } 500 | ], 501 | "name": "balanceOf", 502 | "outputs": [ 503 | { 504 | "internalType": "uint256", 505 | "name": "", 506 | "type": "uint256" 507 | } 508 | ], 509 | "stateMutability": "view", 510 | "type": "function" 511 | }, 512 | { 513 | "inputs": [ 514 | { 515 | "internalType": "uint16", 516 | "name": "_srcChainId", 517 | "type": "uint16" 518 | }, 519 | { 520 | "internalType": "bytes", 521 | "name": "_srcAddress", 522 | "type": "bytes" 523 | }, 524 | { 525 | "internalType": "uint64", 526 | "name": "_nonce", 527 | "type": "uint64" 528 | }, 529 | { 530 | "internalType": "bytes32", 531 | "name": "_from", 532 | "type": "bytes32" 533 | }, 534 | { 535 | "internalType": "address", 536 | "name": "_to", 537 | "type": "address" 538 | }, 539 | { 540 | "internalType": "uint256", 541 | "name": "_amount", 542 | "type": "uint256" 543 | }, 544 | { 545 | "internalType": "bytes", 546 | "name": "_payload", 547 | "type": "bytes" 548 | }, 549 | { 550 | "internalType": "uint256", 551 | "name": "_gasForCall", 552 | "type": "uint256" 553 | } 554 | ], 555 | "name": "callOnOFTReceived", 556 | "outputs": [], 557 | "stateMutability": "nonpayable", 558 | "type": "function" 559 | }, 560 | { 561 | "inputs": [ 562 | { 563 | "internalType": "uint16", 564 | "name": "", 565 | "type": "uint16" 566 | } 567 | ], 568 | "name": "chainIdToFeeBps", 569 | "outputs": [ 570 | { 571 | "internalType": "uint16", 572 | "name": "feeBP", 573 | "type": "uint16" 574 | }, 575 | { 576 | "internalType": "bool", 577 | "name": "enabled", 578 | "type": "bool" 579 | } 580 | ], 581 | "stateMutability": "view", 582 | "type": "function" 583 | }, 584 | { 585 | "inputs": [], 586 | "name": "circulatingSupply", 587 | "outputs": [ 588 | { 589 | "internalType": "uint256", 590 | "name": "", 591 | "type": "uint256" 592 | } 593 | ], 594 | "stateMutability": "view", 595 | "type": "function" 596 | }, 597 | { 598 | "inputs": [ 599 | { 600 | "internalType": "uint16", 601 | "name": "", 602 | "type": "uint16" 603 | }, 604 | { 605 | "internalType": "bytes", 606 | "name": "", 607 | "type": "bytes" 608 | }, 609 | { 610 | "internalType": "uint64", 611 | "name": "", 612 | "type": "uint64" 613 | } 614 | ], 615 | "name": "creditedPackets", 616 | "outputs": [ 617 | { 618 | "internalType": "bool", 619 | "name": "", 620 | "type": "bool" 621 | } 622 | ], 623 | "stateMutability": "view", 624 | "type": "function" 625 | }, 626 | { 627 | "inputs": [], 628 | "name": "decimals", 629 | "outputs": [ 630 | { 631 | "internalType": "uint8", 632 | "name": "", 633 | "type": "uint8" 634 | } 635 | ], 636 | "stateMutability": "pure", 637 | "type": "function" 638 | }, 639 | { 640 | "inputs": [ 641 | { 642 | "internalType": "address", 643 | "name": "spender", 644 | "type": "address" 645 | }, 646 | { 647 | "internalType": "uint256", 648 | "name": "subtractedValue", 649 | "type": "uint256" 650 | } 651 | ], 652 | "name": "decreaseAllowance", 653 | "outputs": [ 654 | { 655 | "internalType": "bool", 656 | "name": "", 657 | "type": "bool" 658 | } 659 | ], 660 | "stateMutability": "nonpayable", 661 | "type": "function" 662 | }, 663 | { 664 | "inputs": [], 665 | "name": "defaultFeeBp", 666 | "outputs": [ 667 | { 668 | "internalType": "uint16", 669 | "name": "", 670 | "type": "uint16" 671 | } 672 | ], 673 | "stateMutability": "view", 674 | "type": "function" 675 | }, 676 | { 677 | "inputs": [ 678 | { 679 | "internalType": "uint16", 680 | "name": "_dstChainId", 681 | "type": "uint16" 682 | }, 683 | { 684 | "internalType": "bytes32", 685 | "name": "_toAddress", 686 | "type": "bytes32" 687 | }, 688 | { 689 | "internalType": "uint256", 690 | "name": "_amount", 691 | "type": "uint256" 692 | }, 693 | { 694 | "internalType": "bytes", 695 | "name": "_payload", 696 | "type": "bytes" 697 | }, 698 | { 699 | "internalType": "uint64", 700 | "name": "_dstGasForCall", 701 | "type": "uint64" 702 | }, 703 | { 704 | "internalType": "bool", 705 | "name": "_useZro", 706 | "type": "bool" 707 | }, 708 | { 709 | "internalType": "bytes", 710 | "name": "_adapterParams", 711 | "type": "bytes" 712 | } 713 | ], 714 | "name": "estimateSendAndCallFee", 715 | "outputs": [ 716 | { 717 | "internalType": "uint256", 718 | "name": "nativeFee", 719 | "type": "uint256" 720 | }, 721 | { 722 | "internalType": "uint256", 723 | "name": "zroFee", 724 | "type": "uint256" 725 | } 726 | ], 727 | "stateMutability": "view", 728 | "type": "function" 729 | }, 730 | { 731 | "inputs": [ 732 | { 733 | "internalType": "uint16", 734 | "name": "_dstChainId", 735 | "type": "uint16" 736 | }, 737 | { 738 | "internalType": "bytes32", 739 | "name": "_toAddress", 740 | "type": "bytes32" 741 | }, 742 | { 743 | "internalType": "uint256", 744 | "name": "_amount", 745 | "type": "uint256" 746 | }, 747 | { 748 | "internalType": "bool", 749 | "name": "_useZro", 750 | "type": "bool" 751 | }, 752 | { 753 | "internalType": "bytes", 754 | "name": "_adapterParams", 755 | "type": "bytes" 756 | } 757 | ], 758 | "name": "estimateSendFee", 759 | "outputs": [ 760 | { 761 | "internalType": "uint256", 762 | "name": "nativeFee", 763 | "type": "uint256" 764 | }, 765 | { 766 | "internalType": "uint256", 767 | "name": "zroFee", 768 | "type": "uint256" 769 | } 770 | ], 771 | "stateMutability": "view", 772 | "type": "function" 773 | }, 774 | { 775 | "inputs": [ 776 | { 777 | "internalType": "uint16", 778 | "name": "", 779 | "type": "uint16" 780 | }, 781 | { 782 | "internalType": "bytes", 783 | "name": "", 784 | "type": "bytes" 785 | }, 786 | { 787 | "internalType": "uint64", 788 | "name": "", 789 | "type": "uint64" 790 | } 791 | ], 792 | "name": "failedMessages", 793 | "outputs": [ 794 | { 795 | "internalType": "bytes32", 796 | "name": "", 797 | "type": "bytes32" 798 | } 799 | ], 800 | "stateMutability": "view", 801 | "type": "function" 802 | }, 803 | { 804 | "inputs": [], 805 | "name": "feeOwner", 806 | "outputs": [ 807 | { 808 | "internalType": "address", 809 | "name": "", 810 | "type": "address" 811 | } 812 | ], 813 | "stateMutability": "view", 814 | "type": "function" 815 | }, 816 | { 817 | "inputs": [ 818 | { 819 | "internalType": "uint16", 820 | "name": "_srcChainId", 821 | "type": "uint16" 822 | }, 823 | { 824 | "internalType": "bytes", 825 | "name": "_srcAddress", 826 | "type": "bytes" 827 | } 828 | ], 829 | "name": "forceResumeReceive", 830 | "outputs": [], 831 | "stateMutability": "nonpayable", 832 | "type": "function" 833 | }, 834 | { 835 | "inputs": [ 836 | { 837 | "internalType": "uint16", 838 | "name": "_version", 839 | "type": "uint16" 840 | }, 841 | { 842 | "internalType": "uint16", 843 | "name": "_chainId", 844 | "type": "uint16" 845 | }, 846 | { 847 | "internalType": "address", 848 | "name": "", 849 | "type": "address" 850 | }, 851 | { 852 | "internalType": "uint256", 853 | "name": "_configType", 854 | "type": "uint256" 855 | } 856 | ], 857 | "name": "getConfig", 858 | "outputs": [ 859 | { 860 | "internalType": "bytes", 861 | "name": "", 862 | "type": "bytes" 863 | } 864 | ], 865 | "stateMutability": "view", 866 | "type": "function" 867 | }, 868 | { 869 | "inputs": [ 870 | { 871 | "internalType": "uint16", 872 | "name": "_remoteChainId", 873 | "type": "uint16" 874 | } 875 | ], 876 | "name": "getTrustedRemoteAddress", 877 | "outputs": [ 878 | { 879 | "internalType": "bytes", 880 | "name": "", 881 | "type": "bytes" 882 | } 883 | ], 884 | "stateMutability": "view", 885 | "type": "function" 886 | }, 887 | { 888 | "inputs": [ 889 | { 890 | "internalType": "address", 891 | "name": "spender", 892 | "type": "address" 893 | }, 894 | { 895 | "internalType": "uint256", 896 | "name": "addedValue", 897 | "type": "uint256" 898 | } 899 | ], 900 | "name": "increaseAllowance", 901 | "outputs": [ 902 | { 903 | "internalType": "bool", 904 | "name": "", 905 | "type": "bool" 906 | } 907 | ], 908 | "stateMutability": "nonpayable", 909 | "type": "function" 910 | }, 911 | { 912 | "inputs": [ 913 | { 914 | "internalType": "uint16", 915 | "name": "_srcChainId", 916 | "type": "uint16" 917 | }, 918 | { 919 | "internalType": "bytes", 920 | "name": "_srcAddress", 921 | "type": "bytes" 922 | } 923 | ], 924 | "name": "isTrustedRemote", 925 | "outputs": [ 926 | { 927 | "internalType": "bool", 928 | "name": "", 929 | "type": "bool" 930 | } 931 | ], 932 | "stateMutability": "view", 933 | "type": "function" 934 | }, 935 | { 936 | "inputs": [], 937 | "name": "lzEndpoint", 938 | "outputs": [ 939 | { 940 | "internalType": "contract ILayerZeroEndpoint", 941 | "name": "", 942 | "type": "address" 943 | } 944 | ], 945 | "stateMutability": "view", 946 | "type": "function" 947 | }, 948 | { 949 | "inputs": [ 950 | { 951 | "internalType": "uint16", 952 | "name": "_srcChainId", 953 | "type": "uint16" 954 | }, 955 | { 956 | "internalType": "bytes", 957 | "name": "_srcAddress", 958 | "type": "bytes" 959 | }, 960 | { 961 | "internalType": "uint64", 962 | "name": "_nonce", 963 | "type": "uint64" 964 | }, 965 | { 966 | "internalType": "bytes", 967 | "name": "_payload", 968 | "type": "bytes" 969 | } 970 | ], 971 | "name": "lzReceive", 972 | "outputs": [], 973 | "stateMutability": "nonpayable", 974 | "type": "function" 975 | }, 976 | { 977 | "inputs": [ 978 | { 979 | "internalType": "uint16", 980 | "name": "", 981 | "type": "uint16" 982 | }, 983 | { 984 | "internalType": "uint16", 985 | "name": "", 986 | "type": "uint16" 987 | } 988 | ], 989 | "name": "minDstGasLookup", 990 | "outputs": [ 991 | { 992 | "internalType": "uint256", 993 | "name": "", 994 | "type": "uint256" 995 | } 996 | ], 997 | "stateMutability": "view", 998 | "type": "function" 999 | }, 1000 | { 1001 | "inputs": [], 1002 | "name": "name", 1003 | "outputs": [ 1004 | { 1005 | "internalType": "string", 1006 | "name": "", 1007 | "type": "string" 1008 | } 1009 | ], 1010 | "stateMutability": "view", 1011 | "type": "function" 1012 | }, 1013 | { 1014 | "inputs": [ 1015 | { 1016 | "internalType": "uint16", 1017 | "name": "_srcChainId", 1018 | "type": "uint16" 1019 | }, 1020 | { 1021 | "internalType": "bytes", 1022 | "name": "_srcAddress", 1023 | "type": "bytes" 1024 | }, 1025 | { 1026 | "internalType": "uint64", 1027 | "name": "_nonce", 1028 | "type": "uint64" 1029 | }, 1030 | { 1031 | "internalType": "bytes", 1032 | "name": "_payload", 1033 | "type": "bytes" 1034 | } 1035 | ], 1036 | "name": "nonblockingLzReceive", 1037 | "outputs": [], 1038 | "stateMutability": "nonpayable", 1039 | "type": "function" 1040 | }, 1041 | { 1042 | "inputs": [], 1043 | "name": "owner", 1044 | "outputs": [ 1045 | { 1046 | "internalType": "address", 1047 | "name": "", 1048 | "type": "address" 1049 | } 1050 | ], 1051 | "stateMutability": "view", 1052 | "type": "function" 1053 | }, 1054 | { 1055 | "inputs": [], 1056 | "name": "precrime", 1057 | "outputs": [ 1058 | { 1059 | "internalType": "address", 1060 | "name": "", 1061 | "type": "address" 1062 | } 1063 | ], 1064 | "stateMutability": "view", 1065 | "type": "function" 1066 | }, 1067 | { 1068 | "inputs": [ 1069 | { 1070 | "internalType": "uint16", 1071 | "name": "_dstChainId", 1072 | "type": "uint16" 1073 | }, 1074 | { 1075 | "internalType": "uint256", 1076 | "name": "_amount", 1077 | "type": "uint256" 1078 | } 1079 | ], 1080 | "name": "quoteOFTFee", 1081 | "outputs": [ 1082 | { 1083 | "internalType": "uint256", 1084 | "name": "fee", 1085 | "type": "uint256" 1086 | } 1087 | ], 1088 | "stateMutability": "view", 1089 | "type": "function" 1090 | }, 1091 | { 1092 | "inputs": [], 1093 | "name": "renounceOwnership", 1094 | "outputs": [], 1095 | "stateMutability": "nonpayable", 1096 | "type": "function" 1097 | }, 1098 | { 1099 | "inputs": [ 1100 | { 1101 | "internalType": "uint16", 1102 | "name": "_srcChainId", 1103 | "type": "uint16" 1104 | }, 1105 | { 1106 | "internalType": "bytes", 1107 | "name": "_srcAddress", 1108 | "type": "bytes" 1109 | }, 1110 | { 1111 | "internalType": "uint64", 1112 | "name": "_nonce", 1113 | "type": "uint64" 1114 | }, 1115 | { 1116 | "internalType": "bytes", 1117 | "name": "_payload", 1118 | "type": "bytes" 1119 | } 1120 | ], 1121 | "name": "retryMessage", 1122 | "outputs": [], 1123 | "stateMutability": "payable", 1124 | "type": "function" 1125 | }, 1126 | { 1127 | "inputs": [ 1128 | { 1129 | "internalType": "address", 1130 | "name": "_from", 1131 | "type": "address" 1132 | }, 1133 | { 1134 | "internalType": "uint16", 1135 | "name": "_dstChainId", 1136 | "type": "uint16" 1137 | }, 1138 | { 1139 | "internalType": "bytes32", 1140 | "name": "_toAddress", 1141 | "type": "bytes32" 1142 | }, 1143 | { 1144 | "internalType": "uint256", 1145 | "name": "_amount", 1146 | "type": "uint256" 1147 | }, 1148 | { 1149 | "internalType": "uint256", 1150 | "name": "_minAmount", 1151 | "type": "uint256" 1152 | }, 1153 | { 1154 | "internalType": "bytes", 1155 | "name": "_payload", 1156 | "type": "bytes" 1157 | }, 1158 | { 1159 | "internalType": "uint64", 1160 | "name": "_dstGasForCall", 1161 | "type": "uint64" 1162 | }, 1163 | { 1164 | "components": [ 1165 | { 1166 | "internalType": "address payable", 1167 | "name": "refundAddress", 1168 | "type": "address" 1169 | }, 1170 | { 1171 | "internalType": "address", 1172 | "name": "zroPaymentAddress", 1173 | "type": "address" 1174 | }, 1175 | { 1176 | "internalType": "bytes", 1177 | "name": "adapterParams", 1178 | "type": "bytes" 1179 | } 1180 | ], 1181 | "internalType": "struct ICommonOFT.LzCallParams", 1182 | "name": "_callParams", 1183 | "type": "tuple" 1184 | } 1185 | ], 1186 | "name": "sendAndCall", 1187 | "outputs": [], 1188 | "stateMutability": "payable", 1189 | "type": "function" 1190 | }, 1191 | { 1192 | "inputs": [ 1193 | { 1194 | "internalType": "address", 1195 | "name": "_from", 1196 | "type": "address" 1197 | }, 1198 | { 1199 | "internalType": "uint16", 1200 | "name": "_dstChainId", 1201 | "type": "uint16" 1202 | }, 1203 | { 1204 | "internalType": "bytes32", 1205 | "name": "_toAddress", 1206 | "type": "bytes32" 1207 | }, 1208 | { 1209 | "internalType": "uint256", 1210 | "name": "_amount", 1211 | "type": "uint256" 1212 | }, 1213 | { 1214 | "internalType": "uint256", 1215 | "name": "_minAmount", 1216 | "type": "uint256" 1217 | }, 1218 | { 1219 | "components": [ 1220 | { 1221 | "internalType": "address payable", 1222 | "name": "refundAddress", 1223 | "type": "address" 1224 | }, 1225 | { 1226 | "internalType": "address", 1227 | "name": "zroPaymentAddress", 1228 | "type": "address" 1229 | }, 1230 | { 1231 | "internalType": "bytes", 1232 | "name": "adapterParams", 1233 | "type": "bytes" 1234 | } 1235 | ], 1236 | "internalType": "struct ICommonOFT.LzCallParams", 1237 | "name": "_callParams", 1238 | "type": "tuple" 1239 | } 1240 | ], 1241 | "name": "sendFrom", 1242 | "outputs": [], 1243 | "stateMutability": "payable", 1244 | "type": "function" 1245 | }, 1246 | { 1247 | "inputs": [ 1248 | { 1249 | "internalType": "uint16", 1250 | "name": "_version", 1251 | "type": "uint16" 1252 | }, 1253 | { 1254 | "internalType": "uint16", 1255 | "name": "_chainId", 1256 | "type": "uint16" 1257 | }, 1258 | { 1259 | "internalType": "uint256", 1260 | "name": "_configType", 1261 | "type": "uint256" 1262 | }, 1263 | { 1264 | "internalType": "bytes", 1265 | "name": "_config", 1266 | "type": "bytes" 1267 | } 1268 | ], 1269 | "name": "setConfig", 1270 | "outputs": [], 1271 | "stateMutability": "nonpayable", 1272 | "type": "function" 1273 | }, 1274 | { 1275 | "inputs": [ 1276 | { 1277 | "internalType": "uint16", 1278 | "name": "_feeBp", 1279 | "type": "uint16" 1280 | } 1281 | ], 1282 | "name": "setDefaultFeeBp", 1283 | "outputs": [], 1284 | "stateMutability": "nonpayable", 1285 | "type": "function" 1286 | }, 1287 | { 1288 | "inputs": [ 1289 | { 1290 | "internalType": "uint16", 1291 | "name": "_dstChainId", 1292 | "type": "uint16" 1293 | }, 1294 | { 1295 | "internalType": "bool", 1296 | "name": "_enabled", 1297 | "type": "bool" 1298 | }, 1299 | { 1300 | "internalType": "uint16", 1301 | "name": "_feeBp", 1302 | "type": "uint16" 1303 | } 1304 | ], 1305 | "name": "setFeeBp", 1306 | "outputs": [], 1307 | "stateMutability": "nonpayable", 1308 | "type": "function" 1309 | }, 1310 | { 1311 | "inputs": [ 1312 | { 1313 | "internalType": "address", 1314 | "name": "_feeOwner", 1315 | "type": "address" 1316 | } 1317 | ], 1318 | "name": "setFeeOwner", 1319 | "outputs": [], 1320 | "stateMutability": "nonpayable", 1321 | "type": "function" 1322 | }, 1323 | { 1324 | "inputs": [ 1325 | { 1326 | "internalType": "uint16", 1327 | "name": "_dstChainId", 1328 | "type": "uint16" 1329 | }, 1330 | { 1331 | "internalType": "uint16", 1332 | "name": "_packetType", 1333 | "type": "uint16" 1334 | }, 1335 | { 1336 | "internalType": "uint256", 1337 | "name": "_minGas", 1338 | "type": "uint256" 1339 | } 1340 | ], 1341 | "name": "setMinDstGas", 1342 | "outputs": [], 1343 | "stateMutability": "nonpayable", 1344 | "type": "function" 1345 | }, 1346 | { 1347 | "inputs": [ 1348 | { 1349 | "internalType": "address", 1350 | "name": "_precrime", 1351 | "type": "address" 1352 | } 1353 | ], 1354 | "name": "setPrecrime", 1355 | "outputs": [], 1356 | "stateMutability": "nonpayable", 1357 | "type": "function" 1358 | }, 1359 | { 1360 | "inputs": [ 1361 | { 1362 | "internalType": "uint16", 1363 | "name": "_version", 1364 | "type": "uint16" 1365 | } 1366 | ], 1367 | "name": "setReceiveVersion", 1368 | "outputs": [], 1369 | "stateMutability": "nonpayable", 1370 | "type": "function" 1371 | }, 1372 | { 1373 | "inputs": [ 1374 | { 1375 | "internalType": "uint16", 1376 | "name": "_version", 1377 | "type": "uint16" 1378 | } 1379 | ], 1380 | "name": "setSendVersion", 1381 | "outputs": [], 1382 | "stateMutability": "nonpayable", 1383 | "type": "function" 1384 | }, 1385 | { 1386 | "inputs": [ 1387 | { 1388 | "internalType": "uint16", 1389 | "name": "_srcChainId", 1390 | "type": "uint16" 1391 | }, 1392 | { 1393 | "internalType": "bytes", 1394 | "name": "_path", 1395 | "type": "bytes" 1396 | } 1397 | ], 1398 | "name": "setTrustedRemote", 1399 | "outputs": [], 1400 | "stateMutability": "nonpayable", 1401 | "type": "function" 1402 | }, 1403 | { 1404 | "inputs": [ 1405 | { 1406 | "internalType": "uint16", 1407 | "name": "_remoteChainId", 1408 | "type": "uint16" 1409 | }, 1410 | { 1411 | "internalType": "bytes", 1412 | "name": "_remoteAddress", 1413 | "type": "bytes" 1414 | } 1415 | ], 1416 | "name": "setTrustedRemoteAddress", 1417 | "outputs": [], 1418 | "stateMutability": "nonpayable", 1419 | "type": "function" 1420 | }, 1421 | { 1422 | "inputs": [ 1423 | { 1424 | "internalType": "bool", 1425 | "name": "_useCustomAdapterParams", 1426 | "type": "bool" 1427 | } 1428 | ], 1429 | "name": "setUseCustomAdapterParams", 1430 | "outputs": [], 1431 | "stateMutability": "nonpayable", 1432 | "type": "function" 1433 | }, 1434 | { 1435 | "inputs": [], 1436 | "name": "sharedDecimals", 1437 | "outputs": [ 1438 | { 1439 | "internalType": "uint8", 1440 | "name": "", 1441 | "type": "uint8" 1442 | } 1443 | ], 1444 | "stateMutability": "view", 1445 | "type": "function" 1446 | }, 1447 | { 1448 | "inputs": [ 1449 | { 1450 | "internalType": "bytes4", 1451 | "name": "interfaceId", 1452 | "type": "bytes4" 1453 | } 1454 | ], 1455 | "name": "supportsInterface", 1456 | "outputs": [ 1457 | { 1458 | "internalType": "bool", 1459 | "name": "", 1460 | "type": "bool" 1461 | } 1462 | ], 1463 | "stateMutability": "view", 1464 | "type": "function" 1465 | }, 1466 | { 1467 | "inputs": [], 1468 | "name": "symbol", 1469 | "outputs": [ 1470 | { 1471 | "internalType": "string", 1472 | "name": "", 1473 | "type": "string" 1474 | } 1475 | ], 1476 | "stateMutability": "view", 1477 | "type": "function" 1478 | }, 1479 | { 1480 | "inputs": [], 1481 | "name": "token", 1482 | "outputs": [ 1483 | { 1484 | "internalType": "address", 1485 | "name": "", 1486 | "type": "address" 1487 | } 1488 | ], 1489 | "stateMutability": "view", 1490 | "type": "function" 1491 | }, 1492 | { 1493 | "inputs": [], 1494 | "name": "totalSupply", 1495 | "outputs": [ 1496 | { 1497 | "internalType": "uint256", 1498 | "name": "", 1499 | "type": "uint256" 1500 | } 1501 | ], 1502 | "stateMutability": "view", 1503 | "type": "function" 1504 | }, 1505 | { 1506 | "inputs": [ 1507 | { 1508 | "internalType": "address", 1509 | "name": "to", 1510 | "type": "address" 1511 | }, 1512 | { 1513 | "internalType": "uint256", 1514 | "name": "amount", 1515 | "type": "uint256" 1516 | } 1517 | ], 1518 | "name": "transfer", 1519 | "outputs": [ 1520 | { 1521 | "internalType": "bool", 1522 | "name": "", 1523 | "type": "bool" 1524 | } 1525 | ], 1526 | "stateMutability": "nonpayable", 1527 | "type": "function" 1528 | }, 1529 | { 1530 | "inputs": [ 1531 | { 1532 | "internalType": "address", 1533 | "name": "from", 1534 | "type": "address" 1535 | }, 1536 | { 1537 | "internalType": "address", 1538 | "name": "to", 1539 | "type": "address" 1540 | }, 1541 | { 1542 | "internalType": "uint256", 1543 | "name": "amount", 1544 | "type": "uint256" 1545 | } 1546 | ], 1547 | "name": "transferFrom", 1548 | "outputs": [ 1549 | { 1550 | "internalType": "bool", 1551 | "name": "", 1552 | "type": "bool" 1553 | } 1554 | ], 1555 | "stateMutability": "nonpayable", 1556 | "type": "function" 1557 | }, 1558 | { 1559 | "inputs": [ 1560 | { 1561 | "internalType": "address", 1562 | "name": "newOwner", 1563 | "type": "address" 1564 | } 1565 | ], 1566 | "name": "transferOwnership", 1567 | "outputs": [], 1568 | "stateMutability": "nonpayable", 1569 | "type": "function" 1570 | }, 1571 | { 1572 | "inputs": [ 1573 | { 1574 | "internalType": "uint16", 1575 | "name": "", 1576 | "type": "uint16" 1577 | } 1578 | ], 1579 | "name": "trustedRemoteLookup", 1580 | "outputs": [ 1581 | { 1582 | "internalType": "bytes", 1583 | "name": "", 1584 | "type": "bytes" 1585 | } 1586 | ], 1587 | "stateMutability": "view", 1588 | "type": "function" 1589 | }, 1590 | { 1591 | "inputs": [], 1592 | "name": "useCustomAdapterParams", 1593 | "outputs": [ 1594 | { 1595 | "internalType": "bool", 1596 | "name": "", 1597 | "type": "bool" 1598 | } 1599 | ], 1600 | "stateMutability": "view", 1601 | "type": "function" 1602 | } 1603 | ] 1604 | ''') 1605 | -------------------------------------------------------------------------------- /abi/erc20_abi.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | ERC20_ABI = json.loads(''' 4 | [ 5 | { 6 | "constant": true, 7 | "inputs": [], 8 | "name": "name", 9 | "outputs": [ 10 | { 11 | "name": "", 12 | "type": "string" 13 | } 14 | ], 15 | "payable": false, 16 | "stateMutability": "view", 17 | "type": "function" 18 | }, 19 | { 20 | "constant": false, 21 | "inputs": [ 22 | { 23 | "name": "_spender", 24 | "type": "address" 25 | }, 26 | { 27 | "name": "_value", 28 | "type": "uint256" 29 | } 30 | ], 31 | "name": "approve", 32 | "outputs": [ 33 | { 34 | "name": "", 35 | "type": "bool" 36 | } 37 | ], 38 | "payable": false, 39 | "stateMutability": "nonpayable", 40 | "type": "function" 41 | }, 42 | { 43 | "constant": true, 44 | "inputs": [], 45 | "name": "totalSupply", 46 | "outputs": [ 47 | { 48 | "name": "", 49 | "type": "uint256" 50 | } 51 | ], 52 | "payable": false, 53 | "stateMutability": "view", 54 | "type": "function" 55 | }, 56 | { 57 | "constant": false, 58 | "inputs": [ 59 | { 60 | "name": "_from", 61 | "type": "address" 62 | }, 63 | { 64 | "name": "_to", 65 | "type": "address" 66 | }, 67 | { 68 | "name": "_value", 69 | "type": "uint256" 70 | } 71 | ], 72 | "name": "transferFrom", 73 | "outputs": [ 74 | { 75 | "name": "", 76 | "type": "bool" 77 | } 78 | ], 79 | "payable": false, 80 | "stateMutability": "nonpayable", 81 | "type": "function" 82 | }, 83 | { 84 | "constant": true, 85 | "inputs": [], 86 | "name": "decimals", 87 | "outputs": [ 88 | { 89 | "name": "", 90 | "type": "uint8" 91 | } 92 | ], 93 | "payable": false, 94 | "stateMutability": "view", 95 | "type": "function" 96 | }, 97 | { 98 | "constant": true, 99 | "inputs": [ 100 | { 101 | "name": "_owner", 102 | "type": "address" 103 | } 104 | ], 105 | "name": "balanceOf", 106 | "outputs": [ 107 | { 108 | "name": "", 109 | "type": "uint256" 110 | } 111 | ], 112 | "payable": false, 113 | "stateMutability": "view", 114 | "type": "function" 115 | }, 116 | { 117 | "constant": true, 118 | "inputs": [], 119 | "name": "symbol", 120 | "outputs": [ 121 | { 122 | "name": "", 123 | "type": "string" 124 | } 125 | ], 126 | "payable": false, 127 | "stateMutability": "view", 128 | "type": "function" 129 | }, 130 | { 131 | "constant": false, 132 | "inputs": [ 133 | { 134 | "name": "_to", 135 | "type": "address" 136 | }, 137 | { 138 | "name": "_value", 139 | "type": "uint256" 140 | } 141 | ], 142 | "name": "transfer", 143 | "outputs": [ 144 | { 145 | "name": "", 146 | "type": "bool" 147 | } 148 | ], 149 | "payable": false, 150 | "stateMutability": "nonpayable", 151 | "type": "function" 152 | }, 153 | { 154 | "constant": true, 155 | "inputs": [ 156 | { 157 | "name": "_owner", 158 | "type": "address" 159 | }, 160 | { 161 | "name": "_spender", 162 | "type": "address" 163 | } 164 | ], 165 | "name": "allowance", 166 | "outputs": [ 167 | { 168 | "name": "", 169 | "type": "uint256" 170 | } 171 | ], 172 | "payable": false, 173 | "stateMutability": "view", 174 | "type": "function" 175 | }, 176 | { 177 | "anonymous": false, 178 | "inputs": [ 179 | { 180 | "indexed": true, 181 | "name": "_from", 182 | "type": "address" 183 | }, 184 | { 185 | "indexed": true, 186 | "name": "_to", 187 | "type": "address" 188 | }, 189 | { 190 | "indexed": false, 191 | "name": "_value", 192 | "type": "uint256" 193 | } 194 | ], 195 | "name": "Transfer", 196 | "type": "event" 197 | }, 198 | { 199 | "anonymous": false, 200 | "inputs": [ 201 | { 202 | "indexed": true, 203 | "name": "_owner", 204 | "type": "address" 205 | }, 206 | { 207 | "indexed": true, 208 | "name": "_spender", 209 | "type": "address" 210 | }, 211 | { 212 | "indexed": false, 213 | "name": "_value", 214 | "type": "uint256" 215 | } 216 | ], 217 | "name": "Approval", 218 | "type": "event" 219 | }, 220 | { 221 | "constant": true, 222 | "inputs": [], 223 | "name": "NAME", 224 | "outputs": [ 225 | { 226 | "name": "", 227 | "type": "string" 228 | } 229 | ], 230 | "payable": false, 231 | "stateMutability": "view", 232 | "type": "function" 233 | }, 234 | { 235 | "constant": true, 236 | "inputs": [], 237 | "name": "SYMBOL", 238 | "outputs": [ 239 | { 240 | "name": "", 241 | "type": "string" 242 | } 243 | ], 244 | "payable": false, 245 | "stateMutability": "view", 246 | "type": "function" 247 | }, 248 | { 249 | "constant": true, 250 | "inputs": [], 251 | "name": "DECIMALS", 252 | "outputs": [ 253 | { 254 | "name": "", 255 | "type": "uint8" 256 | } 257 | ], 258 | "payable": false, 259 | "stateMutability": "view", 260 | "type": "function" 261 | } 262 | ] 263 | ''') 264 | -------------------------------------------------------------------------------- /abi/optimism_gas_oracle_abi.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | OPTIMISM_GAS_ORACLE_ABI = json.loads(''' 4 | [ 5 | { 6 | "inputs": [ 7 | { 8 | "internalType": "address", 9 | "name": "_owner", 10 | "type": "address" 11 | } 12 | ], 13 | "stateMutability": "nonpayable", 14 | "type": "constructor" 15 | }, 16 | { 17 | "anonymous": false, 18 | "inputs": [ 19 | { 20 | "indexed": false, 21 | "internalType": "uint256", 22 | "name": "", 23 | "type": "uint256" 24 | } 25 | ], 26 | "name": "DecimalsUpdated", 27 | "type": "event" 28 | }, 29 | { 30 | "anonymous": false, 31 | "inputs": [ 32 | { 33 | "indexed": false, 34 | "internalType": "uint256", 35 | "name": "", 36 | "type": "uint256" 37 | } 38 | ], 39 | "name": "GasPriceUpdated", 40 | "type": "event" 41 | }, 42 | { 43 | "anonymous": false, 44 | "inputs": [ 45 | { 46 | "indexed": false, 47 | "internalType": "uint256", 48 | "name": "", 49 | "type": "uint256" 50 | } 51 | ], 52 | "name": "L1BaseFeeUpdated", 53 | "type": "event" 54 | }, 55 | { 56 | "anonymous": false, 57 | "inputs": [ 58 | { 59 | "indexed": false, 60 | "internalType": "uint256", 61 | "name": "", 62 | "type": "uint256" 63 | } 64 | ], 65 | "name": "OverheadUpdated", 66 | "type": "event" 67 | }, 68 | { 69 | "anonymous": false, 70 | "inputs": [ 71 | { 72 | "indexed": true, 73 | "internalType": "address", 74 | "name": "previousOwner", 75 | "type": "address" 76 | }, 77 | { 78 | "indexed": true, 79 | "internalType": "address", 80 | "name": "newOwner", 81 | "type": "address" 82 | } 83 | ], 84 | "name": "OwnershipTransferred", 85 | "type": "event" 86 | }, 87 | { 88 | "anonymous": false, 89 | "inputs": [ 90 | { 91 | "indexed": false, 92 | "internalType": "uint256", 93 | "name": "", 94 | "type": "uint256" 95 | } 96 | ], 97 | "name": "ScalarUpdated", 98 | "type": "event" 99 | }, 100 | { 101 | "inputs": [], 102 | "name": "decimals", 103 | "outputs": [ 104 | { 105 | "internalType": "uint256", 106 | "name": "", 107 | "type": "uint256" 108 | } 109 | ], 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "inputs": [], 115 | "name": "gasPrice", 116 | "outputs": [ 117 | { 118 | "internalType": "uint256", 119 | "name": "", 120 | "type": "uint256" 121 | } 122 | ], 123 | "stateMutability": "view", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [ 128 | { 129 | "internalType": "bytes", 130 | "name": "_data", 131 | "type": "bytes" 132 | } 133 | ], 134 | "name": "getL1Fee", 135 | "outputs": [ 136 | { 137 | "internalType": "uint256", 138 | "name": "", 139 | "type": "uint256" 140 | } 141 | ], 142 | "stateMutability": "view", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [ 147 | { 148 | "internalType": "bytes", 149 | "name": "_data", 150 | "type": "bytes" 151 | } 152 | ], 153 | "name": "getL1GasUsed", 154 | "outputs": [ 155 | { 156 | "internalType": "uint256", 157 | "name": "", 158 | "type": "uint256" 159 | } 160 | ], 161 | "stateMutability": "view", 162 | "type": "function" 163 | }, 164 | { 165 | "inputs": [], 166 | "name": "l1BaseFee", 167 | "outputs": [ 168 | { 169 | "internalType": "uint256", 170 | "name": "", 171 | "type": "uint256" 172 | } 173 | ], 174 | "stateMutability": "view", 175 | "type": "function" 176 | }, 177 | { 178 | "inputs": [], 179 | "name": "overhead", 180 | "outputs": [ 181 | { 182 | "internalType": "uint256", 183 | "name": "", 184 | "type": "uint256" 185 | } 186 | ], 187 | "stateMutability": "view", 188 | "type": "function" 189 | }, 190 | { 191 | "inputs": [], 192 | "name": "owner", 193 | "outputs": [ 194 | { 195 | "internalType": "address", 196 | "name": "", 197 | "type": "address" 198 | } 199 | ], 200 | "stateMutability": "view", 201 | "type": "function" 202 | }, 203 | { 204 | "inputs": [], 205 | "name": "renounceOwnership", 206 | "outputs": [], 207 | "stateMutability": "nonpayable", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": [], 212 | "name": "scalar", 213 | "outputs": [ 214 | { 215 | "internalType": "uint256", 216 | "name": "", 217 | "type": "uint256" 218 | } 219 | ], 220 | "stateMutability": "view", 221 | "type": "function" 222 | }, 223 | { 224 | "inputs": [ 225 | { 226 | "internalType": "uint256", 227 | "name": "_decimals", 228 | "type": "uint256" 229 | } 230 | ], 231 | "name": "setDecimals", 232 | "outputs": [], 233 | "stateMutability": "nonpayable", 234 | "type": "function" 235 | }, 236 | { 237 | "inputs": [ 238 | { 239 | "internalType": "uint256", 240 | "name": "_gasPrice", 241 | "type": "uint256" 242 | } 243 | ], 244 | "name": "setGasPrice", 245 | "outputs": [], 246 | "stateMutability": "nonpayable", 247 | "type": "function" 248 | }, 249 | { 250 | "inputs": [ 251 | { 252 | "internalType": "uint256", 253 | "name": "_baseFee", 254 | "type": "uint256" 255 | } 256 | ], 257 | "name": "setL1BaseFee", 258 | "outputs": [], 259 | "stateMutability": "nonpayable", 260 | "type": "function" 261 | }, 262 | { 263 | "inputs": [ 264 | { 265 | "internalType": "uint256", 266 | "name": "_overhead", 267 | "type": "uint256" 268 | } 269 | ], 270 | "name": "setOverhead", 271 | "outputs": [], 272 | "stateMutability": "nonpayable", 273 | "type": "function" 274 | }, 275 | { 276 | "inputs": [ 277 | { 278 | "internalType": "uint256", 279 | "name": "_scalar", 280 | "type": "uint256" 281 | } 282 | ], 283 | "name": "setScalar", 284 | "outputs": [], 285 | "stateMutability": "nonpayable", 286 | "type": "function" 287 | }, 288 | { 289 | "inputs": [ 290 | { 291 | "internalType": "address", 292 | "name": "newOwner", 293 | "type": "address" 294 | } 295 | ], 296 | "name": "transferOwnership", 297 | "outputs": [], 298 | "stateMutability": "nonpayable", 299 | "type": "function" 300 | } 301 | ] 302 | ''') 303 | -------------------------------------------------------------------------------- /abi/stargate_router_abi.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | STARGATE_ROUTER_ABI = json.loads(''' 4 | [ 5 | { 6 | "inputs": [], 7 | "stateMutability": "nonpayable", 8 | "type": "constructor" 9 | }, 10 | { 11 | "anonymous": false, 12 | "inputs": [ 13 | { 14 | "indexed": false, 15 | "internalType": "uint16", 16 | "name": "chainId", 17 | "type": "uint16" 18 | }, 19 | { 20 | "indexed": false, 21 | "internalType": "bytes", 22 | "name": "srcAddress", 23 | "type": "bytes" 24 | }, 25 | { 26 | "indexed": false, 27 | "internalType": "uint256", 28 | "name": "nonce", 29 | "type": "uint256" 30 | }, 31 | { 32 | "indexed": false, 33 | "internalType": "address", 34 | "name": "token", 35 | "type": "address" 36 | }, 37 | { 38 | "indexed": false, 39 | "internalType": "uint256", 40 | "name": "amountLD", 41 | "type": "uint256" 42 | }, 43 | { 44 | "indexed": false, 45 | "internalType": "address", 46 | "name": "to", 47 | "type": "address" 48 | }, 49 | { 50 | "indexed": false, 51 | "internalType": "bytes", 52 | "name": "payload", 53 | "type": "bytes" 54 | }, 55 | { 56 | "indexed": false, 57 | "internalType": "bytes", 58 | "name": "reason", 59 | "type": "bytes" 60 | } 61 | ], 62 | "name": "CachedSwapSaved", 63 | "type": "event" 64 | }, 65 | { 66 | "anonymous": false, 67 | "inputs": [ 68 | { 69 | "indexed": true, 70 | "internalType": "address", 71 | "name": "previousOwner", 72 | "type": "address" 73 | }, 74 | { 75 | "indexed": true, 76 | "internalType": "address", 77 | "name": "newOwner", 78 | "type": "address" 79 | } 80 | ], 81 | "name": "OwnershipTransferred", 82 | "type": "event" 83 | }, 84 | { 85 | "anonymous": false, 86 | "inputs": [ 87 | { 88 | "indexed": false, 89 | "internalType": "uint16", 90 | "name": "srcChainId", 91 | "type": "uint16" 92 | }, 93 | { 94 | "indexed": true, 95 | "internalType": "bytes", 96 | "name": "srcAddress", 97 | "type": "bytes" 98 | }, 99 | { 100 | "indexed": true, 101 | "internalType": "uint256", 102 | "name": "nonce", 103 | "type": "uint256" 104 | }, 105 | { 106 | "indexed": false, 107 | "internalType": "uint256", 108 | "name": "srcPoolId", 109 | "type": "uint256" 110 | }, 111 | { 112 | "indexed": false, 113 | "internalType": "uint256", 114 | "name": "dstPoolId", 115 | "type": "uint256" 116 | }, 117 | { 118 | "indexed": false, 119 | "internalType": "address", 120 | "name": "to", 121 | "type": "address" 122 | }, 123 | { 124 | "indexed": false, 125 | "internalType": "uint256", 126 | "name": "amountSD", 127 | "type": "uint256" 128 | }, 129 | { 130 | "indexed": false, 131 | "internalType": "uint256", 132 | "name": "mintAmountSD", 133 | "type": "uint256" 134 | } 135 | ], 136 | "name": "RedeemLocalCallback", 137 | "type": "event" 138 | }, 139 | { 140 | "anonymous": false, 141 | "inputs": [ 142 | { 143 | "indexed": false, 144 | "internalType": "uint8", 145 | "name": "bridgeFunctionType", 146 | "type": "uint8" 147 | }, 148 | { 149 | "indexed": false, 150 | "internalType": "uint16", 151 | "name": "chainId", 152 | "type": "uint16" 153 | }, 154 | { 155 | "indexed": false, 156 | "internalType": "bytes", 157 | "name": "srcAddress", 158 | "type": "bytes" 159 | }, 160 | { 161 | "indexed": false, 162 | "internalType": "uint256", 163 | "name": "nonce", 164 | "type": "uint256" 165 | } 166 | ], 167 | "name": "Revert", 168 | "type": "event" 169 | }, 170 | { 171 | "anonymous": false, 172 | "inputs": [ 173 | { 174 | "indexed": false, 175 | "internalType": "uint16", 176 | "name": "srcChainId", 177 | "type": "uint16" 178 | }, 179 | { 180 | "indexed": false, 181 | "internalType": "uint256", 182 | "name": "_srcPoolId", 183 | "type": "uint256" 184 | }, 185 | { 186 | "indexed": false, 187 | "internalType": "uint256", 188 | "name": "_dstPoolId", 189 | "type": "uint256" 190 | }, 191 | { 192 | "indexed": false, 193 | "internalType": "bytes", 194 | "name": "to", 195 | "type": "bytes" 196 | }, 197 | { 198 | "indexed": false, 199 | "internalType": "uint256", 200 | "name": "redeemAmountSD", 201 | "type": "uint256" 202 | }, 203 | { 204 | "indexed": false, 205 | "internalType": "uint256", 206 | "name": "mintAmountSD", 207 | "type": "uint256" 208 | }, 209 | { 210 | "indexed": true, 211 | "internalType": "uint256", 212 | "name": "nonce", 213 | "type": "uint256" 214 | }, 215 | { 216 | "indexed": true, 217 | "internalType": "bytes", 218 | "name": "srcAddress", 219 | "type": "bytes" 220 | } 221 | ], 222 | "name": "RevertRedeemLocal", 223 | "type": "event" 224 | }, 225 | { 226 | "inputs": [ 227 | { 228 | "internalType": "uint256", 229 | "name": "_poolId", 230 | "type": "uint256" 231 | }, 232 | { 233 | "internalType": "uint16", 234 | "name": "_dstChainId", 235 | "type": "uint16" 236 | }, 237 | { 238 | "internalType": "uint256", 239 | "name": "_dstPoolId", 240 | "type": "uint256" 241 | } 242 | ], 243 | "name": "activateChainPath", 244 | "outputs": [], 245 | "stateMutability": "nonpayable", 246 | "type": "function" 247 | }, 248 | { 249 | "inputs": [ 250 | { 251 | "internalType": "uint256", 252 | "name": "_poolId", 253 | "type": "uint256" 254 | }, 255 | { 256 | "internalType": "uint256", 257 | "name": "_amountLD", 258 | "type": "uint256" 259 | }, 260 | { 261 | "internalType": "address", 262 | "name": "_to", 263 | "type": "address" 264 | } 265 | ], 266 | "name": "addLiquidity", 267 | "outputs": [], 268 | "stateMutability": "nonpayable", 269 | "type": "function" 270 | }, 271 | { 272 | "inputs": [], 273 | "name": "bridge", 274 | "outputs": [ 275 | { 276 | "internalType": "contract Bridge", 277 | "name": "", 278 | "type": "address" 279 | } 280 | ], 281 | "stateMutability": "view", 282 | "type": "function" 283 | }, 284 | { 285 | "inputs": [ 286 | { 287 | "internalType": "uint16", 288 | "name": "", 289 | "type": "uint16" 290 | }, 291 | { 292 | "internalType": "bytes", 293 | "name": "", 294 | "type": "bytes" 295 | }, 296 | { 297 | "internalType": "uint256", 298 | "name": "", 299 | "type": "uint256" 300 | } 301 | ], 302 | "name": "cachedSwapLookup", 303 | "outputs": [ 304 | { 305 | "internalType": "address", 306 | "name": "token", 307 | "type": "address" 308 | }, 309 | { 310 | "internalType": "uint256", 311 | "name": "amountLD", 312 | "type": "uint256" 313 | }, 314 | { 315 | "internalType": "address", 316 | "name": "to", 317 | "type": "address" 318 | }, 319 | { 320 | "internalType": "bytes", 321 | "name": "payload", 322 | "type": "bytes" 323 | } 324 | ], 325 | "stateMutability": "view", 326 | "type": "function" 327 | }, 328 | { 329 | "inputs": [ 330 | { 331 | "internalType": "uint256", 332 | "name": "_poolId", 333 | "type": "uint256" 334 | }, 335 | { 336 | "internalType": "bool", 337 | "name": "_fullMode", 338 | "type": "bool" 339 | } 340 | ], 341 | "name": "callDelta", 342 | "outputs": [], 343 | "stateMutability": "nonpayable", 344 | "type": "function" 345 | }, 346 | { 347 | "inputs": [ 348 | { 349 | "internalType": "uint16", 350 | "name": "_srcChainId", 351 | "type": "uint16" 352 | }, 353 | { 354 | "internalType": "bytes", 355 | "name": "_srcAddress", 356 | "type": "bytes" 357 | }, 358 | { 359 | "internalType": "uint256", 360 | "name": "_nonce", 361 | "type": "uint256" 362 | } 363 | ], 364 | "name": "clearCachedSwap", 365 | "outputs": [], 366 | "stateMutability": "nonpayable", 367 | "type": "function" 368 | }, 369 | { 370 | "inputs": [ 371 | { 372 | "internalType": "uint256", 373 | "name": "_poolId", 374 | "type": "uint256" 375 | }, 376 | { 377 | "internalType": "uint16", 378 | "name": "_dstChainId", 379 | "type": "uint16" 380 | }, 381 | { 382 | "internalType": "uint256", 383 | "name": "_dstPoolId", 384 | "type": "uint256" 385 | }, 386 | { 387 | "internalType": "uint256", 388 | "name": "_weight", 389 | "type": "uint256" 390 | } 391 | ], 392 | "name": "createChainPath", 393 | "outputs": [], 394 | "stateMutability": "nonpayable", 395 | "type": "function" 396 | }, 397 | { 398 | "inputs": [ 399 | { 400 | "internalType": "uint256", 401 | "name": "_poolId", 402 | "type": "uint256" 403 | }, 404 | { 405 | "internalType": "address", 406 | "name": "_token", 407 | "type": "address" 408 | }, 409 | { 410 | "internalType": "uint8", 411 | "name": "_sharedDecimals", 412 | "type": "uint8" 413 | }, 414 | { 415 | "internalType": "uint8", 416 | "name": "_localDecimals", 417 | "type": "uint8" 418 | }, 419 | { 420 | "internalType": "string", 421 | "name": "_name", 422 | "type": "string" 423 | }, 424 | { 425 | "internalType": "string", 426 | "name": "_symbol", 427 | "type": "string" 428 | } 429 | ], 430 | "name": "createPool", 431 | "outputs": [ 432 | { 433 | "internalType": "address", 434 | "name": "", 435 | "type": "address" 436 | } 437 | ], 438 | "stateMutability": "nonpayable", 439 | "type": "function" 440 | }, 441 | { 442 | "inputs": [ 443 | { 444 | "internalType": "uint16", 445 | "name": "_dstChainId", 446 | "type": "uint16" 447 | }, 448 | { 449 | "internalType": "uint256", 450 | "name": "_dstPoolId", 451 | "type": "uint256" 452 | }, 453 | { 454 | "internalType": "uint256", 455 | "name": "_srcPoolId", 456 | "type": "uint256" 457 | }, 458 | { 459 | "components": [ 460 | { 461 | "internalType": "uint256", 462 | "name": "credits", 463 | "type": "uint256" 464 | }, 465 | { 466 | "internalType": "uint256", 467 | "name": "idealBalance", 468 | "type": "uint256" 469 | } 470 | ], 471 | "internalType": "struct Pool.CreditObj", 472 | "name": "_c", 473 | "type": "tuple" 474 | } 475 | ], 476 | "name": "creditChainPath", 477 | "outputs": [], 478 | "stateMutability": "nonpayable", 479 | "type": "function" 480 | }, 481 | { 482 | "inputs": [], 483 | "name": "factory", 484 | "outputs": [ 485 | { 486 | "internalType": "contract Factory", 487 | "name": "", 488 | "type": "address" 489 | } 490 | ], 491 | "stateMutability": "view", 492 | "type": "function" 493 | }, 494 | { 495 | "inputs": [ 496 | { 497 | "internalType": "uint16", 498 | "name": "_srcPoolId", 499 | "type": "uint16" 500 | }, 501 | { 502 | "internalType": "uint256", 503 | "name": "_amountLP", 504 | "type": "uint256" 505 | }, 506 | { 507 | "internalType": "address", 508 | "name": "_to", 509 | "type": "address" 510 | } 511 | ], 512 | "name": "instantRedeemLocal", 513 | "outputs": [ 514 | { 515 | "internalType": "uint256", 516 | "name": "amountSD", 517 | "type": "uint256" 518 | } 519 | ], 520 | "stateMutability": "nonpayable", 521 | "type": "function" 522 | }, 523 | { 524 | "inputs": [], 525 | "name": "mintFeeOwner", 526 | "outputs": [ 527 | { 528 | "internalType": "address", 529 | "name": "", 530 | "type": "address" 531 | } 532 | ], 533 | "stateMutability": "view", 534 | "type": "function" 535 | }, 536 | { 537 | "inputs": [], 538 | "name": "owner", 539 | "outputs": [ 540 | { 541 | "internalType": "address", 542 | "name": "", 543 | "type": "address" 544 | } 545 | ], 546 | "stateMutability": "view", 547 | "type": "function" 548 | }, 549 | { 550 | "inputs": [], 551 | "name": "protocolFeeOwner", 552 | "outputs": [ 553 | { 554 | "internalType": "address", 555 | "name": "", 556 | "type": "address" 557 | } 558 | ], 559 | "stateMutability": "view", 560 | "type": "function" 561 | }, 562 | { 563 | "inputs": [ 564 | { 565 | "internalType": "uint16", 566 | "name": "_dstChainId", 567 | "type": "uint16" 568 | }, 569 | { 570 | "internalType": "uint8", 571 | "name": "_functionType", 572 | "type": "uint8" 573 | }, 574 | { 575 | "internalType": "bytes", 576 | "name": "_toAddress", 577 | "type": "bytes" 578 | }, 579 | { 580 | "internalType": "bytes", 581 | "name": "_transferAndCallPayload", 582 | "type": "bytes" 583 | }, 584 | { 585 | "components": [ 586 | { 587 | "internalType": "uint256", 588 | "name": "dstGasForCall", 589 | "type": "uint256" 590 | }, 591 | { 592 | "internalType": "uint256", 593 | "name": "dstNativeAmount", 594 | "type": "uint256" 595 | }, 596 | { 597 | "internalType": "bytes", 598 | "name": "dstNativeAddr", 599 | "type": "bytes" 600 | } 601 | ], 602 | "internalType": "struct IStargateRouter.lzTxObj", 603 | "name": "_lzTxParams", 604 | "type": "tuple" 605 | } 606 | ], 607 | "name": "quoteLayerZeroFee", 608 | "outputs": [ 609 | { 610 | "internalType": "uint256", 611 | "name": "", 612 | "type": "uint256" 613 | }, 614 | { 615 | "internalType": "uint256", 616 | "name": "", 617 | "type": "uint256" 618 | } 619 | ], 620 | "stateMutability": "view", 621 | "type": "function" 622 | }, 623 | { 624 | "inputs": [ 625 | { 626 | "internalType": "uint16", 627 | "name": "_dstChainId", 628 | "type": "uint16" 629 | }, 630 | { 631 | "internalType": "uint256", 632 | "name": "_srcPoolId", 633 | "type": "uint256" 634 | }, 635 | { 636 | "internalType": "uint256", 637 | "name": "_dstPoolId", 638 | "type": "uint256" 639 | }, 640 | { 641 | "internalType": "address payable", 642 | "name": "_refundAddress", 643 | "type": "address" 644 | }, 645 | { 646 | "internalType": "uint256", 647 | "name": "_amountLP", 648 | "type": "uint256" 649 | }, 650 | { 651 | "internalType": "bytes", 652 | "name": "_to", 653 | "type": "bytes" 654 | }, 655 | { 656 | "components": [ 657 | { 658 | "internalType": "uint256", 659 | "name": "dstGasForCall", 660 | "type": "uint256" 661 | }, 662 | { 663 | "internalType": "uint256", 664 | "name": "dstNativeAmount", 665 | "type": "uint256" 666 | }, 667 | { 668 | "internalType": "bytes", 669 | "name": "dstNativeAddr", 670 | "type": "bytes" 671 | } 672 | ], 673 | "internalType": "struct IStargateRouter.lzTxObj", 674 | "name": "_lzTxParams", 675 | "type": "tuple" 676 | } 677 | ], 678 | "name": "redeemLocal", 679 | "outputs": [], 680 | "stateMutability": "payable", 681 | "type": "function" 682 | }, 683 | { 684 | "inputs": [ 685 | { 686 | "internalType": "uint16", 687 | "name": "_srcChainId", 688 | "type": "uint16" 689 | }, 690 | { 691 | "internalType": "bytes", 692 | "name": "_srcAddress", 693 | "type": "bytes" 694 | }, 695 | { 696 | "internalType": "uint256", 697 | "name": "_nonce", 698 | "type": "uint256" 699 | }, 700 | { 701 | "internalType": "uint256", 702 | "name": "_srcPoolId", 703 | "type": "uint256" 704 | }, 705 | { 706 | "internalType": "uint256", 707 | "name": "_dstPoolId", 708 | "type": "uint256" 709 | }, 710 | { 711 | "internalType": "address", 712 | "name": "_to", 713 | "type": "address" 714 | }, 715 | { 716 | "internalType": "uint256", 717 | "name": "_amountSD", 718 | "type": "uint256" 719 | }, 720 | { 721 | "internalType": "uint256", 722 | "name": "_mintAmountSD", 723 | "type": "uint256" 724 | } 725 | ], 726 | "name": "redeemLocalCallback", 727 | "outputs": [], 728 | "stateMutability": "nonpayable", 729 | "type": "function" 730 | }, 731 | { 732 | "inputs": [ 733 | { 734 | "internalType": "uint16", 735 | "name": "_srcChainId", 736 | "type": "uint16" 737 | }, 738 | { 739 | "internalType": "bytes", 740 | "name": "_srcAddress", 741 | "type": "bytes" 742 | }, 743 | { 744 | "internalType": "uint256", 745 | "name": "_nonce", 746 | "type": "uint256" 747 | }, 748 | { 749 | "internalType": "uint256", 750 | "name": "_srcPoolId", 751 | "type": "uint256" 752 | }, 753 | { 754 | "internalType": "uint256", 755 | "name": "_dstPoolId", 756 | "type": "uint256" 757 | }, 758 | { 759 | "internalType": "uint256", 760 | "name": "_amountSD", 761 | "type": "uint256" 762 | }, 763 | { 764 | "internalType": "bytes", 765 | "name": "_to", 766 | "type": "bytes" 767 | } 768 | ], 769 | "name": "redeemLocalCheckOnRemote", 770 | "outputs": [], 771 | "stateMutability": "nonpayable", 772 | "type": "function" 773 | }, 774 | { 775 | "inputs": [ 776 | { 777 | "internalType": "uint16", 778 | "name": "_dstChainId", 779 | "type": "uint16" 780 | }, 781 | { 782 | "internalType": "uint256", 783 | "name": "_srcPoolId", 784 | "type": "uint256" 785 | }, 786 | { 787 | "internalType": "uint256", 788 | "name": "_dstPoolId", 789 | "type": "uint256" 790 | }, 791 | { 792 | "internalType": "address payable", 793 | "name": "_refundAddress", 794 | "type": "address" 795 | }, 796 | { 797 | "internalType": "uint256", 798 | "name": "_amountLP", 799 | "type": "uint256" 800 | }, 801 | { 802 | "internalType": "uint256", 803 | "name": "_minAmountLD", 804 | "type": "uint256" 805 | }, 806 | { 807 | "internalType": "bytes", 808 | "name": "_to", 809 | "type": "bytes" 810 | }, 811 | { 812 | "components": [ 813 | { 814 | "internalType": "uint256", 815 | "name": "dstGasForCall", 816 | "type": "uint256" 817 | }, 818 | { 819 | "internalType": "uint256", 820 | "name": "dstNativeAmount", 821 | "type": "uint256" 822 | }, 823 | { 824 | "internalType": "bytes", 825 | "name": "dstNativeAddr", 826 | "type": "bytes" 827 | } 828 | ], 829 | "internalType": "struct IStargateRouter.lzTxObj", 830 | "name": "_lzTxParams", 831 | "type": "tuple" 832 | } 833 | ], 834 | "name": "redeemRemote", 835 | "outputs": [], 836 | "stateMutability": "payable", 837 | "type": "function" 838 | }, 839 | { 840 | "inputs": [], 841 | "name": "renounceOwnership", 842 | "outputs": [], 843 | "stateMutability": "nonpayable", 844 | "type": "function" 845 | }, 846 | { 847 | "inputs": [ 848 | { 849 | "internalType": "uint16", 850 | "name": "_srcChainId", 851 | "type": "uint16" 852 | }, 853 | { 854 | "internalType": "bytes", 855 | "name": "_srcAddress", 856 | "type": "bytes" 857 | }, 858 | { 859 | "internalType": "uint256", 860 | "name": "_nonce", 861 | "type": "uint256" 862 | } 863 | ], 864 | "name": "retryRevert", 865 | "outputs": [], 866 | "stateMutability": "payable", 867 | "type": "function" 868 | }, 869 | { 870 | "inputs": [ 871 | { 872 | "internalType": "uint16", 873 | "name": "", 874 | "type": "uint16" 875 | }, 876 | { 877 | "internalType": "bytes", 878 | "name": "", 879 | "type": "bytes" 880 | }, 881 | { 882 | "internalType": "uint256", 883 | "name": "", 884 | "type": "uint256" 885 | } 886 | ], 887 | "name": "revertLookup", 888 | "outputs": [ 889 | { 890 | "internalType": "bytes", 891 | "name": "", 892 | "type": "bytes" 893 | } 894 | ], 895 | "stateMutability": "view", 896 | "type": "function" 897 | }, 898 | { 899 | "inputs": [ 900 | { 901 | "internalType": "uint16", 902 | "name": "_dstChainId", 903 | "type": "uint16" 904 | }, 905 | { 906 | "internalType": "bytes", 907 | "name": "_srcAddress", 908 | "type": "bytes" 909 | }, 910 | { 911 | "internalType": "uint256", 912 | "name": "_nonce", 913 | "type": "uint256" 914 | }, 915 | { 916 | "internalType": "address payable", 917 | "name": "_refundAddress", 918 | "type": "address" 919 | }, 920 | { 921 | "components": [ 922 | { 923 | "internalType": "uint256", 924 | "name": "dstGasForCall", 925 | "type": "uint256" 926 | }, 927 | { 928 | "internalType": "uint256", 929 | "name": "dstNativeAmount", 930 | "type": "uint256" 931 | }, 932 | { 933 | "internalType": "bytes", 934 | "name": "dstNativeAddr", 935 | "type": "bytes" 936 | } 937 | ], 938 | "internalType": "struct IStargateRouter.lzTxObj", 939 | "name": "_lzTxParams", 940 | "type": "tuple" 941 | } 942 | ], 943 | "name": "revertRedeemLocal", 944 | "outputs": [], 945 | "stateMutability": "payable", 946 | "type": "function" 947 | }, 948 | { 949 | "inputs": [ 950 | { 951 | "internalType": "uint16", 952 | "name": "_dstChainId", 953 | "type": "uint16" 954 | }, 955 | { 956 | "internalType": "uint256", 957 | "name": "_srcPoolId", 958 | "type": "uint256" 959 | }, 960 | { 961 | "internalType": "uint256", 962 | "name": "_dstPoolId", 963 | "type": "uint256" 964 | }, 965 | { 966 | "internalType": "address payable", 967 | "name": "_refundAddress", 968 | "type": "address" 969 | } 970 | ], 971 | "name": "sendCredits", 972 | "outputs": [], 973 | "stateMutability": "payable", 974 | "type": "function" 975 | }, 976 | { 977 | "inputs": [ 978 | { 979 | "internalType": "contract Bridge", 980 | "name": "_bridge", 981 | "type": "address" 982 | }, 983 | { 984 | "internalType": "contract Factory", 985 | "name": "_factory", 986 | "type": "address" 987 | } 988 | ], 989 | "name": "setBridgeAndFactory", 990 | "outputs": [], 991 | "stateMutability": "nonpayable", 992 | "type": "function" 993 | }, 994 | { 995 | "inputs": [ 996 | { 997 | "internalType": "uint256", 998 | "name": "_poolId", 999 | "type": "uint256" 1000 | }, 1001 | { 1002 | "internalType": "bool", 1003 | "name": "_batched", 1004 | "type": "bool" 1005 | }, 1006 | { 1007 | "internalType": "uint256", 1008 | "name": "_swapDeltaBP", 1009 | "type": "uint256" 1010 | }, 1011 | { 1012 | "internalType": "uint256", 1013 | "name": "_lpDeltaBP", 1014 | "type": "uint256" 1015 | }, 1016 | { 1017 | "internalType": "bool", 1018 | "name": "_defaultSwapMode", 1019 | "type": "bool" 1020 | }, 1021 | { 1022 | "internalType": "bool", 1023 | "name": "_defaultLPMode", 1024 | "type": "bool" 1025 | } 1026 | ], 1027 | "name": "setDeltaParam", 1028 | "outputs": [], 1029 | "stateMutability": "nonpayable", 1030 | "type": "function" 1031 | }, 1032 | { 1033 | "inputs": [ 1034 | { 1035 | "internalType": "uint256", 1036 | "name": "_poolId", 1037 | "type": "uint256" 1038 | }, 1039 | { 1040 | "internalType": "address", 1041 | "name": "_feeLibraryAddr", 1042 | "type": "address" 1043 | } 1044 | ], 1045 | "name": "setFeeLibrary", 1046 | "outputs": [], 1047 | "stateMutability": "nonpayable", 1048 | "type": "function" 1049 | }, 1050 | { 1051 | "inputs": [ 1052 | { 1053 | "internalType": "uint256", 1054 | "name": "_poolId", 1055 | "type": "uint256" 1056 | }, 1057 | { 1058 | "internalType": "uint256", 1059 | "name": "_mintFeeBP", 1060 | "type": "uint256" 1061 | } 1062 | ], 1063 | "name": "setFees", 1064 | "outputs": [], 1065 | "stateMutability": "nonpayable", 1066 | "type": "function" 1067 | }, 1068 | { 1069 | "inputs": [ 1070 | { 1071 | "internalType": "address", 1072 | "name": "_owner", 1073 | "type": "address" 1074 | } 1075 | ], 1076 | "name": "setMintFeeOwner", 1077 | "outputs": [], 1078 | "stateMutability": "nonpayable", 1079 | "type": "function" 1080 | }, 1081 | { 1082 | "inputs": [ 1083 | { 1084 | "internalType": "address", 1085 | "name": "_owner", 1086 | "type": "address" 1087 | } 1088 | ], 1089 | "name": "setProtocolFeeOwner", 1090 | "outputs": [], 1091 | "stateMutability": "nonpayable", 1092 | "type": "function" 1093 | }, 1094 | { 1095 | "inputs": [ 1096 | { 1097 | "internalType": "uint256", 1098 | "name": "_poolId", 1099 | "type": "uint256" 1100 | }, 1101 | { 1102 | "internalType": "bool", 1103 | "name": "_swapStop", 1104 | "type": "bool" 1105 | } 1106 | ], 1107 | "name": "setSwapStop", 1108 | "outputs": [], 1109 | "stateMutability": "nonpayable", 1110 | "type": "function" 1111 | }, 1112 | { 1113 | "inputs": [ 1114 | { 1115 | "internalType": "uint256", 1116 | "name": "_poolId", 1117 | "type": "uint256" 1118 | }, 1119 | { 1120 | "internalType": "uint16", 1121 | "name": "_dstChainId", 1122 | "type": "uint16" 1123 | }, 1124 | { 1125 | "internalType": "uint256", 1126 | "name": "_dstPoolId", 1127 | "type": "uint256" 1128 | }, 1129 | { 1130 | "internalType": "uint16", 1131 | "name": "_weight", 1132 | "type": "uint16" 1133 | } 1134 | ], 1135 | "name": "setWeightForChainPath", 1136 | "outputs": [], 1137 | "stateMutability": "nonpayable", 1138 | "type": "function" 1139 | }, 1140 | { 1141 | "inputs": [ 1142 | { 1143 | "internalType": "uint16", 1144 | "name": "_dstChainId", 1145 | "type": "uint16" 1146 | }, 1147 | { 1148 | "internalType": "uint256", 1149 | "name": "_srcPoolId", 1150 | "type": "uint256" 1151 | }, 1152 | { 1153 | "internalType": "uint256", 1154 | "name": "_dstPoolId", 1155 | "type": "uint256" 1156 | }, 1157 | { 1158 | "internalType": "address payable", 1159 | "name": "_refundAddress", 1160 | "type": "address" 1161 | }, 1162 | { 1163 | "internalType": "uint256", 1164 | "name": "_amountLD", 1165 | "type": "uint256" 1166 | }, 1167 | { 1168 | "internalType": "uint256", 1169 | "name": "_minAmountLD", 1170 | "type": "uint256" 1171 | }, 1172 | { 1173 | "components": [ 1174 | { 1175 | "internalType": "uint256", 1176 | "name": "dstGasForCall", 1177 | "type": "uint256" 1178 | }, 1179 | { 1180 | "internalType": "uint256", 1181 | "name": "dstNativeAmount", 1182 | "type": "uint256" 1183 | }, 1184 | { 1185 | "internalType": "bytes", 1186 | "name": "dstNativeAddr", 1187 | "type": "bytes" 1188 | } 1189 | ], 1190 | "internalType": "struct IStargateRouter.lzTxObj", 1191 | "name": "_lzTxParams", 1192 | "type": "tuple" 1193 | }, 1194 | { 1195 | "internalType": "bytes", 1196 | "name": "_to", 1197 | "type": "bytes" 1198 | }, 1199 | { 1200 | "internalType": "bytes", 1201 | "name": "_payload", 1202 | "type": "bytes" 1203 | } 1204 | ], 1205 | "name": "swap", 1206 | "outputs": [], 1207 | "stateMutability": "payable", 1208 | "type": "function" 1209 | }, 1210 | { 1211 | "inputs": [ 1212 | { 1213 | "internalType": "uint16", 1214 | "name": "_srcChainId", 1215 | "type": "uint16" 1216 | }, 1217 | { 1218 | "internalType": "bytes", 1219 | "name": "_srcAddress", 1220 | "type": "bytes" 1221 | }, 1222 | { 1223 | "internalType": "uint256", 1224 | "name": "_nonce", 1225 | "type": "uint256" 1226 | }, 1227 | { 1228 | "internalType": "uint256", 1229 | "name": "_srcPoolId", 1230 | "type": "uint256" 1231 | }, 1232 | { 1233 | "internalType": "uint256", 1234 | "name": "_dstPoolId", 1235 | "type": "uint256" 1236 | }, 1237 | { 1238 | "internalType": "uint256", 1239 | "name": "_dstGasForCall", 1240 | "type": "uint256" 1241 | }, 1242 | { 1243 | "internalType": "address", 1244 | "name": "_to", 1245 | "type": "address" 1246 | }, 1247 | { 1248 | "components": [ 1249 | { 1250 | "internalType": "uint256", 1251 | "name": "amount", 1252 | "type": "uint256" 1253 | }, 1254 | { 1255 | "internalType": "uint256", 1256 | "name": "eqFee", 1257 | "type": "uint256" 1258 | }, 1259 | { 1260 | "internalType": "uint256", 1261 | "name": "eqReward", 1262 | "type": "uint256" 1263 | }, 1264 | { 1265 | "internalType": "uint256", 1266 | "name": "lpFee", 1267 | "type": "uint256" 1268 | }, 1269 | { 1270 | "internalType": "uint256", 1271 | "name": "protocolFee", 1272 | "type": "uint256" 1273 | }, 1274 | { 1275 | "internalType": "uint256", 1276 | "name": "lkbRemove", 1277 | "type": "uint256" 1278 | } 1279 | ], 1280 | "internalType": "struct Pool.SwapObj", 1281 | "name": "_s", 1282 | "type": "tuple" 1283 | }, 1284 | { 1285 | "internalType": "bytes", 1286 | "name": "_payload", 1287 | "type": "bytes" 1288 | } 1289 | ], 1290 | "name": "swapRemote", 1291 | "outputs": [], 1292 | "stateMutability": "nonpayable", 1293 | "type": "function" 1294 | }, 1295 | { 1296 | "inputs": [ 1297 | { 1298 | "internalType": "address", 1299 | "name": "newOwner", 1300 | "type": "address" 1301 | } 1302 | ], 1303 | "name": "transferOwnership", 1304 | "outputs": [], 1305 | "stateMutability": "nonpayable", 1306 | "type": "function" 1307 | }, 1308 | { 1309 | "inputs": [ 1310 | { 1311 | "internalType": "uint256", 1312 | "name": "_poolId", 1313 | "type": "uint256" 1314 | }, 1315 | { 1316 | "internalType": "address", 1317 | "name": "_to", 1318 | "type": "address" 1319 | } 1320 | ], 1321 | "name": "withdrawMintFee", 1322 | "outputs": [], 1323 | "stateMutability": "nonpayable", 1324 | "type": "function" 1325 | }, 1326 | { 1327 | "inputs": [ 1328 | { 1329 | "internalType": "uint256", 1330 | "name": "_poolId", 1331 | "type": "uint256" 1332 | }, 1333 | { 1334 | "internalType": "address", 1335 | "name": "_to", 1336 | "type": "address" 1337 | } 1338 | ], 1339 | "name": "withdrawProtocolFee", 1340 | "outputs": [], 1341 | "stateMutability": "nonpayable", 1342 | "type": "function" 1343 | } 1344 | ]''') 1345 | -------------------------------------------------------------------------------- /base/errors.py: -------------------------------------------------------------------------------- 1 | class BaseError(Exception): 2 | pass 3 | 4 | 5 | class NotSupported(BaseError): 6 | pass 7 | 8 | 9 | class NotEnoughNativeTokenBalance(BaseError): 10 | pass 11 | 12 | 13 | class NotEnoughStablecoinBalance(BaseError): 14 | pass 15 | 16 | 17 | class StablecoinNotSupportedByChain(BaseError): 18 | pass 19 | 20 | 21 | class ConfigurationError(BaseError): 22 | pass 23 | 24 | 25 | # -------- Blockchain error -------- 26 | 27 | class BlockchainError(BaseError): 28 | pass 29 | 30 | 31 | class TransactionNotFound(BlockchainError): 32 | pass 33 | 34 | 35 | class TransactionFailed(BlockchainError): 36 | pass 37 | 38 | 39 | # -------- Exchange error -------- 40 | 41 | class ExchangeError(BaseError): 42 | pass 43 | 44 | 45 | class NotWhitelistedAddress(ExchangeError): 46 | pass 47 | 48 | 49 | class WithdrawCanceled(ExchangeError): 50 | pass 51 | 52 | 53 | class WithdrawTimeout(ExchangeError): 54 | pass 55 | 56 | 57 | class WithdrawNotFound(ExchangeError): 58 | pass 59 | -------------------------------------------------------------------------------- /btcb/__init__.py: -------------------------------------------------------------------------------- 1 | from btcb.btcb import BTCbUtils, BTCbBridgeHelper 2 | from btcb.constants import BTCbConstants 3 | -------------------------------------------------------------------------------- /btcb/btcb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import time 4 | 5 | from eth_account.signers.local import LocalAccount 6 | from web3.types import TxParams 7 | 8 | from abi import BTCB_ABI 9 | from network import EVMNetwork, Optimism, Avalanche 10 | from btcb.constants import BTCbConstants 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class BTCbUtils: 16 | @staticmethod 17 | def get_adapter_params(network: EVMNetwork, address: str) -> str: 18 | if isinstance(network, Optimism): 19 | return f"0x00020000000000000000000000000000000000000000000000000000000000" \ 20 | f"2dc6c00000000000000000000000000000000000000000000000000000000000000000" \ 21 | f"{address[2:]}" 22 | else: 23 | return f"0x000200000000000000000000000000000000000000000000000000000000000" \ 24 | f"3d0900000000000000000000000000000000000000000000000000000000000000000" \ 25 | f"{address[2:]}" 26 | 27 | @staticmethod 28 | def estimate_layerzero_bridge_fee(src_network: EVMNetwork, dst_network: EVMNetwork, dst_address: str) -> int: 29 | btcb_contract = src_network.w3.eth.contract(address=BTCbConstants.BTCB_CONTRACT_ADDRESS, abi=BTCB_ABI) 30 | 31 | to_address = '0x000000000000000000000000' + dst_address[2:] 32 | amount = 100 # Doesn't matter in fee calculation 33 | 34 | quote_data = btcb_contract.functions.estimateSendFee(dst_network.layerzero_chain_id, to_address, 35 | amount, False, 36 | BTCbUtils.get_adapter_params(src_network, dst_address) 37 | ).call() 38 | return quote_data[0] 39 | 40 | @staticmethod 41 | def _get_optimism_bridge_l1_fee(optimism: Optimism, dst_network: EVMNetwork, address: str) -> int: 42 | # Doesn't matter in fee calculation 43 | amount = 100 44 | 45 | bridge_tx = BTCbUtils.build_bridge_transaction(optimism, dst_network, amount, address) 46 | bridge_l1_fee = optimism.get_l1_fee(bridge_tx) 47 | 48 | return bridge_l1_fee 49 | 50 | @staticmethod 51 | def estimate_bridge_gas_price(src_network: EVMNetwork, dst_network: EVMNetwork, address: str) -> int: 52 | max_overall_gas_limit = BTCbConstants.get_max_randomized_bridge_gas_limit(src_network.name) 53 | 54 | # Avalanche network needs BTC.b approval before bridging 55 | if isinstance(src_network, Avalanche): 56 | max_overall_gas_limit += src_network.get_approve_gas_limit() 57 | gas_price = max_overall_gas_limit * src_network.get_max_fee_per_gas() 58 | 59 | # Optimism fee should be calculated in a different way. 60 | # Read more: https://community.optimism.io/docs/developers/build/transaction-fees/# 61 | if isinstance(src_network, Optimism): 62 | gas_price += BTCbUtils._get_optimism_bridge_l1_fee(src_network, dst_network, address) 63 | 64 | return gas_price 65 | 66 | @staticmethod 67 | def is_enough_native_balance_for_bridge_fee(src_network: EVMNetwork, dst_network: EVMNetwork, address: str): 68 | account_balance = src_network.get_balance(address) 69 | gas_price = BTCbUtils.estimate_bridge_gas_price(src_network, dst_network, address) 70 | layerzero_fee = BTCbUtils.estimate_layerzero_bridge_fee(src_network, dst_network, address) 71 | 72 | enough_native_token_balance = account_balance > (gas_price + layerzero_fee) 73 | 74 | return enough_native_token_balance 75 | 76 | @staticmethod 77 | def get_btcb_balance(network: EVMNetwork, address: str) -> int: 78 | if isinstance(network, Avalanche): 79 | return network.get_token_balance(BTCbConstants.BTCB_BASE_AVALANCHE_CONTRACT_ADDRESS, address) 80 | 81 | return network.get_token_balance(BTCbConstants.BTCB_CONTRACT_ADDRESS, address) 82 | 83 | @staticmethod 84 | def build_bridge_transaction(src_network: EVMNetwork, dst_network: EVMNetwork, 85 | amount: int, address: str) -> TxParams: 86 | btcb_contract = src_network.w3.eth.contract(address=BTCbConstants.BTCB_CONTRACT_ADDRESS, abi=BTCB_ABI) 87 | 88 | layerzero_fee = BTCbUtils.estimate_layerzero_bridge_fee(src_network, dst_network, address) 89 | 90 | nonce = src_network.get_nonce(address) 91 | gas_params = src_network.get_transaction_gas_params() 92 | logger.info(f'Estimated fees. LayerZero fee: {layerzero_fee}. Gas settings: {gas_params}') 93 | 94 | tx = btcb_contract.functions.sendFrom( 95 | address, # _from 96 | dst_network.layerzero_chain_id, # _dstChainId 97 | f"0x000000000000000000000000{address[2:]}", # _toAddress 98 | amount, # _amount 99 | amount, # _minAmount 100 | [address, # _callParams.refundAddress 101 | "0x0000000000000000000000000000000000000000", # _callParams.zroPaymentAddress 102 | BTCbUtils.get_adapter_params(src_network, address)] # _callParams.adapterParams 103 | ).build_transaction( 104 | { 105 | 'from': address, 106 | 'value': layerzero_fee, 107 | 'gas': BTCbConstants.get_randomized_bridge_gas_limit(src_network.name), 108 | **gas_params, 109 | 'nonce': nonce 110 | } 111 | ) 112 | 113 | return tx 114 | 115 | 116 | class BTCbBridgeHelper: 117 | def __init__(self, account: LocalAccount, src_network: EVMNetwork, dst_network: EVMNetwork, amount: int) -> None: 118 | self.account = account 119 | self.src_network = src_network 120 | self.dst_network = dst_network 121 | self.amount = amount 122 | 123 | def make_bridge(self) -> bool: 124 | if not self._is_bridge_possible(): 125 | return False 126 | 127 | if isinstance(self.src_network, Avalanche): 128 | result = self._approve_btcb_usage(self.amount) 129 | 130 | if not result: 131 | return False 132 | 133 | time.sleep(random.randint(10, 60)) 134 | 135 | tx_hash = self._send_bridge_transaction() 136 | result = self.src_network.wait_for_transaction(tx_hash) 137 | 138 | return self.src_network.check_tx_result(result, "BTC.b bridge") 139 | 140 | def _is_bridge_possible(self) -> bool: 141 | """ Method that checks BTC.b balance on the source chain and decides if it is possible to make bridge """ 142 | 143 | if not BTCbUtils.is_enough_native_balance_for_bridge_fee(self.src_network, self.dst_network, 144 | self.account.address): 145 | logger.error(f"Not enough native token balance on {self.src_network.name} network") 146 | return False 147 | 148 | btcb_balance = BTCbUtils.get_btcb_balance(self.src_network, self.account.address) 149 | if btcb_balance < self.amount: 150 | logger.error(f"Not enough BTC.b balance on {self.src_network.name} network") 151 | return False 152 | 153 | return True 154 | 155 | def _approve_btcb_usage(self, amount: int) -> bool: 156 | if not isinstance(self.src_network, Avalanche): 157 | raise ValueError("BTC.b needs approval only on Avalanche chain") 158 | 159 | allowance = self.src_network.get_token_allowance(BTCbConstants.BTCB_BASE_AVALANCHE_CONTRACT_ADDRESS, 160 | self.account.address, BTCbConstants.BTCB_CONTRACT_ADDRESS) 161 | if allowance >= amount: 162 | return True 163 | 164 | tx_hash = self.src_network.approve_token_usage(self.account.key, 165 | BTCbConstants.BTCB_BASE_AVALANCHE_CONTRACT_ADDRESS, 166 | BTCbConstants.BTCB_CONTRACT_ADDRESS, amount) 167 | result = self.src_network.wait_for_transaction(tx_hash) 168 | 169 | return self.src_network.check_tx_result(result, f"Approve BTC.b usage") 170 | 171 | def _send_bridge_transaction(self): 172 | tx = BTCbUtils.build_bridge_transaction(self.src_network, self.dst_network, self.amount, self.account.address) 173 | 174 | signed_tx = self.src_network.w3.eth.account.sign_transaction(tx, self.account.key) 175 | tx_hash = self.src_network.w3.eth.send_raw_transaction(signed_tx.rawTransaction) 176 | 177 | logger.info(f'BTC.b bridge transaction signed and sent. Hash: {tx_hash.hex()}') 178 | 179 | return tx_hash 180 | -------------------------------------------------------------------------------- /btcb/constants.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from base.errors import NotSupported 4 | 5 | 6 | class BTCbConstants: 7 | BTCB_CONTRACT_ADDRESS = "0x2297aEbD383787A160DD0d9F71508148769342E3" 8 | BTCB_BASE_AVALANCHE_CONTRACT_ADDRESS = "0x152b9d0FdC40C096757F570A51E494bd4b943E50" 9 | BTCB_DECIMALS = 8 10 | 11 | BRIDGE_GAS_LIMIT = { 12 | 'Ethereum': (300_000, 400_000), 13 | 'Arbitrum': (2_000_000, 3_000_000), 14 | 'Optimism': (300_000, 400_000), 15 | 'Polygon': (300_000, 400_000), 16 | 'BSC': (250_000, 300_000), 17 | 'Avalanche': (300_000, 350_000) 18 | } 19 | 20 | @staticmethod 21 | def get_max_randomized_bridge_gas_limit(network_name: str) -> int: 22 | return BTCbConstants.BRIDGE_GAS_LIMIT[network_name][1] 23 | 24 | @staticmethod 25 | def get_randomized_bridge_gas_limit(network_name: str) -> int: 26 | if network_name not in BTCbConstants.BRIDGE_GAS_LIMIT: 27 | raise NotSupported(f"{network_name} isn't supported by get_randomized_bridge_gas_limit()") 28 | 29 | return random.randint(BTCbConstants.BRIDGE_GAS_LIMIT[network_name][0], 30 | BTCbConstants.BRIDGE_GAS_LIMIT[network_name][1]) 31 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | 4 | from base.errors import ConfigurationError 5 | from network import Ethereum, Polygon, Fantom, Avalanche, Arbitrum, BSC, Optimism 6 | 7 | SUPPORTED_NETWORKS_STARGATE = [ 8 | # Ethereum(), # High gas price 9 | Polygon(), 10 | # Fantom(), # Liquidity problems 11 | Avalanche(), 12 | Arbitrum(), 13 | BSC(), 14 | Optimism() 15 | ] 16 | 17 | SUPPORTED_NETWORKS_BTCB = [ 18 | # Ethereum(), # High gas price 19 | Polygon(), 20 | Avalanche(), 21 | Arbitrum(), 22 | BSC(), 23 | Optimism() 24 | ] 25 | 26 | DEFAULT_PRIVATE_KEYS_FILE_PATH = os.getenv("DEFAULT_PRIVATE_KEYS_FILE_PATH") 27 | 28 | 29 | class BridgerMode(Enum): 30 | STARGATE = "stargate" 31 | BTCB = "btcb" 32 | TESTNET = "testnet" 33 | 34 | 35 | class RefuelMode(Enum): 36 | MANUAL = "manual" # Manual refuel 37 | OKEX = "okex" # Automatic refuel from the Okex exchange 38 | BINANCE = "binance" # Automatic refuel from the Binance exchange 39 | 40 | 41 | # Utility class 42 | class TimeRanges: 43 | MINUTE = 60 44 | HOUR = 3600 45 | 46 | 47 | # Randomization ranges (seconds). The ranges shown are just examples of values that can easily be changed 48 | class SleepTimings: 49 | AFTER_START_RANGE = (0, TimeRanges.MINUTE * 10) # from 0 seconds to 10 minutes. Sleep after start 50 | BEFORE_BRIDGE_RANGE = (30, TimeRanges.HOUR) # from 30 seconds to 1 hour. Sleep before bridge 51 | BALANCE_RECHECK_TIME = TimeRanges.MINUTE * 2 # 2 minutes. Recheck time for stablecoin or native token deposit 52 | BEFORE_WITHDRAW_RANGE = (30, TimeRanges.HOUR) # from 30 seconds to 30 minutes. Sleep before withdraw from exchange 53 | 54 | 55 | # -------- Utility class -------- 56 | class ConfigurationHelper: 57 | @staticmethod 58 | def check_networks_list() -> None: 59 | if len(SUPPORTED_NETWORKS_STARGATE) == 0: 60 | raise ConfigurationError('Supported network list is empty. Unable to run with such a configuration') 61 | elif len(SUPPORTED_NETWORKS_STARGATE) == 1: 62 | raise ConfigurationError('Only one supported network is provided. Unable to run with such a configuration') 63 | 64 | @staticmethod 65 | def check_stargate_slippage() -> None: 66 | if float(os.getenv('STARGATE_SLIPPAGE')) < 0.001: 67 | raise ConfigurationError("Slippage can't be lower than 0.01%. Check configuration settings") 68 | if float(os.getenv('STARGATE_SLIPPAGE')) > 0.2: 69 | raise ConfigurationError("Slippage is too high. It's more than 20%. Check configuration settings") 70 | 71 | @staticmethod 72 | def check_min_stablecoin_balance() -> None: 73 | if float(os.getenv('STARGATE_MIN_STABLECOIN_BALANCE')) < 0: 74 | raise ConfigurationError("Incorrect minimum stablecoin balance. It can't be lower than zero. " 75 | "Check configuration settings") 76 | 77 | @staticmethod 78 | def create_logging_directory() -> None: 79 | log_dir = 'logs' 80 | 81 | if not os.path.exists(log_dir): 82 | os.makedirs(log_dir) 83 | 84 | @staticmethod 85 | def check_configuration() -> None: 86 | ConfigurationHelper.check_networks_list() 87 | ConfigurationHelper.check_stargate_slippage() 88 | ConfigurationHelper.check_min_stablecoin_balance() 89 | 90 | ConfigurationHelper.create_logging_directory() 91 | -------------------------------------------------------------------------------- /exchange/__init__.py: -------------------------------------------------------------------------------- 1 | from exchange.factory import ExchangeFactory 2 | 3 | from exchange.binance.binance import Binance 4 | from exchange.okex.okex import Okex 5 | -------------------------------------------------------------------------------- /exchange/binance/binance.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | from typing import List 4 | 5 | import ccxt 6 | 7 | from base.errors import ExchangeError, NotWhitelistedAddress, WithdrawNotFound 8 | from exchange.binance.constants import BinanceConstants 9 | from exchange.exchange import Exchange, WithdrawInfo, WithdrawStatus 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Binance(Exchange): 15 | def __init__(self, api_key: str, secret_key: str) -> None: 16 | ccxt_args = { 17 | 'options': { 18 | 'defaultType': 'spot' 19 | } 20 | } 21 | super().__init__('binance', api_key, secret_key, ccxt_args) 22 | 23 | def withdraw(self, symbol: str, amount: float, network: str, address: str) -> str: 24 | """ Method that initiates the withdrawal and returns the withdrawal id """ 25 | 26 | logger.info(f'{symbol} withdraw initiated. Amount: {amount}. Network: {network}. Address: {address}') 27 | withdraw_info = self.get_withdraw_info(symbol, network) 28 | 29 | decimals = self._get_precision(symbol) 30 | amount = round(amount, decimals) 31 | logger.debug(f'Amount rounded to {amount}') 32 | 33 | try: 34 | result = self._ccxt_exc.withdraw(symbol, amount, address, tag=None, params={"network": withdraw_info.chain}) 35 | except Exception as ex: 36 | if 'Withdrawal address is not whitelisted for verification exemption' in str(ex): 37 | raise NotWhitelistedAddress(f'Unable to withdraw {symbol}({network}) to {address}. ' 38 | f'The address must be added to the whitelist') from ex 39 | raise 40 | 41 | logger.debug(f'Withdraw result: {result}') 42 | withdraw_id = result['id'] 43 | 44 | return str(withdraw_id) 45 | 46 | def _get_withdraw_infos(self, symbol: str) -> List[WithdrawInfo]: 47 | currencies = self._ccxt_exc.fetch_currencies() 48 | chains_info = currencies[symbol]['networks'] 49 | 50 | result = [] 51 | 52 | for chain_info in chains_info: 53 | info = WithdrawInfo(symbol, chain_info['network'], float(chain_info['withdrawFee']), 54 | float(chain_info['withdrawMin'])) 55 | result.append(info) 56 | 57 | return result 58 | 59 | def is_withdraw_supported(self, symbol: str, network: str) -> bool: 60 | if symbol in BinanceConstants.TOKENS and network in BinanceConstants.TOKENS[symbol]: 61 | return True 62 | return False 63 | 64 | def get_withdraw_info(self, symbol: str, network: str) -> WithdrawInfo: 65 | binance_network = BinanceConstants.NETWORKS[network] 66 | withdraw_options = self._get_withdraw_infos(symbol) 67 | 68 | withdraw_info = next((option for option in withdraw_options if option.chain == binance_network), None) 69 | 70 | if not withdraw_info: 71 | raise ExchangeError(f"OKEX doesn't support {symbol}({network}) withdrawals") 72 | 73 | return withdraw_info 74 | 75 | def get_withdraw_status(self, withdrawal_id: str) -> WithdrawStatus: 76 | withdrawals_info = self._ccxt_exc.fetch_withdrawals() 77 | withdrawal_info = next((withdrawal for withdrawal in withdrawals_info if withdrawal['id'] == withdrawal_id), 78 | None) 79 | 80 | if not withdrawal_info: 81 | raise WithdrawNotFound(f"Withdraw {withdrawal_id} can't be found on Binance") 82 | 83 | return self._parse_withdraw_status(withdrawal_info) 84 | 85 | def _get_min_notional(self, symbol: str) -> float: 86 | trading_symbol = symbol + '/USDT' 87 | markets = self._ccxt_exc.load_markets() 88 | 89 | market = markets[trading_symbol] 90 | minimal_notional = market['info']['filters'][6]['minNotional'] 91 | 92 | return float(minimal_notional) 93 | 94 | def _get_precision(self, symbol: str) -> int: 95 | currencies = self._ccxt_exc.fetch_currencies() 96 | currency_info = currencies[symbol] 97 | decimals = int(currency_info['precision']) 98 | 99 | return decimals 100 | 101 | def buy_tokens_with_usdt(self, symbol: str, amount: float) -> float: 102 | logger.info(f'{symbol} purchase initiated. Amount: {amount}') 103 | trading_symbol = symbol + '/USDT' 104 | 105 | ticker = self._ccxt_exc.fetch_ticker(trading_symbol) 106 | price = ticker['last'] 107 | 108 | notional = amount * price 109 | min_notional = self._get_min_notional(symbol) 110 | while notional < min_notional: 111 | amount *= 1.05 112 | notional = amount * price 113 | 114 | logger.info(f'{symbol} final amount to buy - {amount}') 115 | 116 | price *= 1.05 # 5% more to perform market buy 117 | 118 | creation_result = self._ccxt_exc.create_limit_buy_order(trading_symbol, amount, price) 119 | 120 | filled = float(creation_result['filled']) 121 | fee_rate = 0.001 122 | fee = filled * fee_rate 123 | received_amount = filled - fee 124 | 125 | return received_amount 126 | 127 | def get_funding_balance(self, symbol: str) -> float: 128 | balance = self._ccxt_exc.fetch_balance() 129 | 130 | if symbol not in balance: 131 | return 0 132 | token_balance = float(balance[symbol]['free']) 133 | 134 | logger.debug(f'{symbol} funding balance - {token_balance}') 135 | 136 | return token_balance 137 | 138 | def buy_token_and_withdraw(self, symbol: str, amount: float, network: str, address: str) -> None: 139 | withdraw_info = self.get_withdraw_info(symbol, network) 140 | 141 | if withdraw_info.min_amount > amount: 142 | mul = random.uniform(1, 2) 143 | amount = withdraw_info.min_amount * mul 144 | decimal = random.randint(4, 7) 145 | amount = round(amount, decimal) 146 | 147 | amount += withdraw_info.fee * 3 148 | amount_to_withdraw = amount 149 | 150 | balance = self.get_funding_balance(symbol) 151 | if balance < amount: 152 | # Multiplying to avoid decimals casting 153 | bought_amount = self.buy_tokens_with_usdt(symbol, amount) * 0.99 154 | if bought_amount < amount: 155 | amount_to_withdraw = bought_amount 156 | 157 | withdraw_id = self.withdraw(symbol, amount_to_withdraw, network, address) 158 | self.wait_for_withdraw_to_finish(withdraw_id) 159 | -------------------------------------------------------------------------------- /exchange/binance/constants.py: -------------------------------------------------------------------------------- 1 | class BinanceConstants: 2 | # Mapping inside chain names to OKX chain names 3 | NETWORKS = { 4 | 'Ethereum': 'ETH', 5 | 'Optimism': 'OPTIMISM', 6 | 'Arbitrum': 'ARBITRUM', 7 | 'Polygon': 'MATIC', 8 | 'Fantom': 'FTM', 9 | 'Avalanche': 'AVAXC', 10 | 'BSC': 'BSC', 11 | } 12 | 13 | # Mapping token symbols to available chains for withdraw 14 | TOKENS = { 15 | 'USDT': ['BSC', 'Avalanche', 'Ethereum', 'Arbitrum', 'Optimism', 'Polygon'], 16 | 'USDC': ['BSC', 'Avalanche', 'Ethereum', 'Polygon'], 17 | 'BUSD': ['BSC', 'Avalanche', 'Optimism', 'Polygon'], 18 | 'ETH': ['BSC', 'Ethereum', 'Arbitrum', 'Optimism'], 19 | 'MATIC': ['BSC', 'Ethereum', 'Polygon'], 20 | 'BNB': ['BSC'], 21 | 'FTM': ['BSC', 'FTM', 'ETH'], 22 | 'AVAX': ['BSC', 'Avalanche'] 23 | } 24 | -------------------------------------------------------------------------------- /exchange/exchange.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | 6 | import ccxt 7 | 8 | from base.errors import NotSupported, WithdrawCanceled, WithdrawTimeout 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class WithdrawStatus(Enum): 14 | INITIATED = 0 15 | PENDING = 1 16 | FINISHED = 2 17 | CANCELED = 3 18 | 19 | 20 | @dataclass 21 | class WithdrawInfo: 22 | symbol: str 23 | chain: str 24 | fee: float 25 | min_amount: float 26 | 27 | 28 | # Base exchange class 29 | class Exchange: 30 | 31 | def __init__(self, name: str, api_key: str, secret_key: str, ccxt_args: dict) -> None: 32 | self.name = name 33 | ccxt_exchange = getattr(ccxt, name) 34 | self._ccxt_exc = ccxt_exchange({ 35 | 'apiKey': api_key, 36 | 'secret': secret_key, 37 | 'enableRateLimit': True, 38 | **ccxt_args 39 | }) 40 | 41 | def withdraw(self, symbol: str, amount: float, network: str, address: str) -> WithdrawStatus: 42 | """ Method that initiates withdraw funds from the exchange """ 43 | raise NotSupported(f"{self.name} withdraw() is not implemented") 44 | 45 | def wait_for_withdraw_to_finish(self, withdraw_id: str, timeout: int = 1800) -> None: 46 | time.sleep(10) # Sleep to let the exchange process the withdrawal request 47 | 48 | start_time = time.time() 49 | 50 | logger.info(f'Waiting for {withdraw_id} withdraw to be sent') 51 | while True: 52 | status = self.get_withdraw_status(withdraw_id) 53 | 54 | if status == WithdrawStatus.FINISHED: 55 | logger.info(f"Withdraw {withdraw_id} finished") 56 | return 57 | 58 | if status == WithdrawStatus.CANCELED: 59 | raise WithdrawCanceled(f'Withdraw {withdraw_id} canceled') 60 | 61 | if time.time() - start_time >= timeout: 62 | raise WithdrawTimeout(f"Withdraw timeout reached. Id: {withdraw_id}") 63 | 64 | time.sleep(10) # Wait for 10 seconds before checking again 65 | 66 | def is_withdraw_supported(self, symbol: str, network: str) -> bool: 67 | """ Method that checks if the symbol can be withdrawn """ 68 | raise NotSupported(f"{self.name} is_withdraw_supported() is not implemented") 69 | 70 | def get_withdraw_info(self, symbol: str, network: str) -> WithdrawInfo: 71 | """ Method that fetches symbol withdraw information""" 72 | raise NotSupported(f"{self.name} get_withdraw_info() is not implemented") 73 | 74 | def get_funding_balance(self, symbol: str) -> float: 75 | """ Method that fetches non-trading balance from which we can initiate withdraw (can be named differently) """ 76 | raise NotSupported(f"{self.name} get_funding_balance() is not implemented") 77 | 78 | def get_withdraw_status(self, withdraw_id: str) -> WithdrawStatus: 79 | """ Method that fetches withdraw status """ 80 | raise NotSupported(f"{self.name} get_withdraw_info() is not implemented") 81 | 82 | def buy_token_and_withdraw(self, symbol: str, amount: float, network: str, address: str) -> None: 83 | """ Method that checks balance of symbol token, buys it if it's not enough and withdraws this token """ 84 | raise NotSupported(f"{self.name} buy_token_and_withdraw() is not implemented") 85 | 86 | @staticmethod 87 | def _parse_withdraw_status(withdraw_info: dict) -> WithdrawStatus: 88 | if 'status' not in withdraw_info: 89 | raise ValueError(f"Incorrect withdraw_info: {withdraw_info}") 90 | 91 | if withdraw_info['status'] == 'ok': 92 | return WithdrawStatus.FINISHED 93 | if withdraw_info['status'] == 'pending': 94 | return WithdrawStatus.PENDING 95 | if withdraw_info['status'] == 'canceled': 96 | return WithdrawStatus.CANCELED 97 | 98 | raise ValueError(f'Unknown withdraw status: {withdraw_info}') 99 | -------------------------------------------------------------------------------- /exchange/factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | from exchange.binance.binance import Binance 5 | from exchange.okex.okex import Okex 6 | from exchange.exchange import Exchange 7 | 8 | load_dotenv() 9 | 10 | 11 | class ExchangeFactory: 12 | @staticmethod 13 | def create(exchange_name) -> Exchange: 14 | if exchange_name.lower() == "binance": 15 | api_key = os.getenv("BINANCE_API_KEY") 16 | secret_key = os.getenv("BINANCE_SECRET_KEY") 17 | 18 | return Binance(api_key, secret_key) 19 | elif exchange_name.lower() == "okex": 20 | api_key = os.getenv("OKEX_API_KEY") 21 | secret_key = os.getenv("OKEX_SECRET_KEY") 22 | password = os.getenv("OKEX_PASSWORD") 23 | 24 | return Okex(api_key, secret_key, password) 25 | else: 26 | raise ValueError(f"Unknown exchange: {exchange_name}") 27 | -------------------------------------------------------------------------------- /exchange/okex/constants.py: -------------------------------------------------------------------------------- 1 | class OkexConstants: 2 | # Mapping inside chain names to OKX chain names 3 | NETWORKS = { 4 | 'Ethereum': 'ERC-20', 5 | 'Optimism': 'Optimism', 6 | 'Arbitrum': 'Arbitrum one', 7 | 'Polygon': 'Polygon', 8 | 'Fantom': 'Fantom', 9 | 'Avalanche': 'Avalanche C-Chain', 10 | 'BSC': 'BSC', 11 | } 12 | 13 | # Mapping token symbols to available chains for withdraw 14 | TOKENS = { 15 | 'USDT': ['Avalanche', 'Ethereum', 'Arbitrum', 'Optimism', 'Polygon'], 16 | 'USDC': ['Avalanche', 'Ethereum', 'Arbitrum', 'Optimism', 'Polygon'], 17 | 'ETH': ['Ethereum', 'Arbitrum', 'Optimism'], 18 | 'MATIC': ['Ethereum', 'Polygon'], 19 | 'BNB': ['BSC'], 20 | 'FTM': ['FTM'], 21 | 'AVAX': ['Avalanche'] 22 | } 23 | -------------------------------------------------------------------------------- /exchange/okex/okex.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | from typing import List 4 | 5 | import ccxt 6 | 7 | from base.errors import ExchangeError, NotWhitelistedAddress 8 | from exchange.exchange import Exchange, WithdrawInfo, WithdrawStatus 9 | from exchange.okex.constants import OkexConstants 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Okex(Exchange): 15 | def __init__(self, api_key: str, secret_key: str, api_password: str): 16 | ccxt_args = {'password': api_password} 17 | super().__init__('okex', api_key, secret_key, ccxt_args) 18 | 19 | self.funding_account = 'funding' 20 | self.trading_account = 'spot' 21 | 22 | def _get_withdraw_infos(self, symbol: str) -> List[WithdrawInfo]: 23 | currencies = self._ccxt_exc.fetch_currencies() 24 | chains_info = currencies[symbol]['networks'] 25 | 26 | result = [] 27 | 28 | for chain_info in chains_info.values(): 29 | info = WithdrawInfo(symbol, chain_info['info']['chain'], chain_info['fee'], 30 | chain_info['limits']['withdraw']['min']) 31 | result.append(info) 32 | 33 | return result 34 | 35 | def is_withdraw_supported(self, symbol: str, network: str) -> bool: 36 | if symbol in OkexConstants.TOKENS and network in OkexConstants.TOKENS[symbol]: 37 | return True 38 | return False 39 | 40 | def get_withdraw_info(self, symbol: str, network: str) -> WithdrawInfo: 41 | okx_network = OkexConstants.NETWORKS[network] 42 | withdraw_options = self._get_withdraw_infos(symbol) 43 | 44 | chain = f"{symbol}-{okx_network}" 45 | 46 | withdraw_info = next((option for option in withdraw_options if option.chain == chain), None) 47 | 48 | if not withdraw_info: 49 | raise ExchangeError(f"OKEX doesn't support {symbol}({network}) withdrawals") 50 | 51 | return withdraw_info 52 | 53 | def withdraw(self, symbol: str, amount: float, network: str, address: str) -> str: 54 | """ Method that initiates the withdrawal and returns the withdrawal id """ 55 | 56 | logger.info(f'{symbol} withdraw initiated. Amount: {amount}. Network: {network}. Address: {address}') 57 | withdraw_info = self.get_withdraw_info(symbol, network) 58 | amount -= withdraw_info.fee 59 | logger.info(f'Amount with fee: {amount}') 60 | 61 | try: 62 | result = self._ccxt_exc.withdraw(symbol, amount, address, 63 | {"chain": withdraw_info.chain, 'fee': withdraw_info.fee, 'pwd': "-"}) 64 | except Exception as ex: 65 | if 'Withdrawal address is not whitelisted for verification exemption' in str(ex): 66 | raise NotWhitelistedAddress(f'Unable to withdraw {symbol}({network}) to {address}. ' 67 | f'The address must be added to the whitelist') from ex 68 | raise 69 | 70 | logger.debug(f'Withdraw result: {result}') 71 | withdraw_id = result['id'] 72 | 73 | return str(withdraw_id) 74 | 75 | def get_withdraw_status(self, withdraw_id: str) -> WithdrawStatus: 76 | withdraw_info = self._ccxt_exc.fetch_withdrawal(withdraw_id) 77 | 78 | return self._parse_withdraw_status(withdraw_info) 79 | 80 | def transfer_funds(self, symbol: str, amount: float, from_account: str, to_account: str): 81 | logger.info(f'{symbol} transfer initiated. From {from_account} to {to_account}') 82 | result = self._ccxt_exc.transfer(symbol, amount, from_account, to_account) 83 | logger.debug('Transfer result:', result) 84 | 85 | def buy_tokens_with_usdt(self, symbol: str, amount: float) -> float: 86 | logger.info(f'{symbol} purchase initiated. Amount: {amount}') 87 | trading_symbol = symbol + '/USDT' 88 | 89 | creation_result = self._ccxt_exc.create_market_order(trading_symbol, 'buy', amount) 90 | order = self._ccxt_exc.fetch_order(creation_result['id'], trading_symbol) 91 | 92 | filled = float(order['filled']) 93 | fee = float(order['fee']['cost']) 94 | received_amount = filled - fee 95 | 96 | return received_amount 97 | 98 | def get_funding_balance(self, symbol: str) -> float: 99 | balance = self._ccxt_exc.fetch_balance(params={'type': self.funding_account}) 100 | 101 | if symbol not in balance['total']: 102 | return 0 103 | token_balance = float(balance['total'][symbol]) 104 | 105 | return token_balance 106 | 107 | def buy_token_and_withdraw(self, symbol: str, amount: float, network: str, address: str) -> None: 108 | withdraw_info = self.get_withdraw_info(symbol, network) 109 | 110 | if withdraw_info.min_amount > amount: 111 | mul = random.uniform(1, 2) 112 | amount = withdraw_info.min_amount * mul 113 | decimal = random.randint(4, 7) 114 | amount = round(amount, decimal) 115 | 116 | amount += withdraw_info.fee * 3 117 | 118 | balance = self.get_funding_balance(symbol) 119 | if balance < amount: 120 | # Multiplying to avoid decimals casting 121 | amount_to_withdraw = self.buy_tokens_with_usdt(symbol, amount) * 0.99 122 | self.transfer_funds(symbol, amount_to_withdraw, self.trading_account, self.funding_account) 123 | else: 124 | amount_to_withdraw = amount 125 | 126 | withdraw_id = self.withdraw(symbol, amount_to_withdraw, network, address) 127 | self.wait_for_withdraw_to_finish(withdraw_id) 128 | -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | 4 | 5 | def setup_logger(level=logging.INFO) -> logging.Logger: 6 | """ Setup main console handler """ 7 | 8 | logging.getLogger('web3').setLevel(logging.INFO) 9 | logging.getLogger('urllib3').setLevel(logging.INFO) 10 | logging.getLogger('ccxt').setLevel(logging.INFO) 11 | 12 | formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(threadName)-10s] %(message)s') 13 | 14 | console_handler = logging.StreamHandler() 15 | console_handler.setLevel(level) 16 | console_handler.setFormatter(formatter) 17 | 18 | logger = logging.getLogger() 19 | logger.setLevel(logging.DEBUG) 20 | logger.addHandler(console_handler) 21 | 22 | return logger 23 | 24 | 25 | class ThreadLogFilter(logging.Filter): 26 | """ This filter only show log entries for specified thread name """ 27 | 28 | def __init__(self, thread_name, *args, **kwargs) -> None: 29 | logging.Filter.__init__(self, *args, **kwargs) 30 | self.thread_name = thread_name 31 | 32 | def filter(self, record) -> bool: 33 | return record.threadName == self.thread_name 34 | 35 | 36 | def setup_thread_logger(path: str, log_level=logging.INFO) -> logging.FileHandler: 37 | """ Add a log handler to separate file for current thread """ 38 | 39 | thread_name = threading.current_thread().name 40 | log_file = f'{path}/{thread_name}.log' 41 | file_handler = logging.FileHandler(log_file) 42 | 43 | file_handler.setLevel(log_level) 44 | 45 | formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') 46 | file_handler.setFormatter(formatter) 47 | 48 | log_filter = ThreadLogFilter(thread_name) 49 | file_handler.addFilter(log_filter) 50 | 51 | logger = logging.getLogger() 52 | logger.addHandler(file_handler) 53 | 54 | return file_handler 55 | -------------------------------------------------------------------------------- /logic/__init__.py: -------------------------------------------------------------------------------- 1 | from logic.account_thread import AccountThread 2 | -------------------------------------------------------------------------------- /logic/account_thread.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import time 4 | from typing import Optional 5 | 6 | import requests 7 | from ccxt.base.errors import RateLimitExceeded, InsufficientFunds 8 | from eth_account import Account 9 | 10 | from base.errors import BaseError 11 | from config import TimeRanges, BridgerMode, RefuelMode 12 | from logger import setup_thread_logger 13 | from logic.stargate_states import SleepBeforeStartStargateBridgerState, CheckStablecoinBalanceState 14 | from logic.btcb_states import SleepBeforeStartBTCBridgerState, CheckBTCbBalanceState 15 | from logic.state import InitialState 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class AccountThread(threading.Thread): 21 | def __init__(self, account_id: int, private_key: str, bridger_mode: BridgerMode, refuel_mode: RefuelMode, 22 | bridges_limit: Optional[int]) -> None: 23 | super().__init__(name=f"Account-{account_id}") 24 | self.account_id = account_id 25 | self.account = Account.from_key(private_key) 26 | self.bridger_mode = bridger_mode 27 | self.refuel_mode = refuel_mode 28 | self.bridges_limit = bridges_limit 29 | self.remaining_bridges = bridges_limit 30 | self.state = InitialState() 31 | 32 | def run(self) -> None: 33 | setup_thread_logger("logs") 34 | logger.info(f"Account address: {self.account.address}") 35 | 36 | if self.bridger_mode == BridgerMode.STARGATE: 37 | self._run_stargate_mode() 38 | elif self.bridger_mode == BridgerMode.BTCB: 39 | self._run_btcb_mode() 40 | elif self.bridger_mode == BridgerMode.TESTNET: 41 | pass 42 | else: 43 | raise ValueError("Unknown BridgeMode") 44 | 45 | def set_state(self, state) -> None: 46 | self.state = state 47 | 48 | def are_bridges_left(self) -> bool: 49 | if self.remaining_bridges is None: 50 | return True 51 | 52 | bridges_left = self.remaining_bridges > 0 53 | if not bridges_left: 54 | logger.info('The bridge limit has been reached. The work is over') 55 | return bridges_left 56 | 57 | def _run_stargate_mode(self) -> None: 58 | logger.info("Running Stargate bridger") 59 | 60 | self.set_state(SleepBeforeStartStargateBridgerState()) 61 | while self.are_bridges_left(): 62 | try: 63 | self.state.handle(self) 64 | except BaseError as ex: 65 | logger.error(f'Exception: {ex}') 66 | self.set_state(CheckStablecoinBalanceState()) 67 | except requests.exceptions.HTTPError: 68 | logger.error(f'Too many request to HTTP Provider!') 69 | self.set_state(CheckStablecoinBalanceState()) 70 | time.sleep(TimeRanges.MINUTE) 71 | except RateLimitExceeded: 72 | logger.error(f'Too many request to exchange!') 73 | self.set_state(CheckStablecoinBalanceState()) 74 | time.sleep(TimeRanges.MINUTE) 75 | except InsufficientFunds: 76 | logger.error(f'Not enough balance on exchange!') 77 | self.set_state(CheckStablecoinBalanceState()) 78 | time.sleep(10 * TimeRanges.MINUTE) 79 | 80 | def _run_btcb_mode(self) -> None: 81 | logger.info("Running BTC.b bridger") 82 | 83 | self.set_state(SleepBeforeStartBTCBridgerState()) 84 | while self.are_bridges_left(): 85 | try: 86 | self.state.handle(self) 87 | except BaseError as ex: 88 | logger.error(f'Exception: {ex}') 89 | self.set_state(CheckBTCbBalanceState()) 90 | except requests.exceptions.HTTPError: 91 | logger.error(f'Too many request to HTTP Provider!') 92 | self.set_state(CheckBTCbBalanceState()) 93 | time.sleep(TimeRanges.MINUTE) 94 | except RateLimitExceeded: 95 | logger.error(f'Too many request to exchange!') 96 | self.set_state(CheckBTCbBalanceState()) 97 | time.sleep(TimeRanges.MINUTE) 98 | except InsufficientFunds: 99 | logger.error(f'Not enough balance on exchange!') 100 | self.set_state(CheckBTCbBalanceState()) 101 | time.sleep(10 * TimeRanges.MINUTE) 102 | -------------------------------------------------------------------------------- /logic/btcb_states.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import datetime 3 | import random 4 | import time 5 | import os 6 | from dataclasses import dataclass 7 | from dotenv import load_dotenv 8 | from typing import List 9 | 10 | from base.errors import ConfigurationError, NotWhitelistedAddress 11 | from config import SUPPORTED_NETWORKS_BTCB, SleepTimings, RefuelMode 12 | from logic.state import State 13 | from network import EVMNetwork 14 | from network.polygon.polygon import Polygon 15 | from utility import Stablecoin 16 | from exchange import ExchangeFactory 17 | from btcb import BTCbBridgeHelper, BTCbUtils, BTCbConstants 18 | 19 | logger = logging.getLogger(__name__) 20 | load_dotenv() 21 | 22 | 23 | # State for waiting before start to randomize start time 24 | class SleepBeforeStartBTCBridgerState(State): 25 | def __init__(self) -> None: 26 | pass 27 | 28 | def handle(self, thread) -> None: 29 | sleep_time = random.randint(SleepTimings.AFTER_START_RANGE[0], SleepTimings.AFTER_START_RANGE[1]) 30 | 31 | logger.info(f"Sleeping {sleep_time} seconds before start") 32 | time.sleep(sleep_time) 33 | 34 | thread.set_state(CheckBTCbBalanceState()) 35 | 36 | 37 | # State for waiting for the BTC.b deposit 38 | class WaitForBTCbDeposit(State): 39 | def __init__(self) -> None: 40 | pass 41 | 42 | def handle(self, thread) -> None: 43 | logger.info("Waiting for BTC.b deposit") 44 | time.sleep(SleepTimings.BALANCE_RECHECK_TIME) 45 | 46 | thread.set_state(CheckBTCbBalanceState()) 47 | 48 | 49 | # State for checking the BTC.b balance 50 | class CheckBTCbBalanceState(State): 51 | def __init__(self) -> None: 52 | pass 53 | 54 | def is_enough_balance(self, network: EVMNetwork, address: str) -> bool: 55 | balance = BTCbUtils.get_btcb_balance(network, address) 56 | min_balance = float(os.getenv('BTCB_MIN_BALANCE')) * 10 ** BTCbConstants.BTCB_DECIMALS 57 | min_balance = int(min_balance) 58 | 59 | logger.info(f'{network.name}. BTC.b balance - {balance / 10 ** BTCbConstants.BTCB_DECIMALS}') 60 | 61 | if balance > min_balance: 62 | return True 63 | return False 64 | 65 | def find_networks_with_balance(self, thread) -> List[EVMNetwork]: 66 | """ Method that checks BTC.b balances in all networks and returns the list of networks 67 | that satisfies the minimum balance condition """ 68 | 69 | result = [] 70 | 71 | for network in SUPPORTED_NETWORKS_BTCB: 72 | if self.is_enough_balance(network, thread.account.address): 73 | result.append(network) 74 | 75 | return result 76 | 77 | def handle(self, thread) -> None: 78 | logger.info("Checking BTC.b balance") 79 | 80 | networks = self.find_networks_with_balance(thread) 81 | if len(networks) == 0: 82 | logger.info("Not enough BTC.b balance. Refill one of the supported networks") 83 | thread.set_state(WaitForBTCbDeposit()) 84 | elif len(networks) == 1: 85 | logger.info(f"{networks[0].name} network meet the minimum BTC.b balance requirements") 86 | thread.set_state(ChooseDestinationNetworkState(networks[0])) 87 | else: 88 | logger.info( 89 | f"{len(networks)} networks meet the minimum BTC.b balance requirements. Randomizing choice") 90 | random_network = random.choice(networks) 91 | logger.info(f"{random_network.name} was randomized") 92 | thread.set_state(ChooseDestinationNetworkState(random_network)) 93 | 94 | 95 | # State for choosing a random destination network 96 | class ChooseDestinationNetworkState(State): 97 | def __init__(self, src_network: EVMNetwork) -> None: 98 | self.src_network = src_network 99 | 100 | def handle(self, thread) -> None: 101 | logger.info("Randomizing destination network") 102 | 103 | networks = SUPPORTED_NETWORKS_BTCB.copy() 104 | networks.remove(self.src_network) 105 | 106 | if len(networks) == 0: 107 | raise ConfigurationError("Unable to select destination chain. " 108 | "Revise the list of supported networks in config") 109 | 110 | dst_network = random.choice(networks) 111 | logger.info(f"Destination network is chosen - {dst_network.name}") 112 | thread.set_state(CheckNativeTokenBalanceForGasState(self.src_network, dst_network)) 113 | 114 | 115 | # State for deciding whether gas will be refueled automatically or manually 116 | class RefuelDecisionState(State): 117 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 118 | self.src_network = src_network 119 | self.dst_network = dst_network 120 | 121 | def handle(self, thread) -> None: 122 | logger.info("Checking possible refuel options") 123 | 124 | if thread.refuel_mode in [RefuelMode.OKEX, RefuelMode.BINANCE]: 125 | thread.set_state(SleepBeforeExchangeRefuelState(self.src_network, self.dst_network)) 126 | else: 127 | thread.set_state(WaitForManualRefuelState(self.src_network, self.dst_network)) 128 | 129 | 130 | # State for waiting for a manual native token deposit (in case the auto-refuel failed or disabled) 131 | class WaitForManualRefuelState(State): 132 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 133 | self.src_network = src_network 134 | self.dst_network = dst_network 135 | 136 | def handle(self, thread) -> None: 137 | logger.info(f"{self.src_network.name}. Manual refuel chosen. Waiting for the native token deposit") 138 | time.sleep(SleepTimings.BALANCE_RECHECK_TIME) 139 | thread.set_state(CheckNativeTokenBalanceForGasState(self.src_network, self.dst_network)) 140 | 141 | 142 | # State for waiting before the exchange withdraw to make an account unique 143 | class SleepBeforeExchangeRefuelState(State): 144 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 145 | self.src_network = src_network 146 | self.dst_network = dst_network 147 | 148 | def handle(self, thread) -> None: 149 | sleep_time = random.randint(SleepTimings.BEFORE_WITHDRAW_RANGE[0], SleepTimings.BEFORE_WITHDRAW_RANGE[1]) 150 | 151 | withdraw_dt = datetime.datetime.fromtimestamp(time.time() + sleep_time) 152 | logger.info(f"Sleeping {sleep_time} seconds before withdraw from exchange. Withdraw time: {withdraw_dt}") 153 | time.sleep(sleep_time) 154 | 155 | thread.set_state(RefuelWithExchangeState(self.src_network, self.dst_network)) 156 | 157 | 158 | # State for refueling native token from exchange to cover gas fees 159 | class RefuelWithExchangeState(State): 160 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 161 | self.src_network = src_network 162 | self.dst_network = dst_network 163 | 164 | def refuel(self, thread, amount: float) -> None: 165 | factory = ExchangeFactory() 166 | 167 | if thread.refuel_mode == RefuelMode.OKEX: 168 | exchange = factory.create("okex") 169 | else: 170 | exchange = factory.create("binance") 171 | 172 | symbol = self.src_network.native_token 173 | 174 | try: 175 | exchange.buy_token_and_withdraw(symbol, amount, self.src_network.name, thread.account.address) 176 | except NotWhitelistedAddress: 177 | logger.warning(f"Address {thread.account.address} is not whitelisted to withdraw " 178 | f"{self.src_network.native_token} in {self.src_network.name} network") 179 | 180 | def handle(self, thread) -> None: 181 | logger.info(f"Exchange refueling started") 182 | 183 | layer_zero_fee = BTCbUtils.estimate_layerzero_bridge_fee(self.src_network, self.dst_network, 184 | thread.account.address) / 10 ** 18 185 | bridge_price = BTCbUtils.estimate_bridge_gas_price(self.src_network, self.dst_network, 186 | thread.account.address) / 10 ** 18 187 | mul = 1.1 # Multiplier to withdraw funds with a reserve 188 | 189 | logger.info(f'L0 fee: {layer_zero_fee} {self.src_network.native_token}. ' 190 | f'BTC bridge price: {bridge_price} {self.src_network.native_token}') 191 | 192 | amount_to_withdraw = mul * (layer_zero_fee + bridge_price) 193 | 194 | # Quick fix 195 | if isinstance(self.src_network, Polygon): 196 | amount_to_withdraw /= 3 197 | 198 | # Multiplier to randomize withdraw amount 199 | multiplier = random.uniform(1, 1.5) 200 | amount_to_withdraw *= multiplier 201 | decimals = random.randint(4, 7) 202 | amount_to_withdraw = round(amount_to_withdraw, decimals) 203 | 204 | logger.info(f'To withdraw: {amount_to_withdraw}') 205 | self.refuel(thread, amount_to_withdraw) 206 | 207 | thread.set_state(CheckNativeTokenBalanceForGasState(self.src_network, self.dst_network)) 208 | 209 | 210 | # State for checking the native token balance 211 | class CheckNativeTokenBalanceForGasState(State): 212 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 213 | self.src_network = src_network 214 | self.dst_network = dst_network 215 | 216 | def handle(self, thread) -> None: 217 | logger.info("Checking native token balance") 218 | 219 | if BTCbUtils.is_enough_native_balance_for_bridge_fee(self.src_network, self.dst_network, 220 | thread.account.address): 221 | logger.info("Enough native token amount on source chain. Moving to the bridge") 222 | thread.set_state(SleepBeforeBridgeState(self.src_network, self.dst_network)) 223 | else: 224 | logger.info("Not enough native token amount on source chain to cover the fees") 225 | thread.set_state(RefuelDecisionState(self.src_network, self.dst_network)) 226 | 227 | 228 | # State for waiting before every bridge to make an account unique 229 | class SleepBeforeBridgeState(State): 230 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 231 | self.src_network = src_network 232 | self.dst_network = dst_network 233 | 234 | def handle(self, thread) -> None: 235 | sleep_time = random.randint(SleepTimings.BEFORE_BRIDGE_RANGE[0], SleepTimings.BEFORE_BRIDGE_RANGE[1]) 236 | 237 | next_swap_dt = datetime.datetime.fromtimestamp(time.time() + sleep_time) 238 | logger.info(f"Sleeping {sleep_time} seconds before bridge. Next bridge time: {next_swap_dt}") 239 | time.sleep(sleep_time) 240 | 241 | thread.set_state(BTCBridgeState(self.src_network, self.dst_network)) 242 | 243 | 244 | # State for swapping tokens 245 | class BTCBridgeState(State): 246 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork) -> None: 247 | self.src_network = src_network 248 | self.dst_network = dst_network 249 | 250 | def handle(self, thread) -> None: 251 | amount = BTCbUtils.get_btcb_balance(self.src_network, thread.account.address) 252 | 253 | logger.info(f"Bridging {amount / 10 ** BTCbConstants.BTCB_DECIMALS} BTC.b through BTC bridge. " 254 | f"{self.src_network.name} -> {self.dst_network.name}") 255 | 256 | bh = BTCbBridgeHelper(thread.account, self.src_network, self.dst_network, amount) 257 | bridge_result = bh.make_bridge() 258 | 259 | if bridge_result: 260 | logger.info(f"BTC bridge finished successfully") 261 | if thread.remaining_bridges: 262 | thread.remaining_bridges -= 1 263 | else: 264 | logger.info(f"BTC bridge finished with error") 265 | 266 | if thread.remaining_bridges: 267 | logger.info(f"Remaining bridges: {thread.remaining_bridges}/{thread.bridges_limit}") 268 | 269 | thread.set_state(CheckBTCbBalanceState()) 270 | -------------------------------------------------------------------------------- /logic/stargate_states.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import datetime 3 | import random 4 | import time 5 | import os 6 | from dataclasses import dataclass 7 | from dotenv import load_dotenv 8 | from typing import List 9 | 10 | from base.errors import ConfigurationError, StablecoinNotSupportedByChain, NotWhitelistedAddress 11 | from config import SUPPORTED_NETWORKS_STARGATE, SleepTimings, \ 12 | RefuelMode 13 | from logic.state import State 14 | from network import EVMNetwork 15 | from network.polygon.polygon import Polygon 16 | from utility import Stablecoin 17 | from network.balance_helper import BalanceHelper 18 | from exchange import ExchangeFactory 19 | from stargate import StargateBridgeHelper, StargateUtils 20 | 21 | logger = logging.getLogger(__name__) 22 | load_dotenv() 23 | 24 | 25 | # State for waiting before start to randomize start time 26 | class SleepBeforeStartStargateBridgerState(State): 27 | def __init__(self) -> None: 28 | pass 29 | 30 | def handle(self, thread) -> None: 31 | sleep_time = random.randint(SleepTimings.AFTER_START_RANGE[0], SleepTimings.AFTER_START_RANGE[1]) 32 | 33 | logger.info(f"Sleeping {sleep_time} seconds before start") 34 | time.sleep(sleep_time) 35 | 36 | thread.set_state(CheckStablecoinBalanceState()) 37 | 38 | 39 | # State for waiting for the stablecoin deposit 40 | class WaitForStablecoinDepositState(State): 41 | def __init__(self) -> None: 42 | pass 43 | 44 | def handle(self, thread) -> None: 45 | logger.info("Waiting for stablecoin deposit") 46 | time.sleep(SleepTimings.BALANCE_RECHECK_TIME) 47 | 48 | thread.set_state(CheckStablecoinBalanceState()) 49 | 50 | 51 | # Utility class to store the network and related stablecoin that will be used in future 52 | @dataclass 53 | class NetworkWithStablecoinBalance: 54 | network: EVMNetwork 55 | stablecoin: Stablecoin 56 | 57 | 58 | # State for checking the stablecoin balance 59 | class CheckStablecoinBalanceState(State): 60 | def __init__(self) -> None: 61 | pass 62 | 63 | def is_enough_balance(self, balance_helper: BalanceHelper, stablecoin: Stablecoin) -> bool: 64 | balance = balance_helper.get_stablecoin_balance(stablecoin) 65 | min_balance = float(os.getenv('STARGATE_MIN_STABLECOIN_BALANCE')) * 10 ** stablecoin.decimals 66 | min_balance = int(min_balance) 67 | 68 | logger.info(f'{balance_helper.network.name}. {stablecoin.symbol} ' 69 | f'balance - {balance / 10 ** stablecoin.decimals}') 70 | 71 | if balance > min_balance: 72 | return True 73 | return False 74 | 75 | def find_networks_with_balance(self, thread) -> List[NetworkWithStablecoinBalance]: 76 | """ Method that checks stablecoin balances in all networks and returns the list of networks 77 | and related stablecoins that satisfies the minimum balance condition """ 78 | 79 | result = [] 80 | 81 | for network in SUPPORTED_NETWORKS_STARGATE: 82 | for stablecoin in network.supported_stablecoins.values(): 83 | if self.is_enough_balance(BalanceHelper(network, thread.account.address), 84 | stablecoin): 85 | result.append(NetworkWithStablecoinBalance(network, stablecoin)) 86 | 87 | return result 88 | 89 | def handle(self, thread) -> None: 90 | logger.info("Checking stablecoin balance") 91 | 92 | networks = self.find_networks_with_balance(thread) 93 | if len(networks) == 0: 94 | logger.info("Not enough stablecoin balance. Refill one of the supported networks") 95 | thread.set_state(WaitForStablecoinDepositState()) 96 | elif len(networks) == 1: 97 | logger.info(f"{networks[0].network.name} network meet the minimum stablecoin balance requirements") 98 | thread.set_state(ChooseDestinationNetworkState(networks[0].network, networks[0].stablecoin)) 99 | else: 100 | logger.info( 101 | f"{len(networks)} networks meet the minimum stablecoin balance requirements. Randomizing choice") 102 | random_network = random.choice(networks) 103 | logger.info(f"{random_network.network.name} was randomized") 104 | thread.set_state(ChooseDestinationNetworkState(random_network.network, random_network.stablecoin)) 105 | 106 | 107 | # State for choosing a random destination network 108 | class ChooseDestinationNetworkState(State): 109 | def __init__(self, src_network: EVMNetwork, src_stablecoin: Stablecoin) -> None: 110 | self.src_network = src_network 111 | self.src_stablecoin = src_stablecoin 112 | 113 | def handle(self, thread) -> None: 114 | logger.info("Randomizing destination network") 115 | 116 | networks = SUPPORTED_NETWORKS_STARGATE.copy() 117 | networks.remove(self.src_network) 118 | 119 | if len(networks) == 0: 120 | raise ConfigurationError("Unable to select destination chain. " 121 | "Revise the list of supported networks in config") 122 | 123 | dst_network = random.choice(networks) 124 | logger.info(f"Destination network is chosen - {dst_network.name}") 125 | thread.set_state(ChooseDestinationStablecoinState(self.src_network, dst_network, self.src_stablecoin)) 126 | 127 | 128 | # State for choosing a random destination stablecoin 129 | class ChooseDestinationStablecoinState(State): 130 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, src_stablecoin: Stablecoin) -> None: 131 | self.src_network = src_network 132 | self.dst_network = dst_network 133 | self.src_stablecoin = src_stablecoin 134 | 135 | def handle(self, thread) -> None: 136 | logger.info("Choosing destination stablecoin") 137 | 138 | if len(self.dst_network.supported_stablecoins) == 0: 139 | raise StablecoinNotSupportedByChain(f"{self.dst_network} chain doesn't support any stablecoin") 140 | 141 | dst_stablecoin = random.choice(list(self.dst_network.supported_stablecoins.values())) 142 | 143 | logger.info(f"Destination stablecoin is chosen - {dst_stablecoin.symbol}") 144 | logger.info(f"Path: {self.src_stablecoin.symbol} ({self.src_network.name}) -> " 145 | f"{dst_stablecoin.symbol} ({self.dst_network.name})") 146 | 147 | thread.set_state(CheckNativeTokenBalanceForGasState(self.src_network, self.dst_network, 148 | self.src_stablecoin, dst_stablecoin)) 149 | 150 | 151 | # State for deciding whether gas will be refueled automatically or manually 152 | class RefuelDecisionState(State): 153 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 154 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 155 | self.src_network = src_network 156 | self.dst_network = dst_network 157 | self.src_stablecoin = src_stablecoin 158 | self.dst_stablecoin = dst_stablecoin 159 | 160 | def handle(self, thread) -> None: 161 | logger.info("Checking possible refuel options") 162 | 163 | # TODO: Add auto refuel with Bungee/WooFi 164 | 165 | if thread.refuel_mode == RefuelMode.OKEX or thread.refuel_mode == RefuelMode.BINANCE: 166 | thread.set_state(SleepBeforeExchangeRefuelState(self.src_network, self.dst_network, 167 | self.src_stablecoin, self.dst_stablecoin)) 168 | else: 169 | thread.set_state(WaitForManualRefuelState(self.src_network, self.dst_network, 170 | self.src_stablecoin, self.dst_stablecoin)) 171 | 172 | 173 | # State for waiting for a manual native token deposit (in case the auto-refuel failed or disabled) 174 | class WaitForManualRefuelState(State): 175 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 176 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 177 | self.src_network = src_network 178 | self.dst_network = dst_network 179 | self.src_stablecoin = src_stablecoin 180 | self.dst_stablecoin = dst_stablecoin 181 | 182 | def handle(self, thread) -> None: 183 | logger.info(f"{self.src_network.name}. Manual refuel chosen. Waiting for the native token deposit") 184 | time.sleep(SleepTimings.BALANCE_RECHECK_TIME) 185 | thread.set_state(CheckNativeTokenBalanceForGasState(self.src_network, self.dst_network, 186 | self.src_stablecoin, self.dst_stablecoin)) 187 | 188 | 189 | # State for waiting before the exchange withdraw to make an account unique 190 | class SleepBeforeExchangeRefuelState(State): 191 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 192 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 193 | self.src_network = src_network 194 | self.dst_network = dst_network 195 | self.src_stablecoin = src_stablecoin 196 | self.dst_stablecoin = dst_stablecoin 197 | 198 | def handle(self, thread) -> None: 199 | sleep_time = random.randint(SleepTimings.BEFORE_WITHDRAW_RANGE[0], SleepTimings.BEFORE_WITHDRAW_RANGE[1]) 200 | 201 | withdraw_dt = datetime.datetime.fromtimestamp(time.time() + sleep_time) 202 | logger.info(f"Sleeping {sleep_time} seconds before withdraw from exchange. Withdraw time: {withdraw_dt}") 203 | time.sleep(sleep_time) 204 | 205 | thread.set_state(RefuelWithExchangeState(self.src_network, self.dst_network, 206 | self.src_stablecoin, self.dst_stablecoin)) 207 | 208 | 209 | # State for refueling native token from exchange to cover gas fees 210 | class RefuelWithExchangeState(State): 211 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 212 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 213 | self.src_network = src_network 214 | self.dst_network = dst_network 215 | self.src_stablecoin = src_stablecoin 216 | self.dst_stablecoin = dst_stablecoin 217 | 218 | def refuel(self, thread, amount: float) -> None: 219 | factory = ExchangeFactory() 220 | 221 | if thread.refuel_mode == RefuelMode.OKEX: 222 | exchange = factory.create("okex") 223 | else: 224 | exchange = factory.create("binance") 225 | 226 | symbol = self.src_network.native_token 227 | 228 | try: 229 | exchange.buy_token_and_withdraw(symbol, amount, self.src_network.name, thread.account.address) 230 | except NotWhitelistedAddress: 231 | logger.warning(f"WARNING! Address {thread.account.address} is not whitelisted to withdraw " 232 | f"{self.src_network.native_token} in {self.src_network.name} network") 233 | 234 | def handle(self, thread) -> None: 235 | logger.info(f"Exchange refueling started") 236 | 237 | layer_zero_fee = StargateUtils.estimate_layerzero_swap_fee(self.src_network, self.dst_network, 238 | thread.account.address) / 10 ** 18 239 | swap_price = StargateUtils.estimate_swap_gas_price(self.src_network, self.dst_network, 240 | thread.account.address) / 10 ** 18 241 | mul = 1.1 # Multiplier to withdraw funds with a reserve 242 | 243 | logger.info(f'L0 fee: {layer_zero_fee} {self.src_network.native_token}. ' 244 | f'Swap price: {swap_price} {self.src_network.native_token}') 245 | 246 | amount_to_withdraw = mul * (layer_zero_fee + swap_price) 247 | 248 | # Quick fix 249 | if isinstance(self.src_network, Polygon): 250 | amount_to_withdraw /= 3 251 | 252 | # Multiplier to randomize withdraw amount 253 | multiplier = random.uniform(1, 1.5) 254 | amount_to_withdraw *= multiplier 255 | decimals = random.randint(4, 7) 256 | amount_to_withdraw = round(amount_to_withdraw, decimals) 257 | 258 | logger.info(f'To withdraw: {amount_to_withdraw}') 259 | self.refuel(thread, amount_to_withdraw) 260 | 261 | thread.set_state(CheckNativeTokenBalanceForGasState(self.src_network, self.dst_network, 262 | self.src_stablecoin, self.dst_stablecoin)) 263 | 264 | 265 | # TODO 266 | class RefuelWithBungeeState(State): 267 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 268 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 269 | self.src_network = src_network 270 | self.dst_network = dst_network 271 | self.src_stablecoin = src_stablecoin 272 | self.dst_stablecoin = dst_stablecoin 273 | 274 | 275 | # State for checking the native token balance 276 | class CheckNativeTokenBalanceForGasState(State): 277 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 278 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 279 | self.src_network = src_network 280 | self.dst_network = dst_network 281 | self.src_stablecoin = src_stablecoin 282 | self.dst_stablecoin = dst_stablecoin 283 | 284 | def handle(self, thread) -> None: 285 | logger.info("Checking native token balance") 286 | 287 | if StargateUtils.is_enough_native_balance_for_swap_fee(self.src_network, self.dst_network, 288 | thread.account.address): 289 | logger.info("Enough native token amount on source chain. Moving to the swap") 290 | thread.set_state(SleepBeforeBridgeState(self.src_network, self.dst_network, 291 | self.src_stablecoin, self.dst_stablecoin)) 292 | else: 293 | logger.info("Not enough native token amount on source chain to cover the fees") 294 | thread.set_state(RefuelDecisionState(self.src_network, self.dst_network, 295 | self.src_stablecoin, self.dst_stablecoin)) 296 | 297 | 298 | # State for waiting before every bridge to make an account unique 299 | class SleepBeforeBridgeState(State): 300 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 301 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 302 | self.src_network = src_network 303 | self.dst_network = dst_network 304 | self.src_stablecoin = src_stablecoin 305 | self.dst_stablecoin = dst_stablecoin 306 | 307 | def handle(self, thread) -> None: 308 | sleep_time = random.randint(SleepTimings.BEFORE_BRIDGE_RANGE[0], SleepTimings.BEFORE_BRIDGE_RANGE[1]) 309 | 310 | next_swap_dt = datetime.datetime.fromtimestamp(time.time() + sleep_time) 311 | logger.info(f"Sleeping {sleep_time} seconds before bridge. Next bridge time: {next_swap_dt}") 312 | time.sleep(sleep_time) 313 | 314 | thread.set_state(StargateSwapState(self.src_network, self.dst_network, 315 | self.src_stablecoin, self.dst_stablecoin)) 316 | 317 | 318 | # State for swapping tokens 319 | class StargateSwapState(State): 320 | def __init__(self, src_network: EVMNetwork, dst_network: EVMNetwork, 321 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin) -> None: 322 | self.src_network = src_network 323 | self.dst_network = dst_network 324 | self.src_stablecoin = src_stablecoin 325 | self.dst_stablecoin = dst_stablecoin 326 | 327 | def handle(self, thread) -> None: 328 | balance_helper = BalanceHelper(self.src_network, thread.account.address) 329 | amount = balance_helper.get_stablecoin_balance(self.src_stablecoin) 330 | 331 | logger.info(f"Swapping {amount / 10 ** self.src_stablecoin.decimals} tokens through Stargate bridge. " 332 | f"{self.src_stablecoin.symbol}({self.src_network.name}) -> " 333 | f"{self.dst_stablecoin.symbol}({self.dst_network.name})") 334 | 335 | bridge_helper = StargateBridgeHelper(thread.account, self.src_network, self.dst_network, 336 | self.src_stablecoin, self.dst_stablecoin, amount, 337 | float(os.getenv('STARGATE_SLIPPAGE', 0.01))) 338 | bridge_result = bridge_helper.make_bridge() 339 | 340 | if bridge_result: 341 | logger.info(f"Stargate bridge finished successfully") 342 | if thread.remaining_bridges: 343 | thread.remaining_bridges -= 1 344 | else: 345 | logger.info(f"Stargate bridge finished with error") 346 | 347 | if thread.remaining_bridges: 348 | logger.info(f"Remaining bridges: {thread.remaining_bridges}/{thread.bridges_limit}") 349 | 350 | thread.set_state(CheckStablecoinBalanceState()) 351 | -------------------------------------------------------------------------------- /logic/state.py: -------------------------------------------------------------------------------- 1 | # State interface defining the behavior of different states 2 | class State: 3 | def handle(self, thread): 4 | pass 5 | 6 | 7 | class InitialState(State): 8 | def handle(self, thread): 9 | pass 10 | -------------------------------------------------------------------------------- /lz.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import random 4 | import sys 5 | import time 6 | from typing import Any 7 | 8 | from base.errors import NotWhitelistedAddress 9 | from logic import AccountThread 10 | from config import ConfigurationHelper, DEFAULT_PRIVATE_KEYS_FILE_PATH, BridgerMode, RefuelMode 11 | from logger import setup_logger 12 | from exchange import ExchangeFactory 13 | 14 | from utility import WalletHelper 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class LayerZeroBridger: 20 | 21 | def __init__(self) -> None: 22 | setup_logger() 23 | self.wh = WalletHelper() 24 | 25 | def main(self) -> None: 26 | parser = argparse.ArgumentParser(description="layerzero-bridger CLI") 27 | subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") 28 | 29 | self._create_generate_parser(subparsers) 30 | self._create_withdraw_parser(subparsers) 31 | self._create_run_bridger_parser(subparsers) 32 | 33 | args = parser.parse_args() 34 | if hasattr(args, "func"): 35 | args.func(args) 36 | else: 37 | parser.print_help() 38 | 39 | def generate_private_keys(self, args: argparse.Namespace) -> None: 40 | if args.num_keys <= 0: 41 | logger.info("Number of keys must be a positive integer") 42 | sys.exit(1) 43 | 44 | filename = args.filename if args.filename else "" 45 | private_keys = [] 46 | 47 | for _ in range(args.num_keys): 48 | pk = self.wh.generate_private_key() 49 | private_keys.append(pk) 50 | 51 | self.wh.to_txt(private_keys, filename) 52 | 53 | def withdraw_funds(self, args: argparse.Namespace) -> None: 54 | token = args.token.upper() 55 | network = args.network 56 | 57 | private_keys = self.wh.load_private_keys(args.private_keys) 58 | addresses = self.wh.resolve_addresses(private_keys) 59 | 60 | if not addresses: 61 | logger.info('You should specify at least 1 address for the withdrawal') 62 | sys.exit(1) 63 | 64 | exchange = ExchangeFactory.create(args.exchange) 65 | 66 | if not exchange.is_withdraw_supported(token, network): 67 | logger.info(f'{token} withdrawal on the {network} network is not available') 68 | sys.exit(1) 69 | 70 | for idx, address in enumerate(addresses): 71 | logger.info(f'Processing {idx}/{len(addresses)}') 72 | 73 | amount = random.uniform(args.min_amount, args.max_amount) 74 | decimals = random.randint(3, 6) # May be improved 75 | amount = round(amount, decimals) 76 | 77 | try: 78 | exchange.withdraw(token, amount, network, address) 79 | except NotWhitelistedAddress as ex: 80 | logger.info(str(ex)) 81 | sys.exit(1) 82 | 83 | if idx == len(addresses) - 1: 84 | logger.info('All withdrawals are successfully completed') 85 | sys.exit(0) 86 | 87 | waiting_time = random.uniform(args.min_time, args.max_time) 88 | logger.info(f'Waiting {round(waiting_time, 1)} minutes before the next withdrawal') 89 | time.sleep(waiting_time * 60) # Convert waiting time to seconds 90 | 91 | def run_bridger(self, args: argparse.Namespace) -> None: 92 | config = ConfigurationHelper() 93 | config.check_configuration() 94 | 95 | bridger_mode = BridgerMode(args.bridger_mode) 96 | refuel_mode = RefuelMode(args.refuel_mode) 97 | bridges_limit = args.limit 98 | 99 | private_keys = self.wh.load_private_keys(args.private_keys) 100 | 101 | if not private_keys: 102 | logger.info("Zero private keys was loaded") 103 | sys.exit(1) 104 | 105 | accounts = [] 106 | 107 | for account_id, private_key in enumerate(private_keys): 108 | accounts.append(AccountThread(account_id, private_key, bridger_mode, refuel_mode, bridges_limit)) 109 | accounts[account_id].start() 110 | 111 | for account in accounts: 112 | account.join() 113 | 114 | def _create_generate_parser(self, subparsers: Any) -> None: 115 | generate_parser = subparsers.add_parser("generate", help="Generate new private keys") 116 | 117 | generate_parser.add_argument("num_keys", type=int, help="Number of private keys to generate") 118 | generate_parser.add_argument("filename", nargs="?", help="Path to the file to save the private keys") 119 | 120 | generate_parser.set_defaults(func=self.generate_private_keys) 121 | 122 | def _create_withdraw_parser(self, subparsers: Any) -> None: 123 | withdraw_parser = subparsers.add_parser("withdraw", help="Withdraw funds from exchange to account addresses") 124 | 125 | withdraw_parser.add_argument("token", help="Token to be withdrawn") 126 | withdraw_parser.add_argument("network", choices=["Arbitrum", "Ethereum", "Optimism", "Polygon", 127 | "Fantom", "Avalanche", "BSC"], 128 | help="Network for the withdrawal") 129 | 130 | # Amount 131 | withdraw_parser.add_argument("min_amount", type=float, help="Minimum amount of withdrawal") 132 | withdraw_parser.add_argument("max_amount", type=float, help="Maximum amount of withdrawal") 133 | 134 | # Waiting time 135 | withdraw_parser.add_argument("--min_time", type=float, default=0, dest="min_time", 136 | help="Minimum waiting time between withdraws in minutes") 137 | withdraw_parser.add_argument("--max_time", type=float, default=0, dest="max_time", 138 | help="Maximum waiting time between withdraws in minutes") 139 | 140 | withdraw_parser.add_argument("--keys", type=str, default=DEFAULT_PRIVATE_KEYS_FILE_PATH, 141 | dest="private_keys", 142 | help="Path to the file containing private keys of the account addresses") 143 | withdraw_parser.add_argument("--exchange", choices=["binance", "okex"], default="binance", dest='exchange', 144 | help="Exchange name (binance, okex)") 145 | 146 | withdraw_parser.set_defaults(func=self.withdraw_funds) 147 | 148 | def _create_run_bridger_parser(self, subparsers: Any) -> None: 149 | run_parser = subparsers.add_parser("run", help="Run the LayerZero bridger") 150 | run_parser.add_argument("bridger_mode", choices=["stargate", "btcb"], 151 | help="Running mode (stargate, btcb)") 152 | run_parser.add_argument("--keys", type=str, default=DEFAULT_PRIVATE_KEYS_FILE_PATH, dest="private_keys", 153 | help="Path to the file containing private keys") 154 | run_parser.add_argument("--refuel", choices=["manual", "binance", "okex"], default="manual", dest='refuel_mode', 155 | help="Refuel mode (manual, binance, okex)") 156 | run_parser.add_argument("--limit", type=int, help="Maximum number of bridges to be executed") 157 | 158 | run_parser.set_defaults(func=self.run_bridger) 159 | 160 | 161 | if __name__ == "__main__": 162 | app = LayerZeroBridger() 163 | app.main() 164 | -------------------------------------------------------------------------------- /network/__init__.py: -------------------------------------------------------------------------------- 1 | from network.arbitrum.arbitrum import Arbitrum 2 | from network.avalanche.avalanche import Avalanche 3 | from network.bsc.bsc import BSC 4 | from network.ethereum.ethereum import Ethereum 5 | from network.fantom.fantom import Fantom 6 | from network.optimism.optimism import Optimism 7 | from network.polygon.polygon import Polygon 8 | 9 | from network.network import Network, EVMNetwork, TransactionStatus 10 | -------------------------------------------------------------------------------- /network/arbitrum/arbitrum.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from network.arbitrum.constants import ArbitrumConstants 4 | from network.network import EVMNetwork 5 | from stargate import StargateConstants 6 | from utility import Stablecoin 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Arbitrum(EVMNetwork): 12 | 13 | def __init__(self): 14 | supported_stablecoins = { 15 | 'USDT': Stablecoin('USDT', ArbitrumConstants.USDT_CONTRACT_ADDRESS, ArbitrumConstants.USDT_DECIMALS, 16 | ArbitrumConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDT']), 17 | 'USDC': Stablecoin('USDC', ArbitrumConstants.USDC_CONTRACT_ADDRESS, ArbitrumConstants.USDC_DECIMALS, 18 | ArbitrumConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDC']) 19 | } 20 | 21 | super().__init__(ArbitrumConstants.NAME, ArbitrumConstants.NATIVE_TOKEN, ArbitrumConstants.RPC, 22 | ArbitrumConstants.LAYERZERO_CHAIN_ID, ArbitrumConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 23 | supported_stablecoins) 24 | 25 | def get_approve_gas_limit(self) -> int: 26 | return ArbitrumConstants.APPROVE_GAS_LIMIT 27 | 28 | def get_max_fee_per_gas(self) -> int: 29 | # Fixed value 30 | return 135000000 31 | 32 | def get_transaction_gas_params(self) -> dict: 33 | gas_params = { 34 | 'maxFeePerGas': self.get_max_fee_per_gas(), 35 | 'maxPriorityFeePerGas': 0 36 | } 37 | 38 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 39 | 40 | return gas_params 41 | -------------------------------------------------------------------------------- /network/arbitrum/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class ArbitrumConstants: 8 | NAME = "Arbitrum" 9 | NATIVE_TOKEN = "ETH" 10 | RPC = os.getenv("ARBITRUM_RPC") 11 | CHAIN_ID = 42161 12 | LAYERZERO_CHAIN_ID = 110 13 | 14 | # Contracts 15 | USDC_CONTRACT_ADDRESS = "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8" 16 | USDC_DECIMALS = 6 17 | 18 | USDT_CONTRACT_ADDRESS = "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" 19 | USDT_DECIMALS = 6 20 | 21 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0x53Bf833A5d6c4ddA888F69c22C88C9f356a41614" 22 | 23 | APPROVE_GAS_LIMIT = 1_500_000 24 | -------------------------------------------------------------------------------- /network/avalanche/avalanche.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from network.avalanche.constants import AvalancheConstants 5 | from network.network import EVMNetwork 6 | from stargate import StargateConstants 7 | from utility import Stablecoin 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Avalanche(EVMNetwork): 13 | 14 | def __init__(self): 15 | supported_stablecoins = { 16 | 'USDT': Stablecoin('USDT', AvalancheConstants.USDT_CONTRACT_ADDRESS, AvalancheConstants.USDT_DECIMALS, 17 | AvalancheConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDT']), 18 | 'USDC': Stablecoin('USDC', AvalancheConstants.USDC_CONTRACT_ADDRESS, AvalancheConstants.USDC_DECIMALS, 19 | AvalancheConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDC']) 20 | } 21 | 22 | super().__init__(AvalancheConstants.NAME, AvalancheConstants.NATIVE_TOKEN, AvalancheConstants.RPC, 23 | AvalancheConstants.LAYERZERO_CHAIN_ID, AvalancheConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 24 | supported_stablecoins) 25 | 26 | def get_approve_gas_limit(self) -> int: 27 | return AvalancheConstants.APPROVE_GAS_LIMIT 28 | 29 | def get_max_fee_per_gas(self) -> int: 30 | return int(self.get_current_gas() * 1.5) 31 | 32 | def get_transaction_gas_params(self) -> dict: 33 | max_fee_per_gas = self.get_max_fee_per_gas() 34 | mul = random.uniform(0.9, 1) 35 | max_fee_per_gas = int(max_fee_per_gas * mul) 36 | 37 | gas_params = { 38 | 'maxFeePerGas': max_fee_per_gas, 39 | 'maxPriorityFeePerGas': 1500000000 40 | } 41 | 42 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 43 | 44 | return gas_params 45 | -------------------------------------------------------------------------------- /network/avalanche/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class AvalancheConstants: 8 | NAME = "Avalanche" 9 | NATIVE_TOKEN = "AVAX" 10 | RPC = os.getenv("AVALANCHE_RPC") 11 | CHAIN_ID = 43114 12 | LAYERZERO_CHAIN_ID = 106 13 | 14 | # Contracts 15 | USDC_CONTRACT_ADDRESS = "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" 16 | USDC_DECIMALS = 6 17 | 18 | USDT_CONTRACT_ADDRESS = "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7" 19 | USDT_DECIMALS = 6 20 | 21 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0x45A01E4e04F14f7A4a6702c74187c5F6222033cd" 22 | 23 | APPROVE_GAS_LIMIT = 100_000 24 | -------------------------------------------------------------------------------- /network/balance_helper.py: -------------------------------------------------------------------------------- 1 | from base.errors import StablecoinNotSupportedByChain 2 | from network.network import EVMNetwork 3 | from utility import Stablecoin 4 | 5 | 6 | class BalanceHelper: 7 | def __init__(self, network: EVMNetwork, address: str): 8 | self.network = network 9 | self.address = address 10 | 11 | def get_native_token_balance(self) -> int: 12 | return self.network.get_balance(self.address) 13 | 14 | def get_stablecoin_balance(self, stablecoin: Stablecoin) -> int: 15 | if stablecoin.symbol not in self.network.supported_stablecoins: 16 | raise StablecoinNotSupportedByChain(f"{stablecoin.symbol} is not supported by {self.network.name}") 17 | 18 | return self.network.get_token_balance(stablecoin.contract_address, self.address) 19 | -------------------------------------------------------------------------------- /network/bsc/bsc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from network.bsc.constants import BSCConstants 4 | from network.network import EVMNetwork 5 | from stargate import StargateConstants 6 | from utility import Stablecoin 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class BSC(EVMNetwork): 12 | 13 | def __init__(self): 14 | supported_stablecoins = { 15 | 'USDT': Stablecoin('USDT', BSCConstants.USDT_CONTRACT_ADDRESS, BSCConstants.USDT_DECIMALS, 16 | BSCConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDT']), 17 | 'BUSD': Stablecoin('BUSD', BSCConstants.BUSD_CONTRACT_ADDRESS, BSCConstants.BUSD_DECIMALS, 18 | BSCConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['BUSD']) 19 | } 20 | 21 | super().__init__(BSCConstants.NAME, BSCConstants.NATIVE_TOKEN, BSCConstants.RPC, 22 | BSCConstants.LAYERZERO_CHAIN_ID, BSCConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 23 | supported_stablecoins) 24 | 25 | def get_approve_gas_limit(self) -> int: 26 | return BSCConstants.APPROVE_GAS_LIMIT 27 | 28 | def get_transaction_gas_params(self) -> dict: 29 | gas_params = { 30 | 'gasPrice': self.get_current_gas() 31 | } 32 | 33 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 34 | 35 | return gas_params 36 | -------------------------------------------------------------------------------- /network/bsc/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class BSCConstants: 8 | NAME = "BSC" 9 | NATIVE_TOKEN = "BNB" 10 | RPC = os.getenv("BSC_RPC") 11 | CHAIN_ID = 56 12 | LAYERZERO_CHAIN_ID = 102 13 | 14 | # Contracts 15 | BUSD_CONTRACT_ADDRESS = "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56" 16 | BUSD_DECIMALS = 18 17 | 18 | USDT_CONTRACT_ADDRESS = "0x55d398326f99059fF775485246999027B3197955" 19 | USDT_DECIMALS = 18 20 | 21 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0x4a364f8c717cAAD9A442737Eb7b8A55cc6cf18D8" 22 | 23 | APPROVE_GAS_LIMIT = 100_000 24 | -------------------------------------------------------------------------------- /network/ethereum/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class EthereumConstants: 8 | NAME = "Ethereum" 9 | NATIVE_TOKEN = "ETH" 10 | RPC = os.getenv("ETHEREUM_RPC") 11 | CHAIN_ID = 1 12 | LAYERZERO_CHAIN_ID = 101 13 | 14 | # Contracts 15 | USDC_CONTRACT_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 16 | USDC_DECIMALS = 6 17 | 18 | USDT_CONTRACT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" 19 | USDT_DECIMALS = 6 20 | 21 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0x8731d54E9D02c286767d56ac03e8037C07e01e98" 22 | 23 | APPROVE_GAS_LIMIT = 100_000 24 | -------------------------------------------------------------------------------- /network/ethereum/ethereum.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from network.ethereum.constants import EthereumConstants 5 | from network.network import EVMNetwork 6 | from stargate import StargateConstants 7 | from utility import Stablecoin 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Ethereum(EVMNetwork): 13 | 14 | def __init__(self): 15 | supported_stablecoins = { 16 | 'USDT': Stablecoin('USDT', EthereumConstants.USDT_CONTRACT_ADDRESS, EthereumConstants.USDT_DECIMALS, 17 | EthereumConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDT']), 18 | 'USDC': Stablecoin('USDC', EthereumConstants.USDC_CONTRACT_ADDRESS, EthereumConstants.USDC_DECIMALS, 19 | EthereumConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDC']) 20 | } 21 | 22 | super().__init__(EthereumConstants.NAME, EthereumConstants.NATIVE_TOKEN, EthereumConstants.RPC, 23 | EthereumConstants.LAYERZERO_CHAIN_ID, EthereumConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 24 | supported_stablecoins) 25 | 26 | def get_approve_gas_limit(self) -> int: 27 | return EthereumConstants.APPROVE_GAS_LIMIT 28 | 29 | def get_max_fee_per_gas(self) -> int: 30 | return int(self.get_current_gas() * 1.2) 31 | 32 | def get_transaction_gas_params(self) -> dict: 33 | max_priority_fee = int(self.w3.eth.max_priority_fee * random.uniform(2, 3)) 34 | 35 | gas_params = { 36 | 'maxFeePerGas': self.get_max_fee_per_gas(), 37 | 'maxPriorityFeePerGas': max_priority_fee 38 | } 39 | 40 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 41 | 42 | return gas_params 43 | -------------------------------------------------------------------------------- /network/fantom/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class FantomConstants: 8 | NAME = "Fantom" 9 | NATIVE_TOKEN = "FRM" 10 | RPC = os.getenv("FANTOM_RPC") 11 | CHAIN_ID = 250 12 | LAYERZERO_CHAIN_ID = 112 13 | 14 | # Contracts 15 | USDC_CONTRACT_ADDRESS = "0x04068DA6C83AFCFA0e13ba15A6696662335D5B75" 16 | USDC_DECIMALS = 6 17 | 18 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" 19 | 20 | APPROVE_GAS_LIMIT = 100_000 21 | -------------------------------------------------------------------------------- /network/fantom/fantom.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from network.fantom.constants import FantomConstants 4 | from network.network import EVMNetwork 5 | from stargate import StargateConstants 6 | from utility import Stablecoin 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Fantom(EVMNetwork): 12 | 13 | def __init__(self): 14 | supported_stablecoins = { 15 | 'USDC': Stablecoin('USDC', FantomConstants.USDC_CONTRACT_ADDRESS, FantomConstants.USDC_DECIMALS, 16 | FantomConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDC']) 17 | } 18 | 19 | super().__init__(FantomConstants.NAME, FantomConstants.NATIVE_TOKEN, FantomConstants.RPC, 20 | FantomConstants.LAYERZERO_CHAIN_ID, FantomConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 21 | supported_stablecoins) 22 | 23 | def get_approve_gas_limit(self) -> int: 24 | return FantomConstants.APPROVE_GAS_LIMIT 25 | 26 | def get_transaction_gas_params(self) -> dict: 27 | gas_params = { 28 | 'gasPrice': self.get_current_gas() 29 | } 30 | 31 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 32 | 33 | return gas_params 34 | -------------------------------------------------------------------------------- /network/network.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import time 4 | from enum import Enum 5 | from typing import Dict, Union 6 | 7 | import requests 8 | from eth_typing import Hash32, HexStr 9 | from hexbytes import HexBytes 10 | from web3 import HTTPProvider, Web3 11 | from web3.exceptions import TransactionNotFound 12 | from web3.types import TxParams 13 | 14 | from abi import ERC20_ABI 15 | from base.errors import NotSupported 16 | from utility import Stablecoin 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class TransactionStatus(Enum): 22 | NOT_FOUND = 0 23 | SUCCESS = 1 24 | FAILED = 2 25 | 26 | 27 | class Network: 28 | 29 | def __init__(self, name: str, native_token: str, rpc: str, layerzero_chain_id: int, 30 | stargate_router_address: str) -> None: 31 | self.name = name 32 | self.native_token = native_token 33 | self.rpc = rpc 34 | self.layerzero_chain_id = layerzero_chain_id 35 | self.stargate_router_address = stargate_router_address 36 | 37 | def get_balance(self, address: str) -> int: 38 | """ Method that checks native token balance """ 39 | raise NotSupported(f"{self.name} get_balance() is not implemented") 40 | 41 | def get_token_balance(self, contract_address: str, address: str) -> int: 42 | """ Method that checks ERC-20 token balance """ 43 | raise NotSupported(f"{self.name} get_token_balance() is not implemented") 44 | 45 | def get_token_allowance(self, contract_address: str, owner: str, spender: str) -> int: 46 | """ Method that checks ERC-20 token allowance """ 47 | raise NotSupported(f"{self.name} get_token_allowance() is not implemented") 48 | 49 | def get_current_gas(self) -> int: 50 | """ Method that checks network gas price """ 51 | raise NotSupported(f"{self.name} get_current_gas() is not implemented") 52 | 53 | def get_nonce(self, address: str) -> int: 54 | """ Method that fetches account nonce """ 55 | raise NotSupported(f"{self.name} get_nonce() is not implemented") 56 | 57 | 58 | class EVMNetwork(Network): 59 | 60 | def __init__(self, name: str, native_token: str, rpc: str, 61 | layerzero_chain_id: int, stargate_router_address: str, 62 | supported_stablecoins: Dict[str, Stablecoin]) -> None: 63 | super().__init__(name, native_token, rpc, layerzero_chain_id, stargate_router_address) 64 | self.w3 = Web3(HTTPProvider(rpc)) 65 | self.supported_stablecoins = supported_stablecoins 66 | 67 | def get_balance(self, address: str) -> int: 68 | """ Method that checks native token balance """ 69 | 70 | return self.w3.eth.get_balance(Web3.to_checksum_address(address)) 71 | 72 | def get_current_gas(self) -> int: 73 | """ Method that checks network gas price """ 74 | 75 | return self.w3.eth.gas_price 76 | 77 | def get_max_fee_per_gas(self) -> int: 78 | """ Method that returns maxFeePerGas param for EIP-1559 networks and gasPrice for others """ 79 | 80 | return self.get_current_gas() 81 | 82 | def get_transaction_gas_params(self) -> dict: 83 | """ Method that returns formatted gas params to be added to build_transaction """ 84 | 85 | raise NotSupported(f"{self.name} get_transaction_gas_params() is not implemented") 86 | 87 | def get_nonce(self, address: str) -> int: 88 | """ Method that fetches account nonce """ 89 | 90 | return self.w3.eth.get_transaction_count(Web3.to_checksum_address(address)) 91 | 92 | @staticmethod 93 | def check_tx_result(result: TransactionStatus, name: str) -> bool: 94 | """ Utility method that checks transaction result and returns false if it's not mined or failed """ 95 | 96 | if result == TransactionStatus.SUCCESS: 97 | logger.info(f"{name} transaction succeed") 98 | return True 99 | if result == TransactionStatus.NOT_FOUND: 100 | logger.info(f"{name} transaction can't be found in the blockchain" 101 | " for a log time. Consider changing fee settings") 102 | return False 103 | if result == TransactionStatus.FAILED: 104 | logger.info(f"{name} transaction failed") 105 | return False 106 | 107 | return False 108 | 109 | def wait_for_transaction(self, tx_hash: Union[Hash32, HexBytes, HexStr], timeout: int = 300) -> TransactionStatus: 110 | start_time = time.time() 111 | 112 | logger.info(f'Waiting for transaction {tx_hash.hex()} to be mined') 113 | while True: 114 | try: 115 | tx_receipt = self.w3.eth.get_transaction_receipt(tx_hash) 116 | except TransactionNotFound: 117 | pass 118 | except requests.exceptions.HTTPError: 119 | time.sleep(10) 120 | else: 121 | if tx_receipt is not None: 122 | if tx_receipt["status"]: 123 | logger.info("Transaction mined successfully! Status: Success") 124 | return TransactionStatus.SUCCESS 125 | else: 126 | logger.info("Transaction mined successfully! Status: Failed") 127 | return TransactionStatus.FAILED 128 | 129 | if time.time() - start_time >= timeout: 130 | logger.info("Timeout reached. Transaction not mined within the specified time") 131 | return TransactionStatus.NOT_FOUND 132 | 133 | time.sleep(10) # Wait for 10 seconds before checking again 134 | 135 | # MARK: ERC-20 Token functions 136 | 137 | def get_token_balance(self, contract_address: str, address: str) -> int: 138 | """ Method that checks ERC-20 token balance """ 139 | 140 | contract = self.w3.eth.contract(address=Web3.to_checksum_address(contract_address), abi=ERC20_ABI) 141 | return contract.functions.balanceOf(Web3.to_checksum_address(address)).call() 142 | 143 | def get_token_allowance(self, contract_address: str, owner: str, spender: str) -> int: 144 | """ Method that checks ERC-20 token allowance """ 145 | 146 | contract = self.w3.eth.contract(address=Web3.to_checksum_address(contract_address), abi=ERC20_ABI) 147 | return contract.functions.allowance(Web3.to_checksum_address(owner), 148 | Web3.to_checksum_address(spender)).call() 149 | 150 | def get_approve_gas_limit(self) -> int: 151 | raise NotSupported(f"{self.name} _get_approve_gas_limit() is not implemented") 152 | 153 | def _build_approve_transaction(self, address: str, contract_address: str, spender: str, amount: int) -> TxParams: 154 | contract = self.w3.eth.contract(address=Web3.to_checksum_address(contract_address), abi=ERC20_ABI) 155 | 156 | randomized_gas_limit = random.randint(int(self.get_approve_gas_limit() * 0.95), self.get_approve_gas_limit()) 157 | gas_params = self.get_transaction_gas_params() 158 | 159 | tx = contract.functions.approve(spender, amount).build_transaction( 160 | { 161 | 'from': address, 162 | 'gas': randomized_gas_limit, 163 | **gas_params, 164 | 'nonce': self.get_nonce(address) 165 | } 166 | ) 167 | 168 | return tx 169 | 170 | def approve_token_usage(self, private_key: str, contract_address: str, spender: str, amount: int) -> HexBytes: 171 | """ Method that approves token usage by spender address and returns transaction hash """ 172 | 173 | account = self.w3.eth.account.from_key(private_key) 174 | tx = self._build_approve_transaction(account.address, contract_address, spender, amount) 175 | 176 | signed_tx = self.w3.eth.account.sign_transaction(tx, private_key) 177 | tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) 178 | 179 | return tx_hash 180 | -------------------------------------------------------------------------------- /network/optimism/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class OptimismConstants: 8 | NAME = "Optimism" 9 | NATIVE_TOKEN = "ETH" 10 | RPC = os.getenv("OPTIMISM_RPC") 11 | CHAIN_ID = 10 12 | LAYERZERO_CHAIN_ID = 111 13 | 14 | # Contracts 15 | GAS_ORACLE_CONTRACT_ADDRESS = "0x420000000000000000000000000000000000000F" 16 | 17 | USDC_CONTRACT_ADDRESS = "0x7F5c764cBc14f9669B88837ca1490cCa17c31607" 18 | USDC_DECIMALS = 6 19 | 20 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0xB0D502E938ed5f4df2E681fE6E419ff29631d62b" 21 | 22 | APPROVE_GAS_LIMIT = 100_000 23 | -------------------------------------------------------------------------------- /network/optimism/optimism.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from web3.types import TxParams 4 | 5 | from network.network import EVMNetwork 6 | from network.optimism.constants import OptimismConstants 7 | from stargate import StargateConstants 8 | from utility import Stablecoin 9 | from abi import OPTIMISM_GAS_ORACLE_ABI 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Optimism(EVMNetwork): 15 | 16 | def __init__(self): 17 | supported_stablecoins = { 18 | 'USDC': Stablecoin('USDC', OptimismConstants.USDC_CONTRACT_ADDRESS, OptimismConstants.USDC_DECIMALS, 19 | OptimismConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDC']) 20 | } 21 | 22 | super().__init__(OptimismConstants.NAME, OptimismConstants.NATIVE_TOKEN, OptimismConstants.RPC, 23 | OptimismConstants.LAYERZERO_CHAIN_ID, OptimismConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 24 | supported_stablecoins) 25 | 26 | def get_approve_gas_limit(self) -> int: 27 | return OptimismConstants.APPROVE_GAS_LIMIT 28 | 29 | def get_transaction_gas_params(self) -> dict: 30 | gas_params = { 31 | 'gasPrice': self.get_current_gas() 32 | } 33 | 34 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 35 | 36 | return gas_params 37 | 38 | def get_l1_fee(self, tx_params: TxParams) -> int: 39 | oracle = self.w3.eth.contract(address=OptimismConstants.GAS_ORACLE_CONTRACT_ADDRESS, 40 | abi=OPTIMISM_GAS_ORACLE_ABI) 41 | 42 | gas = oracle.functions.getL1Fee(tx_params['data']).call() 43 | 44 | return gas 45 | 46 | def get_approve_l1_fee(self): 47 | # Almost doesn't matter in fee calculation 48 | addr = "0x0000000000000000000000000000000000000000" 49 | amount = 10 50 | 51 | approve_tx = self._build_approve_transaction(addr, addr, addr, amount) 52 | 53 | return self.get_l1_fee(approve_tx) 54 | -------------------------------------------------------------------------------- /network/polygon/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | class PolygonConstants: 8 | NAME = "Polygon" 9 | NATIVE_TOKEN = "MATIC" 10 | RPC = os.getenv("POLYGON_RPC") 11 | CHAIN_ID = 137 12 | LAYERZERO_CHAIN_ID = 109 13 | 14 | # Contracts 15 | USDC_CONTRACT_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" 16 | USDC_DECIMALS = 6 17 | 18 | USDT_CONTRACT_ADDRESS = "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" 19 | USDT_DECIMALS = 6 20 | 21 | STARGATE_ROUTER_CONTRACT_ADDRESS = "0x45A01E4e04F14f7A4a6702c74187c5F6222033cd" 22 | 23 | APPROVE_GAS_LIMIT = 100_000 24 | -------------------------------------------------------------------------------- /network/polygon/polygon.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from network.network import EVMNetwork 5 | from network.polygon.constants import PolygonConstants 6 | from stargate import StargateConstants 7 | from utility import Stablecoin 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Polygon(EVMNetwork): 13 | 14 | def __init__(self): 15 | supported_stablecoins = { 16 | 'USDT': Stablecoin('USDT', PolygonConstants.USDT_CONTRACT_ADDRESS, PolygonConstants.USDC_DECIMALS, 17 | PolygonConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDT']), 18 | 'USDC': Stablecoin('USDC', PolygonConstants.USDC_CONTRACT_ADDRESS, PolygonConstants.USDC_DECIMALS, 19 | PolygonConstants.LAYERZERO_CHAIN_ID, StargateConstants.POOLS['USDC']) 20 | } 21 | 22 | super().__init__(PolygonConstants.NAME, PolygonConstants.NATIVE_TOKEN, PolygonConstants.RPC, 23 | PolygonConstants.LAYERZERO_CHAIN_ID, PolygonConstants.STARGATE_ROUTER_CONTRACT_ADDRESS, 24 | supported_stablecoins) 25 | 26 | def get_approve_gas_limit(self) -> int: 27 | return PolygonConstants.APPROVE_GAS_LIMIT 28 | 29 | def get_max_fee_per_gas(self) -> int: 30 | return int(self.get_current_gas() * 2.5) 31 | 32 | def get_transaction_gas_params(self) -> dict: 33 | max_priority_fee = int(self.w3.eth.max_priority_fee * random.uniform(1, 2)) 34 | 35 | gas_params = { 36 | 'maxFeePerGas': self.get_max_fee_per_gas(), 37 | 'maxPriorityFeePerGas': max_priority_fee 38 | } 39 | 40 | logger.debug(f"{self.name} gas params fetched. Params: {gas_params}") 41 | 42 | return gas_params 43 | -------------------------------------------------------------------------------- /private_keys.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cppmyk/layerzero-bridger/4fd7ab04739eac56656610aad96749c66529d038/private_keys.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | web3==6.4.0 2 | requests==2.31.0 3 | ccxt==3.1.19 4 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /stargate/__init__.py: -------------------------------------------------------------------------------- 1 | from stargate.constants import StargateConstants 2 | 3 | from stargate.stargate import StargateUtils, StargateBridgeHelper 4 | -------------------------------------------------------------------------------- /stargate/constants.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from base.errors import NotSupported 4 | 5 | 6 | class StargateConstants: 7 | SWAP_GAS_LIMIT = { 8 | 'Ethereum': (580_000, 630_000), 9 | 'Arbitrum': (3_000_000, 4_000_000), 10 | 'Optimism': (700_000, 900_000), 11 | 'Fantom': (800_000, 1_000_000), 12 | 'Polygon': (580_000, 630_000), 13 | 'BSC': (560_000, 600_000), 14 | 'Avalanche': (580_000, 700_000) 15 | } 16 | 17 | POOLS = { 18 | "USDC": 1, 19 | "USDT": 2, 20 | "DAI": 3, 21 | "BUSD": 5, 22 | "FRAX": 7, 23 | "USDD": 11, 24 | "ETH": 13, 25 | } 26 | 27 | @staticmethod 28 | def get_max_randomized_swap_gas_limit(network_name: str) -> int: 29 | return StargateConstants.SWAP_GAS_LIMIT[network_name][1] 30 | 31 | @staticmethod 32 | def get_randomized_swap_gas_limit(network_name: str) -> int: 33 | if network_name not in StargateConstants.SWAP_GAS_LIMIT: 34 | raise NotSupported(f"{network_name} isn't supported by get_randomized_swap_gas_limit()") 35 | 36 | return random.randint(StargateConstants.SWAP_GAS_LIMIT[network_name][0], 37 | StargateConstants.SWAP_GAS_LIMIT[network_name][1]) 38 | -------------------------------------------------------------------------------- /stargate/stargate.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import time 4 | 5 | from hexbytes import HexBytes 6 | from web3 import Web3 7 | from web3.types import TxParams 8 | 9 | from abi import STARGATE_ROUTER_ABI 10 | from network.network import EVMNetwork 11 | from network.optimism.optimism import Optimism 12 | from utility import Stablecoin 13 | 14 | from stargate.constants import StargateConstants 15 | from eth_account.signers.local import LocalAccount 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class StargateUtils: 21 | @staticmethod 22 | def estimate_layerzero_swap_fee(src_network: EVMNetwork, dst_network: EVMNetwork, dst_address: str) -> int: 23 | """ Method that estimates LayerZero fee to make the swap in native token """ 24 | 25 | contract = src_network.w3.eth.contract( 26 | address=Web3.to_checksum_address(src_network.stargate_router_address), 27 | abi=STARGATE_ROUTER_ABI) 28 | 29 | quote_data = contract.functions.quoteLayerZeroFee( 30 | dst_network.layerzero_chain_id, # destination chainId 31 | 1, # function type (1 - swap): see Bridge.sol for all types 32 | dst_address, # destination of tokens 33 | "0x", # payload, using abi.encode() 34 | [0, # extra gas, if calling smart contract 35 | 0, # amount of dust dropped in destination wallet 36 | "0x" # destination wallet for dust 37 | ] 38 | ).call() 39 | 40 | return quote_data[0] 41 | 42 | @staticmethod 43 | def _get_optimism_swap_l1_fee(optimism: Optimism, dst_network: EVMNetwork, address: str) -> int: 44 | # Doesn't matter in fee calculation 45 | amount = 100 46 | slippage = 0.01 47 | _, src_stablecoin = random.choice(list(optimism.supported_stablecoins.items())) 48 | _, dst_stablecoin = random.choice(list(dst_network.supported_stablecoins.items())) 49 | 50 | swap_tx = StargateUtils.build_swap_transaction(address, optimism, dst_network, 51 | src_stablecoin, dst_stablecoin, amount, slippage) 52 | swap_l1_fee = optimism.get_l1_fee(swap_tx) 53 | approve_l1_fee = optimism.get_approve_l1_fee() 54 | 55 | l1_fee = swap_l1_fee + approve_l1_fee 56 | 57 | return l1_fee 58 | 59 | @staticmethod 60 | def estimate_swap_gas_price(src_network: EVMNetwork, dst_network: EVMNetwork, address: str) -> int: 61 | approve_gas_limit = src_network.get_approve_gas_limit() 62 | max_overall_gas_limit = StargateConstants.get_max_randomized_swap_gas_limit( 63 | src_network.name) + approve_gas_limit 64 | gas_price = max_overall_gas_limit * src_network.get_max_fee_per_gas() 65 | 66 | # Optimism fee should be calculated in a different way. 67 | # Read more: https://community.optimism.io/docs/developers/build/transaction-fees/# 68 | if isinstance(src_network, Optimism): 69 | gas_price += StargateUtils._get_optimism_swap_l1_fee(src_network, dst_network, address) 70 | 71 | return gas_price 72 | 73 | @staticmethod 74 | def is_enough_native_balance_for_swap_fee(src_network: EVMNetwork, dst_network: EVMNetwork, address: str) -> bool: 75 | account_balance = src_network.get_balance(address) 76 | gas_price = StargateUtils.estimate_swap_gas_price(src_network, dst_network, address) 77 | layerzero_fee = StargateUtils.estimate_layerzero_swap_fee(src_network, dst_network, address) 78 | 79 | enough_native_token_balance = account_balance > (gas_price + layerzero_fee) 80 | 81 | return enough_native_token_balance 82 | 83 | @staticmethod 84 | def build_swap_transaction(address: str, src_network: EVMNetwork, dst_network: EVMNetwork, 85 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin, 86 | amount: int, slippage: float) -> TxParams: 87 | contract = src_network.w3.eth.contract( 88 | address=Web3.to_checksum_address(src_network.stargate_router_address), 89 | abi=STARGATE_ROUTER_ABI) 90 | 91 | layerzero_fee = StargateUtils.estimate_layerzero_swap_fee(src_network, dst_network, address) 92 | nonce = src_network.get_nonce(address) 93 | gas_params = src_network.get_transaction_gas_params() 94 | amount_with_slippage = amount - int(amount * slippage) 95 | 96 | logger.info(f'Estimated fees. LayerZero fee: {layerzero_fee}. Gas settings: {gas_params}') 97 | tx = contract.functions.swap( 98 | dst_network.layerzero_chain_id, # destination chainId 99 | src_stablecoin.stargate_pool_id, # source poolId 100 | dst_stablecoin.stargate_pool_id, # destination poolId 101 | address, # refund address. extra gas (if any) is returned to this address 102 | amount, # quantity to swap 103 | amount_with_slippage, # the min qty you would accept on the destination 104 | [0, # extra gas, if calling smart contract 105 | 0, # amount of dust dropped in destination wallet 106 | "0x0000000000000000000000000000000000000001" # destination wallet for dust 107 | ], 108 | address, # the address to send the tokens to on the destination 109 | "0x", # "fee" is the native gas to pay for the cross chain message fee 110 | ).build_transaction( 111 | { 112 | 'from': address, 113 | 'value': layerzero_fee, 114 | 'gas': StargateConstants.get_randomized_swap_gas_limit(src_network.name), 115 | **gas_params, 116 | 'nonce': nonce 117 | } 118 | ) 119 | 120 | return tx 121 | 122 | 123 | class StargateBridgeHelper: 124 | 125 | def __init__(self, account: LocalAccount, src_network: EVMNetwork, dst_network: EVMNetwork, 126 | src_stablecoin: Stablecoin, dst_stablecoin: Stablecoin, amount: int, slippage: float): 127 | self.account = account 128 | self.src_network = src_network 129 | self.dst_network = dst_network 130 | self.src_stablecoin = src_stablecoin 131 | self.dst_stablecoin = dst_stablecoin 132 | self.amount = amount 133 | self.slippage = slippage 134 | 135 | def make_bridge(self) -> bool: 136 | """ Method that performs bridge from src_network to dst_network """ 137 | 138 | if not self._is_bridge_possible(): 139 | return False 140 | 141 | if not self._approve_stablecoin_usage(self.amount): 142 | return False 143 | 144 | # Wait for a blockchain sync to fix 'nonce too low' 145 | time.sleep(random.randint(10, 60)) 146 | 147 | tx_hash = self._send_swap_transaction() 148 | result = self.src_network.wait_for_transaction(tx_hash) 149 | 150 | return self.src_network.check_tx_result(result, "Stargate swap") 151 | 152 | def _send_swap_transaction(self) -> HexBytes: 153 | """ Utility method that signs and sends tx - Swap src_pool_id token from src_network chain to dst_chain_id """ 154 | 155 | tx = StargateUtils.build_swap_transaction(self.account.address, self.src_network, self.dst_network, 156 | self.src_stablecoin, self.dst_stablecoin, self.amount, self.slippage) 157 | signed_tx = self.src_network.w3.eth.account.sign_transaction(tx, self.account.key) 158 | tx_hash = self.src_network.w3.eth.send_raw_transaction(signed_tx.rawTransaction) 159 | 160 | logger.info(f'Stargate swap transaction signed and sent. Hash: {tx_hash.hex()}') 161 | 162 | return tx_hash 163 | 164 | def _is_bridge_possible(self) -> bool: 165 | """ Method that checks account balance on the source chain and decides if it is possible to make bridge """ 166 | 167 | if not StargateUtils.is_enough_native_balance_for_swap_fee(self.src_network, self.dst_network, 168 | self.account.address): 169 | return False 170 | 171 | stablecoin_balance = self.src_network.get_token_balance(self.src_stablecoin.contract_address, 172 | self.account.address) 173 | if stablecoin_balance < self.amount: 174 | return False 175 | 176 | return True 177 | 178 | def _approve_stablecoin_usage(self, amount: int) -> bool: 179 | allowance = self.src_network.get_token_allowance(self.src_stablecoin.contract_address, self.account.address, 180 | self.src_network.stargate_router_address) 181 | if allowance >= amount: 182 | return True 183 | 184 | logger.debug(f'Approving {self.src_stablecoin.symbol} usage to perform Stargate bridge') 185 | 186 | tx_hash = self.src_network.approve_token_usage(self.account.key, self.src_stablecoin.contract_address, 187 | self.src_network.stargate_router_address, amount) 188 | result = self.src_network.wait_for_transaction(tx_hash) 189 | 190 | return self.src_network.check_tx_result(result, f"Approve {self.src_stablecoin.symbol} usage") 191 | -------------------------------------------------------------------------------- /utility/__init__.py: -------------------------------------------------------------------------------- 1 | from utility.stablecoin import Stablecoin 2 | from utility.wallet import WalletHelper 3 | -------------------------------------------------------------------------------- /utility/stablecoin.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Stablecoin: 6 | symbol: str 7 | contract_address: str 8 | decimals: int 9 | layerzero_chain_id: int 10 | stargate_pool_id: int 11 | -------------------------------------------------------------------------------- /utility/wallet.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from typing import List 4 | 5 | from eth_account import Account 6 | 7 | 8 | class WalletHelper: 9 | 10 | def generate_private_key(self) -> str: 11 | account = Account.create() 12 | 13 | return account.key.hex()[2:] 14 | 15 | def resolve_address(self, private_key: str) -> str: 16 | account = Account.from_key(private_key) 17 | 18 | return account.address 19 | 20 | def resolve_addresses(self, private_keys: List[str]) -> List[str]: 21 | addresses = [] 22 | for key in private_keys: 23 | addresses.append(self.resolve_address(key)) 24 | 25 | return addresses 26 | 27 | def load_private_keys(self, file_path: str) -> List[str]: 28 | with open(file_path, 'r') as file: 29 | keys = file.read().splitlines() 30 | filtered = [s for s in keys if s] 31 | return filtered 32 | 33 | def _prepare_keys_directory(self) -> str: 34 | keys_dir = 'generated_keys' 35 | 36 | if not os.path.exists(keys_dir): 37 | os.makedirs(keys_dir) 38 | 39 | return keys_dir 40 | 41 | def to_txt(self, private_keys: List[str], filename: str = "") -> None: 42 | if not filename: 43 | keys_dir = self._prepare_keys_directory() 44 | current_date = datetime.now().strftime('%Y-%m-%d') 45 | current_time = datetime.now().strftime('%H-%M-%S') 46 | filename = f"{keys_dir}/private_keys_{current_date}_{current_time}.txt" 47 | 48 | with open(filename, 'a') as file: 49 | for key in private_keys: 50 | file.write(key + "\n") 51 | --------------------------------------------------------------------------------