diff --git a/README.md b/README.md index e89cbf1..3cb72a4 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ The object of this library is to decode & encode the transaction input sent to t on Ethereum Mainnet). It is based on, and is intended to be used with [web3.py](https://github.com/ethereum/web3.py) The target audience is Python developers who are familiar with the Ethereum blockchain concepts and web3.py, and how DEXes work. -⚠ This library has not been audited, so use at your own risk ! +⚠️ This library has not been audited, so use at your own risk ! -⚠ Before using this library, ensure you are familiar with general blockchain concepts and [web3.py](https://github.com/ethereum/web3.py) in particular. +⚠️ Before using this library, ensure you are familiar with general blockchain concepts and [web3.py](https://github.com/ethereum/web3.py) in particular. -⚠ This project is a work in progress so not all commands are decoded yet. Below the list of the already implemented ones. +⚠️ This project is a work in progress so not all commands are decoded yet. Below the list of the already implemented ones. | Command Id | Function Name | Decode | Encode | ---------- | ------------- |:------:|:------: @@ -67,7 +67,12 @@ The target audience is Python developers who are familiar with the Ethereum bloc | 0x0c | UNWRAP_WETH | ✅ | ✅ | 0x0d | PERMIT2_TRANSFER_FROM_BATCH | ❌ | ❌ | 0x0e - 0x0f | placeholders | N/A | N/A -| 0x10 - 0x1d | | ❌ | ❌ +| 0x10 | SEAPORT_V1_5 | ✅ | ✅ +| 0x11 - 0x14 | | ❌ | ❌ +| 0x15 | OWNER_CHECK_721 | ✅ | ✅ +| 0x16 | OWNER_CHECK_1155 | ❌ | ❌ +| 0x17 | SWEEP_ERC721 | ✅ | ✅ +| 0x18 - 0x1d | | ❌ | ❌ | 0x1e - 0x3f | placeholders | N/A | N/A --- @@ -398,6 +403,27 @@ Example where an USDC amount is sent to a recipient: .transfer(FunctionRecipient.CUSTOM, usdc_address, usdc_amount, recipient_address) ``` +#### SEAPORT_V1_5, OWNER_CHECK_721 and SWEEP_ERC721 +`SEAPORT_V1_5` encodes the call to the function SEAPORT_V1_5 which allows interacting with the OpenSea protocol, +ie: buy, sell, ... NFTs. +As the Universal Router is, in this case, mostly a gateway for Seaport, +building the `call_data` is not managed by this SDK and thus left to the users. + +⚠️ Important when buying a NFT: +1. chain `sweep_erc721()` after `seaport_v1_5()` to get the NFT. +2. chain `owner_check_721()` after `sweep_erc721()` to confirm the new owner. +```python +encoded_input = ( + codec + .encode + .chain() + .seaport_v1_5(value, call_data) # buy nft for value ETH (in Wei) with the call_data for Seaport + .sweep_erc721(FunctionRecipient.SENDER, nft_address, nft_id) # get nft + .owner_check_721(sender_address, nft_address, nft_id) # you want to be sure you're the new owner otherwise revert the trx + .build() + ) +``` + ### How to build directly a transaction The SDK provides a handy method to build very easily the full transaction in addition to the input data. It can compute most of the transaction parameters (if the codec has been instantiated with a valid w3 or rpc url) diff --git a/coverage.json b/coverage.json index 60f560e..04557b3 100644 --- a/coverage.json +++ b/coverage.json @@ -1 +1 @@ -{"meta": {"format": 2, "version": "7.4.1", "timestamp": "2024-05-16T09:59:53.915249", "branch_coverage": false, "show_contexts": false}, "files": {"uniswap_universal_router_decoder/__init__.py": {"executed_lines": [1, 5, 8], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_abi_builder.py": {"executed_lines": [1, 8, 10, 14, 21, 23, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 38, 39, 42, 43, 44, 45, 48, 51, 52, 53, 55, 56, 57, 59, 60, 61, 63, 65, 66, 67, 69, 70, 71, 73, 74, 75, 77, 78, 79, 81, 82, 84, 85, 86, 88, 89, 90, 92, 93, 94, 97, 98, 99, 112, 114, 115, 116, 117, 118, 120, 121, 122, 123, 124, 126, 127, 128, 129, 130, 131, 132, 133, 135, 136, 137, 138, 140, 141, 142, 143, 144, 146, 147, 148, 149, 151, 152, 153, 154, 155, 157, 158, 159, 160, 161, 163, 164, 165, 166, 168, 169, 170, 171, 173, 174, 175, 176], "summary": {"covered_lines": 112, "num_statements": 112, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_constants.py": {"executed_lines": [1, 8, 13, 14, 17, 18, 19, 20, 21, 24, 25, 27], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_decoder.py": {"executed_lines": [1, 8, 9, 17, 18, 19, 26, 27, 28, 34, 35, 36, 37, 38, 40, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 62, 63, 64, 65, 67, 76, 77, 78, 79, 80, 82, 83, 85, 86, 95, 96, 97, 98, 99, 100, 101, 108, 110, 111, 113], "summary": {"covered_lines": 51, "num_statements": 51, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_encoder.py": {"executed_lines": [1, 8, 10, 21, 22, 23, 24, 25, 26, 27, 36, 37, 45, 51, 54, 57, 58, 59, 60, 61, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 77, 78, 79, 81, 85, 88, 89, 90, 91, 92, 93, 94, 96, 97, 100, 105, 106, 107, 109, 114, 115, 116, 118, 119, 120, 121, 123, 124, 125, 126, 128, 129, 130, 131, 132, 134, 148, 149, 150, 151, 153, 154, 155, 156, 157, 159, 173, 174, 175, 176, 178, 185, 186, 187, 188, 189, 191, 212, 213, 214, 225, 227, 246, 255, 262, 263, 264, 265, 266, 268, 289, 290, 291, 302, 304, 311, 312, 313, 314, 315, 316, 318, 340, 341, 342, 353, 355, 375, 384, 391, 392, 393, 394, 395, 396, 398, 420, 421, 422, 433, 435, 439, 444, 445, 446, 447, 448, 450, 463, 464, 472, 474, 475, 476, 477, 478, 480, 496, 497, 498, 507, 509, 510, 511, 512, 513, 515, 532, 537, 539, 540, 541, 550, 552, 553, 554, 555, 556, 558, 576, 577, 578, 587, 589, 596, 597, 598, 600, 601, 603, 646, 648, 649, 651, 652, 654, 655, 656, 658, 659, 661, 662, 664, 665, 666, 672, 684, 685, 686, 688, 690], "summary": {"covered_lines": 203, "num_statements": 203, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_enums.py": {"executed_lines": [1, 10, 12, 13, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 30, 31, 38, 39, 40, 43, 45, 46, 47, 48, 49, 52, 53, 54, 55, 56], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/router_codec.py": {"executed_lines": [1, 8, 9, 16, 20, 21, 26, 27, 31, 32, 35, 36, 37, 40, 41, 42, 43, 44, 45, 46, 48, 49, 50, 52, 53, 57, 59, 60, 64, 66, 67, 71, 73, 74, 106, 112, 117, 118, 119, 120, 121], "summary": {"covered_lines": 40, "num_statements": 40, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/utils.py": {"executed_lines": [1, 2, 8, 9, 15, 18, 26, 43, 44, 45, 51, 52, 54, 55, 57, 58, 60], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "totals": {"covered_lines": 466, "num_statements": 466, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}} \ No newline at end of file +{"meta": {"format": 2, "version": "7.4.1", "timestamp": "2024-05-24T18:21:40.570630", "branch_coverage": false, "show_contexts": false}, "files": {"uniswap_universal_router_decoder/__init__.py": {"executed_lines": [1, 5, 8], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_abi_builder.py": {"executed_lines": [1, 8, 10, 14, 21, 23, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 38, 39, 42, 43, 44, 45, 48, 51, 52, 53, 55, 56, 57, 59, 60, 61, 63, 65, 66, 67, 69, 70, 71, 73, 74, 75, 77, 78, 79, 81, 82, 84, 85, 86, 88, 89, 90, 92, 93, 94, 97, 98, 99, 115, 117, 118, 119, 120, 121, 123, 124, 125, 126, 127, 129, 130, 131, 132, 133, 134, 135, 136, 138, 139, 140, 141, 143, 144, 145, 146, 147, 149, 150, 151, 152, 154, 155, 156, 157, 158, 160, 161, 162, 163, 164, 166, 167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 179, 181, 182, 183, 184, 186, 187, 188, 189, 191, 192, 193, 194], "summary": {"covered_lines": 124, "num_statements": 124, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_constants.py": {"executed_lines": [1, 8, 13, 14, 17, 18, 19, 20, 21, 22, 25, 26, 28], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_decoder.py": {"executed_lines": [1, 8, 9, 17, 18, 19, 26, 27, 28, 34, 35, 36, 37, 38, 40, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 62, 63, 64, 65, 67, 76, 77, 78, 79, 80, 82, 83, 85, 86, 95, 96, 97, 98, 99, 100, 101, 108, 110, 111, 113], "summary": {"covered_lines": 51, "num_statements": 51, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_encoder.py": {"executed_lines": [1, 8, 10, 21, 22, 23, 24, 25, 26, 27, 37, 38, 46, 52, 55, 58, 59, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 78, 79, 80, 82, 86, 89, 90, 91, 92, 93, 94, 95, 97, 98, 101, 106, 107, 108, 110, 115, 116, 117, 119, 120, 121, 122, 124, 125, 126, 127, 129, 130, 131, 132, 133, 135, 149, 150, 151, 152, 154, 155, 156, 157, 158, 160, 174, 175, 176, 177, 179, 186, 187, 188, 189, 190, 192, 213, 214, 215, 226, 228, 247, 256, 263, 264, 265, 266, 267, 269, 290, 291, 292, 303, 305, 312, 313, 314, 315, 316, 317, 319, 341, 342, 343, 354, 356, 376, 385, 392, 393, 394, 395, 396, 397, 399, 421, 422, 423, 434, 436, 440, 445, 446, 447, 448, 449, 451, 464, 465, 473, 475, 476, 477, 478, 479, 481, 497, 498, 499, 508, 510, 511, 512, 513, 514, 516, 533, 538, 540, 541, 542, 551, 553, 554, 555, 556, 557, 559, 577, 578, 579, 588, 590, 591, 592, 593, 594, 596, 597, 598, 599, 607, 609, 614, 615, 616, 617, 619, 625, 626, 627, 636, 638, 643, 644, 645, 646, 648, 653, 654, 663, 665, 672, 673, 674, 676, 677, 679, 722, 724, 725, 727, 728, 730, 731, 732, 734, 735, 737, 738, 740, 741, 742, 748, 760, 761, 762, 764, 766], "summary": {"covered_lines": 232, "num_statements": 232, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/_enums.py": {"executed_lines": [1, 10, 12, 13, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 33, 34, 41, 42, 43, 46, 48, 49, 50, 51, 52, 55, 56, 57, 58, 59], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/router_codec.py": {"executed_lines": [1, 8, 9, 16, 20, 21, 28, 29, 35, 36, 39, 40, 41, 44, 45, 46, 47, 48, 49, 50, 52, 53, 54, 56, 57, 61, 63, 64, 68, 70, 71, 75, 77, 78, 110, 116, 121, 122, 123, 124, 125, 127, 148, 149, 150, 151], "summary": {"covered_lines": 45, "num_statements": 45, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "uniswap_universal_router_decoder/utils.py": {"executed_lines": [1, 2, 8, 9, 15, 18, 26, 43, 44, 45, 51, 52, 54, 55, 57, 58, 60], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "totals": {"covered_lines": 516, "num_statements": 516, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}} \ No newline at end of file diff --git a/integration_tests/test_opensea.py b/integration_tests/test_opensea.py new file mode 100644 index 0000000..1210b49 --- /dev/null +++ b/integration_tests/test_opensea.py @@ -0,0 +1,119 @@ +import os +import subprocess +import time + +from web3 import Web3 +from web3.types import Wei + +from uniswap_universal_router_decoder import ( + FunctionRecipient, + RouterCodec, +) + + +web3_provider = os.environ['WEB3_HTTP_PROVIDER_URL_ETHEREUM_MAINNET'] +w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) +chain_id = 1 +block_number = 19931244 + +wallet = Web3.to_checksum_address("0xa71023e8D2D9922a6A75AB732cFeF5Ab11C3854D") +wallet_init_amount = 44659750000000000 + +tor_address = Web3.to_checksum_address("0xc2f05fD9fE4cfE8f7395ddb220c84D3Ef240cF3D") +tor_abi = '[{"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"},{"internalType":"uint256","name":"maxTorSupply","type":"uint256"},{"internalType":"address","name":"mintTicketContractAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"PaymentReleased","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"MAX_MINTS_PER_TXN","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"emergencySetStartingIndexBlock","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"flipPreSaleState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"flipSaleState","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTokenSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTokensPerTicket","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintPrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"numberOfTokens","type":"uint256"}],"name":"mintTOR","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"numberOfTokens","type":"uint256"}],"name":"mintUsingTicket","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"preSaleIsActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"provenance","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"reservedAmount","type":"uint256"}],"name":"reserveMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"reservedAmount","type":"uint256"},{"internalType":"address","name":"mintAddress","type":"address"}],"name":"reserveMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"saleIsActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newBaseURI","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"maxTorSupply","type":"uint256"}],"name":"setMaxTokenSupply","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"maxTokensPerMintTicket","type":"uint256"}],"name":"setMaxTokensPerTicket","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newPrice","type":"uint256"}],"name":"setMintPrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"provenanceHash","type":"string"}],"name":"setProvenanceHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"setStartingIndex","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"mintTicketContractAddress","type":"address"}],"name":"setTicketContractAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"startingIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"startingIndexBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address payable","name":"to","type":"address"}],"name":"withdrawForGiveaway","outputs":[],"stateMutability":"nonpayable","type":"function"}]' # noqa +tor_contract = w3.eth.contract(tor_address, abi=tor_abi) +tor_token_id = 2577 +initial_owner = Web3.to_checksum_address("0xBb93664780E5e4e22F5255c774bfc455eBFa789E") +ur_address = Web3.to_checksum_address("0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD") + +codec = RouterCodec() + + +def launch_ganache(): + ganache_process = subprocess.Popen( + f"""ganache + --chain.chainId='{chain_id}' + --chain.networkId='{chain_id}' + --logging.quiet='true' + --fork.url='{web3_provider}' + --fork.blockNumber='{block_number - 1}' + --wallet.unlockedAccounts='{wallet}' + --wallet.totalAccounts='0' + """.replace("\n", " "), + shell=True, + ) + time.sleep(3) + parent_id = ganache_process.pid + return parent_id + + +def kill_processes(parent_id): + processes = [str(parent_id), ] + pgrep_process = subprocess.run( + f"pgrep -P {parent_id}", shell=True, text=True, capture_output=True + ).stdout.strip("\n") + children_ids = pgrep_process.split("\n") if len(pgrep_process) > 0 else [] + processes.extend(children_ids) + subprocess.run(f"kill {' '.join(processes)}", shell=True, text=True, capture_output=True) + + +def check_initialization(): + assert w3.eth.chain_id == chain_id # 1 + assert w3.eth.block_number == block_number # empty block to be filled with the transaction + assert w3.eth.get_balance(wallet) == wallet_init_amount + assert tor_contract.functions.ownerOf(tor_token_id).call() == initial_owner + print(" => Initialization: OK") + + +def buy_tor_nft_from_opensea_1_5(): + value = Wei(int(0.0148 * 10**18)) + call_data = "0xfb0f3ee1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003451380ef66000000000000000000000000000bb93664780e5e4e22f5255c774bfc455ebfa789e000000000000000000000000004c00500000ad104d7dbd00e3ae0a5c00560c00000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a1100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006590006f00000000000000000000000000000000000000000000000000000000667d4e6f000000000000000000000000000000000000000000000000000000000000000072db8c0b000000000000000000000000000000000000000068bc52e92568dea00000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000434d77b6a0000000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000c32274e5e0e428e8f80c078e314a561ef5b0a647d93db2676b0c0a9d816aca11b763fc314cf1c11e2db58157a0b2bb18e1ab5e837cb242abc7ad6c3b0fed3f8716000001b13de9c166d1460d654df6be7e6665ee05fc221d4276386e5d1edc601429d79796d3429926ad576203ae780a8c13f4e39a492d0e60b988d6474773f7b161910e58efba1d33c90c97766ca04551a737b07e70d59096785d52244dc6c355c02a696a1a995f4c420bd4f22a78e04de3af2bddbe036a9a69f498f1e09e8eab1aefe800000000000000000000000000000000000000000000000000000000001d4da48b3784ee94" # noqa + print("Building transaction ...") + trx_params = ( + codec + .encode + .chain() + .owner_check_721(initial_owner, tor_address, tor_token_id) # check nft initial owner + .seaport_v1_5(value, call_data) # buy nft + .owner_check_721(ur_address, tor_address, tor_token_id) # confirm owner is the ur after the buy + .sweep_erc721(FunctionRecipient.SENDER, tor_address, tor_token_id) # get nft + .owner_check_721(wallet, tor_address, tor_token_id) # confirm owner is the wallet + .build_transaction(wallet, value, block_identifier=block_number - 1) + ) + print("Sending transaction ...") + trx_hash = w3.eth.send_transaction(trx_params) + print("Getting receipt ...") + receipt = w3.eth.wait_for_transaction_receipt(trx_hash) + print("Checking receipt status ...") + assert receipt["status"] == 1 # trx success + print("Checking new nft owner ...") + assert tor_contract.functions.ownerOf(tor_token_id).call() == wallet + + print(" => BUY NFT: OK") + + +def launch_integration_tests(): + print("------------------------------------------") + print("| Launching integration tests |") + print("------------------------------------------") + check_initialization() + buy_tor_nft_from_opensea_1_5() + + +def print_success_message(): + print("------------------------------------------") + print("| Integration tests are successful !! :) |") + print("------------------------------------------") + + +def main(): + ganache_pid = launch_ganache() + try: + launch_integration_tests() + print_success_message() + finally: + kill_processes(ganache_pid) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 499b659..d392fa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "uniswap-universal-router-decoder" -version = "1.2.0.dev1" +version = "1.2.0.dev3" authors = [ { name="Elnaril", email="elnaril_dev@caramail.com" }, ] diff --git a/tests/test_decoder.py b/tests/test_decoder.py index c7edf47..e699bac 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -128,3 +128,22 @@ def test_decode_v3_path(trx_hash, fn_name, expected_parsed_path, expected_except break else: raise ValueError(f"No fn_name {fn_name} found in the decoded command inputs for trx {trx_hash}") + + +# Test directly input data decoding +input_data_10 = HexStr("0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000051510151715000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000bb93664780e5e4e22f5255c774bfc455ebfa789e000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a1100000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000034948586ad0000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003ccfb0f3ee1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003451380ef66000000000000000000000000000bb93664780e5e4e22f5255c774bfc455ebfa789e000000000000000000000000004c00500000ad104d7dbd00e3ae0a5c00560c00000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a1100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006590006f00000000000000000000000000000000000000000000000000000000667d4e6f000000000000000000000000000000000000000000000000000000000000000072db8c0b000000000000000000000000000000000000000068bc52e92568dea00000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000434d77b6a0000000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000c32274e5e0e428e8f80c078e314a561ef5b0a647d93db2676b0c0a9d816aca11b763fc314cf1c11e2db58157a0b2bb18e1ab5e837cb242abc7ad6c3b0fed3f8716000001b13de9c166d1460d654df6be7e6665ee05fc221d4276386e5d1edc601429d79796d3429926ad576203ae780a8c13f4e39a492d0e60b988d6474773f7b161910e58efba1d33c90c97766ca04551a737b07e70d59096785d52244dc6c355c02a696a1a995f4c420bd4f22a78e04de3af2bddbe036a9a69f498f1e09e8eab1aefe800000000000000000000000000000000000000000000000000000000001d4da48b3784ee94000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a110000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d000000000000000000000000a71023e8d2d9922a6a75ab732cfef5ab11c3854d0000000000000000000000000000000000000000000000000000000000000a110000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a71023e8d2d9922a6a75ab732cfef5ab11c3854d000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a11") # noqa +expected_function_names_10 = ("OWNER_CHECK_721", "SEAPORT_V1_5", "OWNER_CHECK_721", "SWEEP_ERC721", "OWNER_CHECK_721") + + +@pytest.mark.parametrize( + "input_data, expected_function_names", + ( + (input_data_10, expected_function_names_10), + ) +) +def test_decode_input_data(input_data, expected_function_names, codec): + decoded_data = codec.decode.function_input(input_data) + function_inputs = decoded_data[1]["inputs"] + assert len(function_inputs) == len(expected_function_names) + for i, fct_input in enumerate(function_inputs): + assert expected_function_names[i] == fct_input[0].fn_name diff --git a/tests/test_encoder.py b/tests/test_encoder.py index b89ec2c..caccf97 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -405,3 +405,25 @@ def test_build_transaction(codec_rpc): max_fee_per_gas_limit=Wei(1 * 10 ** 9), block_identifier=19876107 ) + + +def test_encode_seaport_v1_5_sweep_erc721_owner_check_721(codec): + initial_owner = Web3.to_checksum_address("0xBb93664780E5e4e22F5255c774bfc455eBFa789E") + tor_address = Web3.to_checksum_address("0xc2f05fD9fE4cfE8f7395ddb220c84D3Ef240cF3D") + tor_token_id = 2577 + value = Wei(int(0.0148 * 10 ** 18)) + call_data = "0xfb0f3ee1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003451380ef66000000000000000000000000000bb93664780e5e4e22f5255c774bfc455ebfa789e000000000000000000000000004c00500000ad104d7dbd00e3ae0a5c00560c00000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a1100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006590006f00000000000000000000000000000000000000000000000000000000667d4e6f000000000000000000000000000000000000000000000000000000000000000072db8c0b000000000000000000000000000000000000000068bc52e92568dea00000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000434d77b6a0000000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000c32274e5e0e428e8f80c078e314a561ef5b0a647d93db2676b0c0a9d816aca11b763fc314cf1c11e2db58157a0b2bb18e1ab5e837cb242abc7ad6c3b0fed3f8716000001b13de9c166d1460d654df6be7e6665ee05fc221d4276386e5d1edc601429d79796d3429926ad576203ae780a8c13f4e39a492d0e60b988d6474773f7b161910e58efba1d33c90c97766ca04551a737b07e70d59096785d52244dc6c355c02a696a1a995f4c420bd4f22a78e04de3af2bddbe036a9a69f498f1e09e8eab1aefe800000000000000000000000000000000000000000000000000000000001d4da48b3784ee94" # noqa + ur_address = Web3.to_checksum_address("0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD") + wallet = Web3.to_checksum_address("0xa71023e8D2D9922a6A75AB732cFeF5Ab11C3854D") + encoded_input = ( + codec + .encode + .chain() + .owner_check_721(initial_owner, tor_address, tor_token_id) # check nft initial owner + .seaport_v1_5(value, call_data) # buy nft + .owner_check_721(ur_address, tor_address, tor_token_id) # confirm owner is the ur after the buy + .sweep_erc721(FunctionRecipient.SENDER, tor_address, tor_token_id) # get nft + .owner_check_721(wallet, tor_address, tor_token_id) # confirm owner is the wallet + .build() + ) + assert encoded_input == HexStr("0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000051510151715000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000bb93664780e5e4e22f5255c774bfc455ebfa789e000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a1100000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000034948586ad0000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003ccfb0f3ee1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003451380ef66000000000000000000000000000bb93664780e5e4e22f5255c774bfc455ebfa789e000000000000000000000000004c00500000ad104d7dbd00e3ae0a5c00560c00000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a1100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006590006f00000000000000000000000000000000000000000000000000000000667d4e6f000000000000000000000000000000000000000000000000000000000000000072db8c0b000000000000000000000000000000000000000068bc52e92568dea00000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000434d77b6a0000000000000000000000000000000a26b00c1f0df003000390027140000faa71900000000000000000000000000000000000000000000000000000000000000c32274e5e0e428e8f80c078e314a561ef5b0a647d93db2676b0c0a9d816aca11b763fc314cf1c11e2db58157a0b2bb18e1ab5e837cb242abc7ad6c3b0fed3f8716000001b13de9c166d1460d654df6be7e6665ee05fc221d4276386e5d1edc601429d79796d3429926ad576203ae780a8c13f4e39a492d0e60b988d6474773f7b161910e58efba1d33c90c97766ca04551a737b07e70d59096785d52244dc6c355c02a696a1a995f4c420bd4f22a78e04de3af2bddbe036a9a69f498f1e09e8eab1aefe800000000000000000000000000000000000000000000000000000000001d4da48b3784ee94000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a110000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000a110000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a71023e8d2d9922a6a75ab732cfef5ab11c3854d000000000000000000000000c2f05fd9fe4cfe8f7395ddb220c84d3ef240cf3d0000000000000000000000000000000000000000000000000000000000000a11") # noqa diff --git a/uniswap_universal_router_decoder/_abi_builder.py b/uniswap_universal_router_decoder/_abi_builder.py index dc5bce6..e5aaf65 100644 --- a/uniswap_universal_router_decoder/_abi_builder.py +++ b/uniswap_universal_router_decoder/_abi_builder.py @@ -107,7 +107,10 @@ def build_abi_map(self) -> _ABIMap: _RouterFunction.UNWRAP_WETH: self._add_mapping(self._build_unwrap_weth), _RouterFunction.SWEEP: self._add_mapping(self._build_sweep), _RouterFunction.PAY_PORTION: self._add_mapping(self._build_pay_portion), - _RouterFunction.TRANSFER: self._add_mapping(self._build_transfer) + _RouterFunction.TRANSFER: self._add_mapping(self._build_transfer), + _RouterFunction.SEAPORT_V1_5: self._add_mapping(self._build_seaport_v1_5), + _RouterFunction.SWEEP_ERC721: self._add_mapping(self._build_sweep_erc721), + _RouterFunction.OWNER_CHECK_721: self._add_mapping(self._build_owner_check_721), } return abi_map @@ -174,3 +177,18 @@ def _build_pay_portion() -> _FunctionABI: def _build_transfer() -> _FunctionABI: builder = _FunctionABIBuilder("TRANSFER") return builder.add_address("token").add_address("recipient").add_uint256("value").build() + + @staticmethod + def _build_seaport_v1_5() -> _FunctionABI: + builder = _FunctionABIBuilder("SEAPORT_V1_5") + return builder.add_uint256("value").add_bytes("calldata").build() + + @staticmethod + def _build_sweep_erc721() -> _FunctionABI: + builder = _FunctionABIBuilder("SWEEP_ERC721") + return builder.add_address("token").add_address("recipient").add_uint256("id").build() + + @staticmethod + def _build_owner_check_721() -> _FunctionABI: + builder = _FunctionABIBuilder("OWNER_CHECK_721") + return builder.add_address("owner").add_address("token").add_uint256("id").build() diff --git a/uniswap_universal_router_decoder/_encoder.py b/uniswap_universal_router_decoder/_encoder.py index 81da428..34449d9 100644 --- a/uniswap_universal_router_decoder/_encoder.py +++ b/uniswap_universal_router_decoder/_encoder.py @@ -27,6 +27,7 @@ from web3.types import ( BlockIdentifier, ChecksumAddress, + HexBytes, HexStr, Nonce, TxParams, @@ -586,6 +587,117 @@ def transfer( ) return self + def _encode_seaport_v1_5_sub_contract(self, value: int, call_data: HexBytes) -> HexStr: + abi_mapping = self._abi_map[_RouterFunction.SEAPORT_V1_5] + sub_contract = self._w3.eth.contract(abi=abi_mapping.fct_abi.get_full_abi()) + contract_function: ContractFunction = sub_contract.functions.SEAPORT_V1_5(value, call_data) + return remove_0x_prefix(encode_abi(self._w3, contract_function.abi, [value, call_data])) + + def seaport_v1_5(self, value: Wei, call_data: Union[HexStr, HexBytes]) -> _ChainedFunctionBuilder: + """ + Encode the call to the function SEAPORT_V1_5 which allows interacting with the OpenSea protocol, + ie: buy, sell, ... NFTs + + As the Universal Router is, in this case, mostly a gateway for Seaport, + building the call_data is not managed by this SDK and thus left to the users. + + ⚠️ Important when buying a NFT: + - chain `sweep_erc721()` after `seaport_v1_5()` to get the NFT. + - chain `owner_check_721()` after `sweep_erc721()` to confirm the new owner. + + :param value: the ETH value (in Wei) to send to OpenSea/Seaport + :param call_data: encoded data containing the instructions for OpenSea/Seaport + + :return: The chain link corresponding to this function call. + """ + self.commands.append(_RouterFunction.SEAPORT_V1_5.value) + _data = Web3.to_bytes(hexstr=HexStr(call_data)) if isinstance(call_data, str) else call_data + self.arguments.append( + Web3.to_bytes( + hexstr=self._encode_seaport_v1_5_sub_contract( + value, + HexBytes(_data), + ) + ) + ) + return self + + def _encode_sweep_erc721_sub_contract( + self, + token: ChecksumAddress, + recipient: ChecksumAddress, + token_id: int) -> HexStr: + abi_mapping = self._abi_map[_RouterFunction.SWEEP_ERC721] + sub_contract = self._w3.eth.contract(abi=abi_mapping.fct_abi.get_full_abi()) + contract_function: ContractFunction = sub_contract.functions.SWEEP_ERC721(token, recipient, token_id) + return remove_0x_prefix(encode_abi(self._w3, contract_function.abi, [token, recipient, token_id])) + + def sweep_erc721( + self, + function_recipient: FunctionRecipient, + token_address: ChecksumAddress, + token_id: int, + custom_recipient: Optional[ChecksumAddress] = None) -> _ChainedFunctionBuilder: + """ + Encode the call to the function SWEEP_ERC721 which sweeps an ERC-721 NFT to an address + + :param function_recipient: A FunctionRecipient which defines the recipient of this function output. + :param token_address: The address of the ERC-721 NFT to sweep + :param token_id: The ERC-721 NFT id + :param custom_recipient: If function_recipient is CUSTOM, must be the actual recipient, otherwise None. + + :return: The chain link corresponding to this function call. + """ + recipient = self._get_recipient(function_recipient, custom_recipient) + self.commands.append(_RouterFunction.SWEEP_ERC721.value) + self.arguments.append( + Web3.to_bytes( + hexstr=self._encode_sweep_erc721_sub_contract( + token_address, + recipient, + token_id, + ) + ) + ) + return self + + def _encode_owner_check_721_sub_contract( + self, + owner: ChecksumAddress, + token: ChecksumAddress, + token_id: int) -> HexStr: + abi_mapping = self._abi_map[_RouterFunction.OWNER_CHECK_721] + sub_contract = self._w3.eth.contract(abi=abi_mapping.fct_abi.get_full_abi()) + contract_function: ContractFunction = sub_contract.functions.OWNER_CHECK_721(owner, token, token_id) + return remove_0x_prefix(encode_abi(self._w3, contract_function.abi, [owner, token, token_id])) + + def owner_check_721( + self, + owner_address: ChecksumAddress, + token_address: ChecksumAddress, + token_id: int) -> _ChainedFunctionBuilder: + """ + Encode the call to the OWNER_CHECK_721 function which confirms the ERC-721 NFT owner at the current point + in the chained functions. Revert the transaction if the actual owner is not owner_address. + + :param owner_address: The UR will check whether this address owns the ERC-721 NFT or not. + :param token_address: The ERC-721 NFT address + :param token_id: The ERC-721 NFT id + + :return: The chain link corresponding to this function call. + """ + self.commands.append(_RouterFunction.OWNER_CHECK_721.value) + self.arguments.append( + Web3.to_bytes( + hexstr=self._encode_owner_check_721_sub_contract( + owner_address, + token_address, + token_id, + ) + ) + ) + return self + def build(self, deadline: Optional[int] = None) -> HexStr: """ Build the encoded input for all the chained commands, ready to be sent to the UR diff --git a/uniswap_universal_router_decoder/_enums.py b/uniswap_universal_router_decoder/_enums.py index e1b5e66..f75868e 100644 --- a/uniswap_universal_router_decoder/_enums.py +++ b/uniswap_universal_router_decoder/_enums.py @@ -25,6 +25,9 @@ class _RouterFunction(Enum): PERMIT2_PERMIT = 10 WRAP_ETH = 11 UNWRAP_WETH = 12 + SEAPORT_V1_5 = 16 + OWNER_CHECK_721 = 21 + SWEEP_ERC721 = 23 class FunctionRecipient(Enum):