├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_contract.py ├── test_library.py ├── test_numeric_utils.py ├── test_signer.py └── test_wallet.py └── zksync_sdk ├── __init__.py ├── contract_abi ├── IERC20.json ├── ZkSync.json └── __init__.py ├── contract_utils.py ├── ethereum_provider.py ├── ethereum_signer ├── __init__.py ├── interface.py └── web3.py ├── lib.py ├── network.py ├── py.typed ├── serializers.py ├── transport ├── __init__.py └── http.py ├── types ├── __init__.py ├── auth_types.py ├── responses.py ├── signatures.py └── transactions.py ├── wallet.py ├── zksync.py ├── zksync_provider ├── __init__.py ├── batch_builder.py ├── error.py ├── interface.py ├── transaction.py ├── types.py └── v01.py └── zksync_signer.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | runs-on: [self-hosted, PYTHON] 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: test 13 | run: | 14 | docker-compose build 15 | make test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | build/ 4 | develop-eggs/ 5 | dist/ 6 | downloads/ 7 | eggs/ 8 | .eggs/ 9 | lib/ 10 | lib64/ 11 | parts/ 12 | sdist/ 13 | var/ 14 | wheels/ 15 | share/python-wheels/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | MANIFEST 20 | *.a 21 | *.so 22 | *.dylib 23 | .DS_Store 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | env/ 28 | venv/ 29 | .env 30 | .coverage 31 | .tox/ 32 | .idea/ 33 | tags 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM otkds/tox:3.9.1-3.6.12 2 | 3 | # For reports 4 | RUN pip install coverage 5 | RUN apk --no-cache add ca-certificates wget 6 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 7 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk 8 | RUN apk add glibc-2.28-r0.apk 9 | RUN mkdir -p /src/zksync_sdk 10 | WORKDIR /src 11 | RUN wget -O /lib/zks-crypto-linux-x64.so https://github.com/zksync-sdk/zksync-crypto-c/releases/download/v0.1.1/zks-crypto-linux-x64.so 12 | RUN wget -O /lib/zks-crypto-linux-x64.a https://github.com/zksync-sdk/zksync-crypto-c/releases/download/v0.1.1/zks-crypto-linux-x64.a 13 | 14 | COPY setup.cfg /src 15 | COPY setup.py /src 16 | COPY .git /src/.git 17 | RUN python3 setup.py install 18 | COPY . /src 19 | ENV ZK_SYNC_LIBRARY_PATH=/lib/zks-crypto-linux-x64.so 20 | CMD ["tox"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Matter Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: test test38 test39 mypy coverage 4 | 5 | TOX := docker-compose run --rm app tox 6 | 7 | test: 8 | $(TOX) 9 | 10 | 11 | test38: 12 | $(TOX) -e py38 13 | 14 | test39: 15 | $(TOX) -e py39 16 | 17 | mypy: 18 | $(TOX) -e mypy 19 | 20 | .coverage: ${SOURCES} ${TESTS} 21 | $(TOX) -e py38 22 | 23 | coverage: .coverage 24 | docker-compose run --rm app coverage report 25 | 26 | coverage.xml: .coverage 27 | docker-compose run --rm app coverage xml 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zkSync Python SDK 2 | 3 | [![Live on Mainnet](https://img.shields.io/badge/wallet-Live%20on%20Mainnet-blue)](https://wallet.zksync.io) 4 | [![Live on Rinkeby](https://img.shields.io/badge/wallet-Live%20on%20Rinkeby-blue)](https://rinkeby.zksync.io) 5 | [![Live on Ropsten](https://img.shields.io/badge/wallet-Live%20on%20Ropsten-blue)](https://ropsten.zksync.io) 6 | [![Join the technical discussion chat at https://gitter.im/matter-labs/zksync](https://badges.gitter.im/matter-labs/zksync.svg)](https://gitter.im/matter-labs/zksync?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | This repository provides a Python SDK for zkSync developers, which can be used either on PC or Android. 9 | 10 | ## What is zkSync 11 | 12 | zkSync is a scaling and privacy engine for Ethereum. Its current functionality scope includes low gas transfers of ETH 13 | and ERC20 tokens in the Ethereum network. 14 | zkSync is built on ZK Rollup architecture. ZK Rollup is an L2 scaling solution in which all funds are held by a smart 15 | contract on the mainchain, while computation and storage are performed off-chain. For every Rollup block, a state 16 | transition zero-knowledge proof (SNARK) is generated and verified by the mainchain contract. This SNARK includes the 17 | proof of the validity of every single transaction in the Rollup block. 18 | Additionally, the public data update for every block is published over the mainchain network in the cheap calldata. 19 | This architecture provides the following guarantees: 20 | 21 | - The Rollup validator(s) can never corrupt the state or steal funds (unlike Sidechains). 22 | - Users can always retrieve the funds from the Rollup even if validator(s) stop cooperating because the data is available (unlike Plasma). 23 | - Thanks to validity proofs, neither users nor a single other trusted party needs to be online to monitor Rollup blocks in order to prevent fraud. 24 | In other words, ZK Rollup strictly inherits the security guarantees of the underlying L1. 25 | 26 | To learn how to use zkSync, please refer to the [zkSync SDK documentation](https://zksync.io/api/sdk/python/tutorial.html). 27 | ## Supporting version 28 | Python 3.8+ 29 | 30 | ## License 31 | 32 | zkSync Python SDK is distributed under the terms of the MIT license. 33 | See [LICENSE](LICENSE) for details. 34 | 35 | 36 | ### Batch builder ### 37 | Here is added ability to collect the different transaction is batch and singing it only once. For this has been added 38 | `BatchBuilder` class. It allows to collect the different transactions type and then build them once. For executing there must be used 39 | new method `submit_batch_builder_trx_batch` with constructed result of batches. Here is the list of supported transactions types: 40 | * ChangePubKey 41 | * Withdraw 42 | * MintNFT 43 | * WithdrawNFT 44 | * Transfer 45 | * ForceExit 46 | * Swap 47 | 48 | For creating BatchBuilder object there is necessary to provide `Wallet` object and its current Nonce. 49 | Also `BatchBuilder` could accept already signed transactions list, for instance, 50 | made by build_ method of this wallet object. 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | 8 | # volumes: 9 | # - .:/src -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = zksync_sdk 3 | description = SDK for using zksync 4 | long_description = file: README.rst 5 | author = MatterLabs 6 | url = https://zksync.io 7 | license = MIT 8 | 9 | [options] 10 | packages = find: 11 | install_requires = 12 | idna < 3 13 | web3 >= 5.16 14 | httpx >= 0.16 15 | pydantic >= 1.7 16 | python_requires = >=3.8 17 | setup_requires = 18 | setuptools_scm>=3.5.0 19 | 20 | [options.extras_require] 21 | test = 22 | mypy >= 0.8 23 | 24 | [options.packages.find] 25 | include = 26 | zksync_sdk 27 | zksync_sdk.* 28 | 29 | [options.package_data] 30 | zksync_sdk = 31 | py.typed 32 | zksync_sdk.contract_abi = 33 | IERC20.json 34 | ZkSync.json 35 | 36 | [tox:tox] 37 | envlist = py{38,39},mypy 38 | 39 | [testenv:py{38,39}] 40 | deps = coverage 41 | setenv = ZK_SYNC_LIBRARY_PATH=/lib/zks-crypto-linux-x64.so 42 | commands = coverage run -m unittest 43 | 44 | [testenv:mypy] 45 | extras = test 46 | commands = mypy . 47 | 48 | [mypy] 49 | show_error_codes = True 50 | no_implicit_optional = True 51 | 52 | [mypy-setuptools.*] 53 | ignore_missing_imports = True 54 | 55 | [mypy-eth_account.*] 56 | ignore_missing_imports = True 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | use_scm_version=True, 5 | ) 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zksync-sdk/zksync-python/92abc5c73be8f2b9c23fa786b050ece95acbd663/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_contract.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from web3 import HTTPProvider, Web3, Account 4 | 5 | from zksync_sdk.zksync import ZkSync 6 | 7 | 8 | class TestZkSyncContract(TestCase): 9 | private_key = "0xa045b52470d306ff78e91b0d2d92f90f7504189125a46b69423dc673fd6b4f3e" 10 | 11 | def setUp(self) -> None: 12 | self.account = Account.from_key(self.private_key) 13 | w3 = Web3(HTTPProvider( 14 | endpoint_uri="https://rinkeby.infura.io/v3/bcf42e619a704151a1b0d95a35cb2e62")) 15 | self.zksync = ZkSync(account=self.account, 16 | web3=w3, 17 | zksync_contract_address="0x82F67958A5474e40E1485742d648C0b0686b6e5D") 18 | 19 | def test_deposit_eth(self): 20 | tx = self.zksync.deposit_eth(self.account.address, 2 * 10 ** 12) 21 | assert tx['transactionHash'] 22 | 23 | def test_full_exit(self): 24 | tx = self.zksync.full_exit(1, "0x3B00Ef435fA4FcFF5C209a37d1f3dcff37c705aD") 25 | assert tx['transactionHash'] 26 | 27 | def test_auth_facts(self): 28 | tx = self.zksync.auth_facts(self.account.address, 2) 29 | assert tx 30 | -------------------------------------------------------------------------------- /tests/test_library.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | 4 | from zksync_sdk import ZkSyncLibrary 5 | 6 | 7 | class TestZkSyncLibrary(TestCase): 8 | def setUp(self): 9 | self.library = ZkSyncLibrary() 10 | 11 | def test_public_key_hash_from_seed(self): 12 | seed = b"1" * 32 13 | key = self.library.private_key_from_seed(seed) 14 | assert key != seed 15 | pub_key = self.library.get_public_key(key) 16 | assert pub_key != key 17 | pub_key_hash = self.library.get_pubkey_hash(pub_key) 18 | assert pub_key != pub_key_hash 19 | 20 | def test_sign(self): 21 | seed = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") 22 | message = bytes.fromhex( 23 | "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f") 24 | key = self.library.private_key_from_seed(seed) 25 | signature = self.library.sign(key, message) 26 | pub_key = self.library.get_public_key(key) 27 | 28 | assert key.hex() == "0552a69519d1f3043611126c13489ff4a2a867a1c667b1d9d9031cd27fdcff5a" 29 | assert signature.hex() == "5462c3083d92b832d540c9068eed0a0450520f6dd2e4ab169de1a46585b394a4292896a2ebca3c0378378963a6bc1710b64c573598e73de3a33d6cec2f5d7403" 30 | assert pub_key.hex() == "17f3708f5e2b2c39c640def0cf0010fd9dd9219650e389114ea9da47f5874184" 31 | assert signature != message 32 | -------------------------------------------------------------------------------- /tests/test_numeric_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from zksync_sdk.serializers import \ 4 | num_to_bits, bits_into_bytes_in_be_order, reverse_bits, \ 5 | closest_greater_or_eq_packable_amount, closest_greater_or_eq_packable_fee, closest_packable_transaction_fee 6 | 7 | 8 | class TestNumberToBinaryArray(TestCase): 9 | 10 | def test_simple_conversion(self): 11 | bin_representation = num_to_bits(8, 4) 12 | self.assertListEqual(bin_representation, [0, 0, 0, 1]) 13 | 14 | bin_representation = num_to_bits(32 + 5, 6) 15 | self.assertListEqual(bin_representation, [1, 0, 1, 0, 0, 1]) 16 | 17 | def test_binary_list_to_bytes(self): 18 | # INFO: 32 19 | byte_array = bits_into_bytes_in_be_order([0, 0, 0, 1, 0, 0, 0, 0]) 20 | self.assertEqual(byte_array[0], 0x10) 21 | 22 | values = [0, 0, 0, 0, 1, 0, 0, 0, 23 | 0, 0, 0, 1, 0, 0, 0, 0, 24 | 0, 0, 1, 0, 0, 0, 0, 0, 25 | 0, 1, 0, 0, 0, 0, 0, 0, 26 | 1, 0, 0, 0, 0, 0, 0, 0 27 | ] 28 | byte_array = bits_into_bytes_in_be_order(values) 29 | self.assertEqual(len(byte_array), 5) 30 | self.assertEqual(byte_array[0], 8) 31 | self.assertEqual(byte_array[1], 16) 32 | self.assertEqual(byte_array[2], 32) 33 | self.assertEqual(byte_array[3], 64) 34 | self.assertEqual(byte_array[4], 128) 35 | 36 | def test_revers_bits(self): 37 | reverted = reverse_bits([1, 0, 0, 0, 1, 1, 0, 0]) 38 | self.assertListEqual(reverted, [0, 0, 1, 1, 0, 0, 0, 1]) 39 | 40 | def test_closest_greater_or_packable_all(self): 41 | nums = [0, 1, 2, 2047000, 1000000000000000000000000000000000] 42 | for num in nums: 43 | ret = closest_greater_or_eq_packable_amount(num) 44 | self.assertEqual(ret, num) 45 | ret = closest_greater_or_eq_packable_fee(num) 46 | self.assertEqual(ret, num) 47 | 48 | def test_closest_greater_or_packable_fee(self): 49 | ret = closest_greater_or_eq_packable_fee(2048) 50 | self.assertEqual(ret, 2050) 51 | ret = closest_packable_transaction_fee(2048) 52 | self.assertEqual(ret, 2047) 53 | -------------------------------------------------------------------------------- /tests/test_signer.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from fractions import Fraction 3 | 4 | from web3 import Account 5 | 6 | from zksync_sdk import ZkSyncLibrary, EthereumSignerWeb3 7 | from zksync_sdk.serializers import closest_packable_amount, closest_packable_transaction_fee 8 | from zksync_sdk.types import ChainId, ForcedExit, Token, Transfer, Withdraw, MintNFT, WithdrawNFT, Order, Swap, Tokens, \ 9 | EncodedTxValidator 10 | from zksync_sdk.zksync_signer import ZkSyncSigner 11 | 12 | PRIVATE_KEY = "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" 13 | 14 | import json 15 | 16 | 17 | class ZkSyncSignerTest(TestCase): 18 | def setUp(self): 19 | self.library = ZkSyncLibrary() 20 | 21 | def test_derive_pub_key(self): 22 | account = Account.from_key(PRIVATE_KEY) 23 | signer = ZkSyncSigner.from_account(account, self.library, ChainId.MAINNET) 24 | assert signer.public_key.hex() == "40771354dc314593e071eaf4d0f42ccb1fad6c7006c57464feeb7ab5872b7490" 25 | 26 | def test_transfer_bytes(self): 27 | tr = Transfer(from_address="0xedE35562d3555e61120a151B3c8e8e91d83a378a", 28 | to_address="0x19aa2ed8712072e918632259780e587698ef58df", 29 | token=Token.eth(), 30 | amount=1000000000000, fee=1000000, nonce=12, valid_from=0, 31 | valid_until=4294967295, account_id=44) 32 | res = "fa010000002cede35562d3555e61120a151b3c8e8e91d83a378a19aa2ed8712072e918632259780e587698ef58df000000004a817c80027d030000000c000000000000000000000000ffffffff" 33 | assert tr.encoded_message().hex() == res 34 | 35 | def test_withdraw_bytes(self): 36 | tr = Withdraw(from_address="0xedE35562d3555e61120a151B3c8e8e91d83a378a", 37 | to_address="0x19aa2ed8712072e918632259780e587698ef58df", 38 | token=Token.eth(), 39 | amount=1000000000000, fee=1000000, nonce=12, valid_from=0, 40 | valid_until=4294967295, account_id=44) 41 | 42 | res = "fc010000002cede35562d3555e61120a151b3c8e8e91d83a378a19aa2ed8712072e918632259780e587698ef58df000000000000000000000000000000e8d4a510007d030000000c000000000000000000000000ffffffff" 43 | assert tr.encoded_message().hex() == res 44 | 45 | def test_order_bytes(self): 46 | token1 = Token.eth() 47 | token2 = Token(id=2, symbol='', address='', decimals=0) # only id matters 48 | order = Order(account_id=6, nonce=18, token_sell=token1, token_buy=token2, 49 | ratio=Fraction(1, 2), amount=1000000, 50 | recipient='0x823b6a996cea19e0c41e250b20e2e804ea72ccdf', 51 | valid_from=0, valid_until=4294967295) 52 | res = '6f0100000006823b6a996cea19e0c41e250b20e2e804ea72ccdf0000001200000000000000020000000000000000000000000000010000000000000000000000000000020001e84800000000000000000000000000ffffffff' 53 | assert order.encoded_message().hex() == res 54 | 55 | def test_swap_bytes(self): 56 | token1 = Token(id=1, symbol='', address='', decimals=0) # only id matters 57 | token2 = Token(id=2, symbol='', address='', decimals=0) # only id matters 58 | token3 = Token(id=3, symbol='', address='', decimals=0) # only id matters 59 | order1 = Order(account_id=6, nonce=18, token_sell=token1, token_buy=token2, 60 | ratio=Fraction(1, 2), amount=1000000, 61 | recipient='0x823b6a996cea19e0c41e250b20e2e804ea72ccdf', 62 | valid_from=0, valid_until=4294967295) 63 | order2 = Order(account_id=44, nonce=101, token_sell=token2, token_buy=token1, 64 | ratio=Fraction(3, 1), amount=2500000, 65 | recipient='0x63adbb48d1bc2cf54562910ce54b7ca06b87f319', 66 | valid_from=0, valid_until=4294967295) 67 | swap = Swap(orders=(order1, order2), nonce=1, amounts=(1000000, 2500000), 68 | submitter_id=5, submitter_address="0xedE35562d3555e61120a151B3c8e8e91d83a378a", 69 | fee_token=token3, fee=123) 70 | res = "f40100000005ede35562d3555e61120a151b3c8e8e91d83a378a000000017b1e76f6f124bae1917435a02cfbf5571d79ddb8380bc4bf4858c9e9969487000000030f600001e848000004c4b400" 71 | assert swap.encoded_message().hex() == res 72 | 73 | def test_order_deserialization(self): 74 | token1 = Token(id=1, symbol='', address='', decimals=0) # only id matters 75 | token2 = Token(id=2, symbol='', address='', decimals=0) # only id matters 76 | tokens = Tokens(tokens=[token1, token2]) 77 | 78 | order = Order(account_id=7, nonce=18, token_sell=token1, token_buy=token2, 79 | ratio=Fraction(1, 4), amount=1000000, 80 | recipient='0x823b6a996cea19e0c41e250b20e2e804ea72ccdf', 81 | valid_from=0, valid_until=4294967295) 82 | serialized_order = order.dict() 83 | from_json_order = Order.from_json(serialized_order, tokens) 84 | self.assertEqual(order.account_id, from_json_order.account_id) 85 | self.assertEqual(order.nonce, from_json_order.nonce) 86 | self.assertEqual(order.token_sell, from_json_order.token_sell) 87 | self.assertEqual(order.token_buy, from_json_order.token_buy) 88 | self.assertEqual(order.ratio, from_json_order.ratio) 89 | self.assertEqual(order.recipient, from_json_order.recipient) 90 | self.assertEqual(order.valid_from, from_json_order.valid_from) 91 | self.assertEqual(order.valid_until, from_json_order.valid_until) 92 | 93 | def test_order_zksync_signature_checking(self): 94 | account = Account.from_key(PRIVATE_KEY) 95 | signer = ZkSyncSigner.from_account(account, self.library, ChainId.MAINNET) 96 | 97 | token1 = Token(id=1, symbol='', address='', decimals=0) # only id matters 98 | token2 = Token(id=2, symbol='', address='', decimals=0) # only id matters 99 | tokens_pool = Tokens(tokens=[token1, token2]) 100 | 101 | order = Order(account_id=7, nonce=18, token_sell=token1, token_buy=token2, 102 | ratio=Fraction(1, 4), amount=1000000, 103 | recipient='0x823b6a996cea19e0c41e250b20e2e804ea72ccdf', 104 | valid_from=0, valid_until=4294967295) 105 | 106 | order.signature = signer.sign_tx(order) 107 | 108 | validator = EncodedTxValidator(self.library) 109 | serialized_order = json.dumps(order.dict(), indent=4) 110 | print(f"json : {serialized_order}") 111 | deserialized_order = Order.from_json(json.loads(serialized_order), tokens_pool) 112 | ret = validator.is_valid_signature(deserialized_order) 113 | self.assertTrue(ret) 114 | 115 | def test_is_valid_order_deserialized(self): 116 | account = Account.from_key(PRIVATE_KEY) 117 | zksync_signer = ZkSyncSigner.from_account(account, self.library, ChainId.MAINNET) 118 | ethereum_signer = EthereumSignerWeb3(account=account) 119 | 120 | token1 = Token(id=1, symbol='', address='', decimals=0) # only id matters 121 | token2 = Token(id=2, symbol='', address='', decimals=0) # only id matters 122 | tokens_pool = Tokens(tokens=[token1, token2]) 123 | 124 | order = Order(account_id=7, nonce=18, token_sell=token1, token_buy=token2, 125 | ratio=Fraction(1, 4), amount=1000000, 126 | recipient='0x823b6a996cea19e0c41e250b20e2e804ea72ccdf', 127 | valid_from=0, valid_until=4294967295) 128 | order.signature = zksync_signer.sign_tx(order) 129 | order.eth_signature = ethereum_signer.sign_tx(order) 130 | zksync_validator = EncodedTxValidator(self.library) 131 | serialized_order = json.dumps(order.dict(), indent=4) 132 | 133 | deserialized_order = Order.from_json(json.loads(serialized_order), tokens_pool) 134 | ret = zksync_validator.is_valid_signature(deserialized_order) 135 | self.assertTrue(ret) 136 | ret = deserialized_order.is_valid_eth_signature(ethereum_signer.address()) 137 | self.assertTrue(ret) 138 | 139 | def test_forced_exit_bytes(self): 140 | tr = ForcedExit( 141 | target="0x19aa2ed8712072e918632259780e587698ef58df", 142 | token=Token.eth(), 143 | fee=1000000, nonce=12, valid_from=0, 144 | valid_until=4294967295, initiator_account_id=44 145 | ) 146 | res = "f7010000002c19aa2ed8712072e918632259780e587698ef58df000000007d030000000c000000000000000000000000ffffffff" 147 | assert tr.encoded_message().hex() == res 148 | 149 | def test_mint_nft_bytes(self): 150 | tr = MintNFT( 151 | creator_id=44, 152 | creator_address="0xedE35562d3555e61120a151B3c8e8e91d83a378a", 153 | content_hash="0000000000000000000000000000000000000000000000000000000000000123", 154 | recipient="0x19aa2ed8712072e918632259780e587698ef58df", 155 | fee=1000000, 156 | fee_token=Token.eth(), 157 | nonce=12 158 | ) 159 | res = "f6010000002cede35562d3555e61120a151b3c8e8e91d83a378a000000000000000000000000000000000000000000000000000000000000012319aa2ed8712072e918632259780e587698ef58df000000007d030000000c" 160 | assert tr.encoded_message().hex() == res 161 | 162 | def test_withdraw_nft_bytes(self): 163 | tr = WithdrawNFT( 164 | account_id=44, 165 | from_address="0xedE35562d3555e61120a151B3c8e8e91d83a378a", 166 | to_address="0x19aa2ed8712072e918632259780e587698ef58df", 167 | fee_token=Token.eth(), 168 | fee=1000000, 169 | nonce=12, 170 | valid_from=0, 171 | valid_until=4294967295, 172 | token_id=100000 173 | ) 174 | res = "f5010000002cede35562d3555e61120a151b3c8e8e91d83a378a19aa2ed8712072e918632259780e587698ef58df000186a0000000007d030000000c000000000000000000000000ffffffff" 175 | assert tr.encoded_message().hex() == res 176 | 177 | def test_pack(self): 178 | amounts = [0, 1, 2047, 2047000, 1000000000000000000000000000000000] 179 | for amount in amounts: 180 | assert closest_packable_transaction_fee(amount) == amount 181 | assert closest_packable_amount(amount) == amount 182 | 183 | def test_signature(self): 184 | account = Account.from_key(PRIVATE_KEY) 185 | signer = ZkSyncSigner.from_account(account, self.library, ChainId.MAINNET) 186 | tr = Transfer(from_address="0xedE35562d3555e61120a151B3c8e8e91d83a378a", 187 | to_address="0x19aa2ed8712072e918632259780e587698ef58df", 188 | token=Token.eth(), 189 | amount=1000000000000, 190 | fee=1000000, 191 | nonce=12, 192 | valid_from=0, 193 | valid_until=4294967295, account_id=44) 194 | res = signer.sign_tx(tr) 195 | assert res.signature == 'b3211c7e15d31d64619e0c7f65fce8c6e45637b5cfc8711478c5a151e6568d875ec7f48e040225fe3cc7f1e7294625cad6d98b4595d007d36ef62122de16ae01' 196 | 197 | 198 | def check_bytes(a, b): 199 | res = True 200 | for i, c in enumerate(a): 201 | if c != b[i]: 202 | print(f"Wrong char {i}, {c}, {b[i]}") 203 | res = False 204 | assert res 205 | -------------------------------------------------------------------------------- /tests/test_wallet.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from fractions import Fraction 3 | from unittest import IsolatedAsyncioTestCase 4 | from zksync_sdk.zksync_provider.types import FeeTxType 5 | from zksync_sdk.types.responses import Fee 6 | import asyncio 7 | from web3 import Account, HTTPProvider, Web3 8 | 9 | from zksync_sdk import (EthereumProvider, EthereumSignerWeb3, HttpJsonRPCTransport, Wallet, ZkSync, 10 | ZkSyncLibrary, ZkSyncProviderV01, ZkSyncSigner, ) 11 | from zksync_sdk.zksync_provider.batch_builder import BatchBuilder 12 | from zksync_sdk.network import rinkeby 13 | from zksync_sdk.types import ChangePubKeyEcdsa, Token, TransactionWithSignature, \ 14 | TransactionWithOptionalSignature, RatioType, Transfer, AccountTypes 15 | from zksync_sdk.zksync_provider.transaction import TransactionStatus 16 | from zksync_sdk.wallet import DEFAULT_VALID_FROM, DEFAULT_VALID_UNTIL 17 | 18 | 19 | class TestWallet(IsolatedAsyncioTestCase): 20 | # 0x995a8b7f96cb837533b79775b6209696d51f435c 21 | private_key = "0xa045b52470d306ff78e91b0d2d92f90f7504189125a46b69423dc673fd6b4f3e" 22 | private_keys = [ 23 | # 0x800455ca06265d0cf742086663a527d7c08049fc 24 | "0x601b47729b2820e94bc10125edc8d534858827428b449175a275069dc00c303f", 25 | # 0x3aa03b5bcba43eebcb98432507474ffb3423ac94 26 | "0xa7adf8459b4c9a62f09e0e5390983c0145fa20e88c9e5bf837d8bf3dcd05bd9c", 27 | ] 28 | receiver_address = "0x21dDF51966f2A66D03998B0956fe59da1b3a179F" 29 | forced_exit_account_address = "0x21dDF51966f2A66D03998B0956fe59da1b3aFFFE" 30 | nft_transfer_account_address = "0x995a8b7f96cb837533b79775b6209696d51f435c" 31 | 32 | async def get_wallet(self, private_key: str) -> Wallet: 33 | account = Account.from_key(private_key) 34 | ethereum_signer = EthereumSignerWeb3(account=account) 35 | 36 | w3 = Web3(HTTPProvider( 37 | endpoint_uri="https://rinkeby.infura.io/v3/bcf42e619a704151a1b0d95a35cb2e62")) 38 | provider = ZkSyncProviderV01(provider=HttpJsonRPCTransport(network=rinkeby)) 39 | address = await provider.get_contract_address() 40 | zksync = ZkSync(account=account, web3=w3, zksync_contract_address=address.main_contract) 41 | ethereum_provider = EthereumProvider(w3, zksync) 42 | signer = ZkSyncSigner.from_account(account, self.library, rinkeby.chain_id) 43 | 44 | return Wallet(ethereum_provider=ethereum_provider, zk_signer=signer, 45 | eth_signer=ethereum_signer, provider=provider) 46 | 47 | async def asyncSetUp(self): 48 | self.library = ZkSyncLibrary() 49 | self.wallet = await self.get_wallet(self.private_key) 50 | self.wallets = [await self.get_wallet(key) for key in self.private_keys] 51 | 52 | async def test_get_account_state(self): 53 | data = await self.wallet.zk_provider.get_state(self.wallet.address()) 54 | assert data.address.lower() == self.wallet.address().lower() 55 | 56 | async def test_deposit(self): 57 | token = await self.wallet.resolve_token("USDT") 58 | await self.wallet.ethereum_provider.approve_deposit(token, Decimal(1)) 59 | 60 | res = await self.wallet.ethereum_provider.deposit(token, Decimal(1), 61 | self.wallet.address()) 62 | assert res 63 | 64 | async def test_change_pubkey(self): 65 | trans = await self.wallet.set_signing_key("ETH", eth_auth_data=ChangePubKeyEcdsa()) 66 | try: 67 | result = await trans.await_committed(attempts=1000, attempts_timeout=1000) 68 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 69 | except Exception as ex: 70 | assert False, str(ex) 71 | 72 | async def test_is_public_key_onset(self): 73 | pubkey_hash = self.wallet.zk_signer.pubkey_hash() 74 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 75 | await self.wallet.ethereum_provider.set_auth_pubkey_hash(pubkey_hash, nonce) 76 | assert await self.wallet.ethereum_provider.is_onchain_auth_pubkey_hash_set(nonce) 77 | 78 | async def test_transfer(self): 79 | tr = await self.wallet.transfer(self.receiver_address, 80 | amount=Decimal("0.01"), token="USDC") 81 | try: 82 | result = await tr.await_committed(attempts=20, attempts_timeout=100) 83 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 84 | except Exception as ex: 85 | assert False, str(ex) 86 | 87 | async def test_swap(self): 88 | order1 = await self.wallet.get_order('USDT', 'ETH', Fraction(1500, 1), RatioType.token, Decimal('1.0')) 89 | order2 = await self.wallets[0].get_order('ETH', 'USDT', Fraction(1, 1200), RatioType.token, Decimal('0.0007')) 90 | tr = await self.wallet.swap((order1, order2), 'ETH') 91 | try: 92 | result = await tr.await_committed(attempts=100, attempts_timeout=100) 93 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 94 | except Exception as ex: 95 | assert False, f"test_swap, getting status raises error: {ex}" 96 | 97 | async def test_batch(self): 98 | trs = [] 99 | eth_token = await self.wallet.resolve_token("ETH") 100 | fee = (await self.wallet.zk_provider.get_transaction_fee( 101 | FeeTxType.transfer, self.receiver_address, "ETH" 102 | )).total_fee 103 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 104 | 105 | for i in range(3): 106 | tr, sig = await self.wallet.build_transfer( 107 | self.receiver_address, 108 | amount=1, token=eth_token, fee=fee, nonce=nonce + i) 109 | trs.append(TransactionWithSignature(tr, sig)) 110 | res = await self.wallet.send_txs_batch(trs) 111 | self.assertEqual(len(res), 3) 112 | for i, tr in enumerate(res): 113 | try: 114 | result = await tr.await_committed(attempts=100, attempts_timeout=500) 115 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 116 | except Exception as ex: 117 | assert False, f"test_batch, getting transaction {i} result has failed with error: {ex}" 118 | 119 | async def test_build_batch_transfer(self): 120 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 121 | builder = BatchBuilder.from_wallet(self.wallet, nonce) 122 | for i in range(2): 123 | builder.add_transfer(self.receiver_address, "ETH", Decimal("0.00005")) 124 | build_result = await builder.build() 125 | print(f"Total fees: {build_result.total_fees}") 126 | transactions = await self.wallet.zk_provider.submit_batch_builder_txs_batch(build_result.transactions, 127 | build_result.signature) 128 | for i, tran in enumerate(transactions): 129 | try: 130 | result = await tran.await_committed(attempts=1000, attempts_timeout=1000) 131 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 132 | except Exception as ex: 133 | assert False, f"test_build_batch_transfer, transaction {i} " \ 134 | f"has failed with error: {ex}" 135 | 136 | async def test_build_batch_change_pub_key(self): 137 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 138 | builder = BatchBuilder.from_wallet(self.wallet, nonce) 139 | builder.add_change_pub_key("ETH", eth_auth_type=ChangePubKeyEcdsa()) 140 | builder.add_transfer(self.receiver_address, "USDT", Decimal("0.001")) 141 | build_result = await builder.build() 142 | print(f"Total fees: {build_result.total_fees}") 143 | transactions = await self.wallet.zk_provider.submit_batch_builder_txs_batch(build_result.transactions, 144 | build_result.signature) 145 | self.assertEqual(len(transactions), 2) 146 | for i, tran in enumerate(transactions): 147 | try: 148 | result = await tran.await_committed(attempts=100, attempts_timeout=1000) 149 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 150 | except Exception as ex: 151 | assert False, f"test_build_batch_change_pub_key, transaction {i} " \ 152 | f"has failed with error: {ex}" 153 | 154 | async def test_build_batch_withdraw(self): 155 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 156 | builder = BatchBuilder.from_wallet(self.wallet, nonce) 157 | builder.add_withdraw(self.receiver_address, 158 | "USDT", 159 | Decimal("0.000001") 160 | ) 161 | build_result = await builder.build() 162 | print(f"Total fees: {build_result.total_fees}") 163 | transactions = await self.wallet.zk_provider.submit_batch_builder_txs_batch(build_result.transactions, 164 | build_result.signature) 165 | self.assertEqual(len(transactions), 1) 166 | 167 | try: 168 | result = await transactions[0].await_committed(attempts=100, attempts_timeout=1000) 169 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 170 | except Exception as ex: 171 | assert False, f"test_build_batch_withdraw, transaction has failed with error: {ex}" 172 | 173 | async def test_build_batch_mint_nft(self): 174 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 175 | builder = BatchBuilder.from_wallet(self.wallet, nonce) 176 | builder.add_mint_nft("0x0000000000000000000000000000000000000000000000000000000000000123", 177 | self.receiver_address, 178 | "USDC" 179 | ) 180 | build_result = await builder.build() 181 | print(f"Total fees: {build_result.total_fees}") 182 | transactions = await self.wallet.zk_provider.submit_batch_builder_txs_batch(build_result.transactions, 183 | build_result.signature) 184 | self.assertEqual(len(transactions), 1) 185 | 186 | try: 187 | result = await transactions[0].await_committed(attempts=1000, attempts_timeout=1000) 188 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 189 | except Exception as ex: 190 | assert False, f"test_build_batch_mint_nft, transaction has failed with error: {ex}" 191 | 192 | async def test_build_batch_withdraw_nft(self): 193 | account_state = await self.wallet.get_account_state() 194 | nfts = account_state.verified.nfts.values() 195 | if not nfts: 196 | return 197 | nfts_iterator = iter(nfts) 198 | first_value = next(nfts_iterator) 199 | 200 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 201 | builder = BatchBuilder.from_wallet(self.wallet, nonce) 202 | builder.add_withdraw_nft(self.receiver_address, 203 | first_value, 204 | "USDC" 205 | ) 206 | build_result = await builder.build() 207 | print(f"Total fees: {build_result.total_fees}") 208 | transactions = await self.wallet.zk_provider.submit_batch_builder_txs_batch(build_result.transactions, 209 | build_result.signature) 210 | self.assertEqual(len(transactions), 1) 211 | try: 212 | result = await transactions[0].await_committed(attempts=1000, attempts_timeout=1000) 213 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 214 | except Exception as ex: 215 | assert False, f"test_build_batch_withdraw_nft, transaction has failed with error: {ex}" 216 | 217 | async def test_build_batch_swap(self): 218 | nonce = await self.wallet.zk_provider.get_account_nonce(self.wallet.address()) 219 | nonce0 = await self.wallets[0].zk_provider.get_account_nonce(self.wallets[0].address()) 220 | builder = BatchBuilder.from_wallet(self.wallet, nonce) 221 | test_n = 2 222 | for i in range(test_n): 223 | order1 = await self.wallet.get_order('USDT', 224 | 'ETH', 225 | Fraction(1500, 1), 226 | RatioType.token, 227 | Decimal('0.1') 228 | , nonce=nonce + i 229 | ) 230 | order2 = await self.wallets[0].get_order('ETH', 231 | 'USDT', 232 | Fraction(1, 1200), 233 | RatioType.token, 234 | Decimal('0.00007'), 235 | nonce=nonce0 + i) 236 | builder.add_swap((order1, order2), 'ETH') 237 | build_result = await builder.build() 238 | print(f"Total fees: {build_result.total_fees}") 239 | transactions = await self.wallet.zk_provider.submit_batch_builder_txs_batch(build_result.transactions, 240 | build_result.signature) 241 | self.assertEqual(len(transactions), test_n) 242 | for i, tran in enumerate(transactions): 243 | try: 244 | result = await tran.await_committed(attempts=1000, attempts_timeout=1000) 245 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 246 | except Exception as ex: 247 | assert False, f"test_build_batch_swap, transaction {i} " \ 248 | f"has failed with error: {ex}" 249 | 250 | async def test_forced_exit(self): 251 | result_transaction = await self.wallet.transfer(self.forced_exit_account_address, Decimal("0.1"), "USDC") 252 | result = await result_transaction.await_committed() 253 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 254 | tr = await self.wallet.forced_exit(self.forced_exit_account_address, "USDC") 255 | try: 256 | result = await tr.await_verified(attempts=10, attempts_timeout=1000) 257 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 258 | except Exception as ex: 259 | assert False, f"test_forced_exit, getting transaction result has failed with error: {result.error_message}" 260 | 261 | async def test_mint_nft(self): 262 | tr = await self.wallet.mint_nft("0x0000000000000000000000000000000000000000000000000000000000000123", 263 | self.receiver_address, "USDC") 264 | try: 265 | result = await tr.await_committed(attempts=20, attempts_timeout=100) 266 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 267 | except Exception as ex: 268 | assert False, f"test_mint_nft, getting transaction result has failed with error: {ex}" 269 | 270 | async def test_transfer_nft(self): 271 | """ 272 | INFO: During the testing there are cases when this wallet does not own any NFT tokens by default, 273 | use mint_nft to VERIFIED state took too long and failed 274 | There are 2 solutions for the whole situation: 275 | 1. Prepare the docker with local ZkSync & Eth servers & achieve VERIFIED state fast => 276 | Any token or data can be transfered/deposited inside the test and do manipulations 277 | 2. If this wallet does not have NFT tokens do nothing 278 | Currently this choise is made 279 | 280 | PS: previous version of the tests was passing due to no one does not test the trasaction result 281 | it failed 282 | """ 283 | 284 | account_state = await self.wallet.zk_provider.get_state(self.nft_transfer_account_address) 285 | nfts = account_state.verified.nfts.items() 286 | first_value = None 287 | for key, value in nfts: 288 | if value.content_hash == "0x0000000000000000000000000000000000000000000000000000000000000123": 289 | first_value = value 290 | break 291 | if first_value is None: 292 | return 293 | 294 | txs = await self.wallet.transfer_nft( 295 | self.wallet.address(), 296 | first_value, 297 | "USDC", 298 | Decimal(0.01) 299 | ) 300 | self.assertEqual(len(txs), 2) 301 | for i, tr in enumerate(txs): 302 | try: 303 | result = await tr.await_committed(attempts=1000, attempts_timeout=1000) 304 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 305 | except Exception as ex: 306 | assert False, f"test_transfer_nft, transaction {i} has failed with error: {ex}" 307 | 308 | async def test_withdraw_nft(self): 309 | """ 310 | INFO: During the testing there are cases when this wallet does not own any NFT tokens by default, 311 | use mint_nft to VERIFIED state took too long and failed 312 | There are 2 solutions for the whole situation: 313 | 1. Prepare the docker with local ZkSync & Eth servers & achieve VERIFIED state fast => 314 | Any token or data can be transfered/deposited inside the test and do manipulations 315 | 2. If this wallet does not have NFT tokens do nothing 316 | Currently this choise is made 317 | 318 | PS: previous version of the tests was passing due to no one does not test the trasaction result 319 | it failed 320 | """ 321 | account_state = await self.wallet.zk_provider.get_state(self.wallet.address()) 322 | 323 | nfts = account_state.verified.nfts.values() 324 | if not nfts: 325 | return 326 | nfts_iter = iter(nfts) 327 | first_value = next(nfts_iter) 328 | tr = await self.wallet.withdraw_nft(self.nft_transfer_account_address, first_value, "USDC") 329 | try: 330 | result = await tr.await_committed(attempts=1000, attempts_timeout=1000) 331 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 332 | except Exception as ex: 333 | assert False, f"test_withdraw_nft, transaction has failed with error: {ex}" 334 | 335 | async def test_withdraw(self): 336 | tr = await self.wallet.withdraw(self.receiver_address, 337 | Decimal("0.000001"), "USDT") 338 | try: 339 | result = await tr.await_committed(attempts=30, attempts_timeout=100) 340 | self.assertEqual(result.status, TransactionStatus.COMMITTED) 341 | except Exception as ex: 342 | assert False, f"test_withdraw, transaction has failed with error: {ex}" 343 | 344 | async def test_get_tokens(self): 345 | tokens = await self.wallet.zk_provider.get_tokens() 346 | assert tokens.find_by_symbol("ETH") 347 | 348 | async def test_is_signing_key_set(self): 349 | assert await self.wallet.is_signing_key_set() 350 | 351 | async def test_toggle_2fa(self): 352 | """ 353 | Relate to the server-side code it must be Owned type if enable_2fa is passed 354 | let new_type = if toggle_2fa.enable { 355 | EthAccountType::Owned 356 | } else { 357 | EthAccountType::No2FA 358 | }; 359 | """ 360 | result = await self.wallet.enable_2fa() 361 | self.assertTrue(result) 362 | account_state = await self.wallet.get_account_state() 363 | self.assertEqual(AccountTypes.OWNED, account_state.account_type) 364 | 365 | pub_key_hash = self.wallet.zk_signer.pubkey_hash_str() 366 | result = await self.wallet.disable_2fa(pub_key_hash) 367 | self.assertTrue(result) 368 | account_state = await self.wallet.get_account_state() 369 | self.assertEqual(AccountTypes.NO_2FA, account_state.account_type) 370 | 371 | 372 | class TestEthereumProvider(IsolatedAsyncioTestCase): 373 | private_key = "0xa045b52470d306ff78e91b0d2d92f90f7504189125a46b69423dc673fd6b4f3e" 374 | 375 | async def asyncSetUp(self) -> None: 376 | self.account = Account.from_key(self.private_key) 377 | self.library = ZkSyncLibrary() 378 | 379 | w3 = Web3(HTTPProvider( 380 | endpoint_uri="https://rinkeby.infura.io/v3/bcf42e619a704151a1b0d95a35cb2e62")) 381 | provider = ZkSyncProviderV01(provider=HttpJsonRPCTransport(network=rinkeby)) 382 | address = await provider.get_contract_address() 383 | self.zksync = ZkSync(account=self.account, web3=w3, 384 | zksync_contract_address=address.main_contract) 385 | self.ethereum_provider = EthereumProvider(w3, self.zksync) 386 | 387 | async def test_approve_deposit(self): 388 | token = Token( 389 | address=Web3.toChecksumAddress('0xeb8f08a975ab53e34d8a0330e0d34de942c95926'), 390 | id=20, symbol='USDC', 391 | decimals=18) 392 | assert await self.ethereum_provider.approve_deposit(token, Decimal(1)) 393 | 394 | async def test_full_exit(self): 395 | token = Token( 396 | address=Web3.toChecksumAddress('0xD2084eA2AE4bBE1424E4fe3CDE25B713632fb988'), 397 | id=20, symbol='BAT', 398 | decimals=18) 399 | assert await self.ethereum_provider.full_exit(token, 6713) 400 | 401 | async def test_full_exit_nft(self): 402 | """ 403 | INFO: made by getting all NFT by corresponded address & dumping, 404 | Symbol: 'NFT-70848' 405 | '70848' 406 | address: '0x5e71f0f9b891f22d79ff8697dd4e3e0db371cda5' 407 | creator_address: '0x995a8b7f96cb837533b79775b6209696d51f435c' 408 | id: 70848 409 | account_id: 36357 410 | """ 411 | account_id = 36357 412 | token = Token( 413 | address=Web3.toChecksumAddress('0x5e71f0f9b891f22d79ff8697dd4e3e0db371cda5'), 414 | id=70848, 415 | symbol='NFT-70848', 416 | decimals=0 417 | ) 418 | assert await self.ethereum_provider.full_exit_nft(token, account_id) 419 | 420 | async def test_is_deposit_approved(self): 421 | token = Token( 422 | address=Web3.toChecksumAddress('0xD2084eA2AE4bBE1424E4fe3CDE25B713632fb988'), 423 | id=20, symbol='BAT', 424 | decimals=18) 425 | assert await self.ethereum_provider.is_deposit_approved(token, 1) 426 | 427 | 428 | class TestZkSyncProvider(IsolatedAsyncioTestCase): 429 | def setUp(self) -> None: 430 | self.provider = ZkSyncProviderV01(provider=HttpJsonRPCTransport(network=rinkeby)) 431 | 432 | async def test_get_token_price(self): 433 | tokens = await self.provider.get_tokens() 434 | price = await self.provider.get_token_price(tokens.find_by_symbol("USDC")) 435 | self.assertAlmostEqual(float(price), 1.0, delta=0.2) 436 | -------------------------------------------------------------------------------- /zksync_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | from .ethereum_provider import EthereumProvider 2 | from .ethereum_signer import EthereumSignerWeb3 3 | from .lib import ZkSyncLibrary 4 | from .transport.http import HttpJsonRPCTransport 5 | from .wallet import Wallet 6 | from .zksync import ZkSync 7 | from .zksync_provider import ZkSyncProviderV01 8 | from .zksync_signer import ZkSyncSigner 9 | -------------------------------------------------------------------------------- /zksync_sdk/contract_abi/IERC20.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "constant": false, 5 | "inputs": [ 6 | { 7 | "name": "spender", 8 | "type": "address" 9 | }, 10 | { 11 | "name": "amount", 12 | "type": "uint256" 13 | } 14 | ], 15 | "name": "approve", 16 | "outputs": [ 17 | { 18 | "name": "", 19 | "type": "bool" 20 | } 21 | ], 22 | "payable": false, 23 | "stateMutability": "nonpayable", 24 | "type": "function" 25 | }, 26 | { 27 | "constant": true, 28 | "inputs": [], 29 | "name": "totalSupply", 30 | "outputs": [ 31 | { 32 | "name": "", 33 | "type": "uint256" 34 | } 35 | ], 36 | "payable": false, 37 | "stateMutability": "view", 38 | "type": "function" 39 | }, 40 | { 41 | "constant": false, 42 | "inputs": [ 43 | { 44 | "name": "sender", 45 | "type": "address" 46 | }, 47 | { 48 | "name": "recipient", 49 | "type": "address" 50 | }, 51 | { 52 | "name": "amount", 53 | "type": "uint256" 54 | } 55 | ], 56 | "name": "transferFrom", 57 | "outputs": [ 58 | { 59 | "name": "", 60 | "type": "bool" 61 | } 62 | ], 63 | "payable": false, 64 | "stateMutability": "nonpayable", 65 | "type": "function" 66 | }, 67 | { 68 | "constant": true, 69 | "inputs": [ 70 | { 71 | "name": "account", 72 | "type": "address" 73 | } 74 | ], 75 | "name": "balanceOf", 76 | "outputs": [ 77 | { 78 | "name": "", 79 | "type": "uint256" 80 | } 81 | ], 82 | "payable": false, 83 | "stateMutability": "view", 84 | "type": "function" 85 | }, 86 | { 87 | "constant": false, 88 | "inputs": [ 89 | { 90 | "name": "recipient", 91 | "type": "address" 92 | }, 93 | { 94 | "name": "amount", 95 | "type": "uint256" 96 | } 97 | ], 98 | "name": "transfer", 99 | "outputs": [ 100 | { 101 | "name": "", 102 | "type": "bool" 103 | } 104 | ], 105 | "payable": false, 106 | "stateMutability": "nonpayable", 107 | "type": "function" 108 | }, 109 | { 110 | "constant": true, 111 | "inputs": [ 112 | { 113 | "name": "owner", 114 | "type": "address" 115 | }, 116 | { 117 | "name": "spender", 118 | "type": "address" 119 | } 120 | ], 121 | "name": "allowance", 122 | "outputs": [ 123 | { 124 | "name": "", 125 | "type": "uint256" 126 | } 127 | ], 128 | "payable": false, 129 | "stateMutability": "view", 130 | "type": "function" 131 | }, 132 | { 133 | "anonymous": false, 134 | "inputs": [ 135 | { 136 | "indexed": true, 137 | "name": "from", 138 | "type": "address" 139 | }, 140 | { 141 | "indexed": true, 142 | "name": "to", 143 | "type": "address" 144 | }, 145 | { 146 | "indexed": false, 147 | "name": "value", 148 | "type": "uint256" 149 | } 150 | ], 151 | "name": "Transfer", 152 | "type": "event" 153 | }, 154 | { 155 | "anonymous": false, 156 | "inputs": [ 157 | { 158 | "indexed": true, 159 | "name": "owner", 160 | "type": "address" 161 | }, 162 | { 163 | "indexed": true, 164 | "name": "spender", 165 | "type": "address" 166 | }, 167 | { 168 | "indexed": false, 169 | "name": "value", 170 | "type": "uint256" 171 | } 172 | ], 173 | "name": "Approval", 174 | "type": "event" 175 | } 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /zksync_sdk/contract_abi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zksync-sdk/zksync-python/92abc5c73be8f2b9c23fa786b050ece95acbd663/zksync_sdk/contract_abi/__init__.py -------------------------------------------------------------------------------- /zksync_sdk/contract_utils.py: -------------------------------------------------------------------------------- 1 | import importlib.resources as pkg_resources 2 | import json 3 | 4 | from . import contract_abi 5 | 6 | zksync_abi_cache = None 7 | ierc20_abi_cache = None 8 | 9 | __all__ = ['zksync_abi', 'erc20_abi'] 10 | 11 | 12 | def zksync_abi(): 13 | global zksync_abi_cache 14 | 15 | if zksync_abi_cache is None: 16 | abi_text = pkg_resources.read_text(contract_abi, 'ZkSync.json') 17 | zksync_abi_cache = json.loads(abi_text)['abi'] 18 | 19 | return zksync_abi_cache 20 | 21 | 22 | def erc20_abi(): 23 | global ierc20_abi_cache 24 | 25 | if ierc20_abi_cache is None: 26 | abi_text = pkg_resources.read_text(contract_abi, 'IERC20.json') 27 | ierc20_abi_cache = json.loads(abi_text)['abi'] 28 | 29 | return ierc20_abi_cache 30 | -------------------------------------------------------------------------------- /zksync_sdk/ethereum_provider.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Optional 3 | 4 | from web3 import Web3 5 | 6 | from zksync_sdk.types import Token 7 | from zksync_sdk.zksync import ERC20Contract, ZkSync 8 | 9 | DEFAULT_AUTH_FACTS = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 10 | 11 | 12 | class EthereumProvider: 13 | def __init__(self, web3: Web3, zksync: ZkSync): 14 | self.web3 = web3 15 | self.zksync = zksync 16 | 17 | async def approve_deposit(self, token: Token, limit: Decimal): 18 | contract = ERC20Contract(self.web3, self.zksync.contract_address, token.address, 19 | self.zksync.account) 20 | return contract.approve_deposit(token.from_decimal(limit)) 21 | 22 | async def deposit(self, token: Token, amount: Decimal, address: str): 23 | if token.is_eth(): 24 | return self.zksync.deposit_eth(address, token.from_decimal(amount)) 25 | else: 26 | return self.zksync.deposit_erc20(token.address, address, token.from_decimal(amount)) 27 | 28 | async def full_exit(self, token: Token, account_id: int): 29 | return self.zksync.full_exit(account_id, token.address) 30 | 31 | async def full_exit_nft(self, nft: Token, account_id: int): 32 | return self.zksync.full_exit_nft(account_id, nft.id) 33 | 34 | async def set_auth_pubkey_hash(self, pubkey_hash: bytes, nonce: int): 35 | return self.zksync.set_auth_pub_key_hash(pubkey_hash, nonce) 36 | 37 | async def is_deposit_approved(self, token: Token, threshold: int) -> bool: 38 | contract = ERC20Contract(self.web3, self.zksync.contract_address, token.address, 39 | self.zksync.account) 40 | return contract.is_deposit_approved(threshold) 41 | 42 | async def is_onchain_auth_pubkey_hash_set(self, nonce: int) -> bool: 43 | auth_facts = self.zksync.auth_facts(self.zksync.account.address, nonce) 44 | return auth_facts != DEFAULT_AUTH_FACTS 45 | -------------------------------------------------------------------------------- /zksync_sdk/ethereum_signer/__init__.py: -------------------------------------------------------------------------------- 1 | from .interface import * 2 | from .web3 import * 3 | -------------------------------------------------------------------------------- /zksync_sdk/ethereum_signer/interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from zksync_sdk.types import EncodedTx, TxEthSignature 4 | 5 | __all__ = ['EthereumSignerInterface'] 6 | 7 | 8 | class EthereumSignerInterface(ABC): 9 | 10 | @abstractmethod 11 | def sign_tx(self, tx: EncodedTx) -> TxEthSignature: 12 | raise NotImplementedError 13 | 14 | @abstractmethod 15 | def sign(self, message: bytes) -> TxEthSignature: 16 | raise NotImplementedError 17 | 18 | @abstractmethod 19 | def address(self) -> str: 20 | raise NotImplementedError 21 | -------------------------------------------------------------------------------- /zksync_sdk/ethereum_signer/web3.py: -------------------------------------------------------------------------------- 1 | from eth_account.messages import encode_defunct 2 | from eth_account.signers.base import BaseAccount 3 | from zksync_sdk.ethereum_signer.interface import EthereumSignerInterface 4 | from zksync_sdk.types import EncodedTx, SignatureType, TxEthSignature 5 | 6 | __all__ = ['EthereumSignerWeb3'] 7 | 8 | 9 | class EthereumSignerWeb3(EthereumSignerInterface): 10 | def __init__(self, account: BaseAccount): 11 | self.account = account 12 | 13 | def sign_tx(self, tx: EncodedTx) -> TxEthSignature: 14 | message = tx.human_readable_message() 15 | return self.sign(message.encode()) 16 | 17 | def sign(self, message: bytes) -> TxEthSignature: 18 | signature = self.account.sign_message(encode_defunct(message)) 19 | return TxEthSignature(signature=signature.signature, sig_type=SignatureType.ethereum_signature) 20 | 21 | def address(self) -> str: 22 | return self.account.address 23 | -------------------------------------------------------------------------------- /zksync_sdk/lib.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from ctypes import (Structure, c_ubyte, cdll) 3 | import os 4 | from typing import Optional 5 | 6 | PRIVATE_KEY_LEN = 32 7 | PUBLIC_KEY_LEN = 32 8 | PUBKEY_HASH_LEN = 20 9 | PACKED_SIGNATURE_LEN = 64 10 | ORDER_LEN = 89 11 | ORDERS_HASH_LEN = 31 12 | 13 | 14 | class ZksPrivateKey(Structure): 15 | _fields_ = [ 16 | ("data", c_ubyte * PRIVATE_KEY_LEN), 17 | ] 18 | 19 | 20 | class ZksPackedPublicKey(Structure): 21 | _fields_ = [ 22 | ("data", c_ubyte * PUBLIC_KEY_LEN), 23 | ] 24 | 25 | 26 | class ZksPubkeyHash(Structure): 27 | _fields_ = [ 28 | ("data", c_ubyte * PUBKEY_HASH_LEN), 29 | ] 30 | 31 | 32 | class ZksSignature(Structure): 33 | _fields_ = [ 34 | ("data", c_ubyte * PACKED_SIGNATURE_LEN), 35 | ] 36 | 37 | 38 | class ZksOrdersHash(Structure): 39 | _fields_ = [ 40 | ("data", c_ubyte * ORDERS_HASH_LEN), 41 | ] 42 | 43 | 44 | class ZksOrders(Structure): 45 | _fields_ = [ 46 | ("data", c_ubyte * (ORDER_LEN * 2)), 47 | ] 48 | 49 | 50 | class ZkSyncLibrary: 51 | 52 | def __init__(self, library_path: Optional[str] = None): 53 | if library_path is None: 54 | library_path = os.environ["ZK_SYNC_LIBRARY_PATH"] 55 | self.lib = cdll.LoadLibrary(library_path) 56 | 57 | def private_key_from_seed(self, seed: bytes): 58 | private_key = ctypes.pointer(ZksPrivateKey()) 59 | self.lib.zks_crypto_private_key_from_seed(seed, len(seed), private_key) 60 | return bytes(private_key.contents.data) 61 | 62 | def get_public_key(self, private_key: bytes): 63 | assert len(private_key) == PRIVATE_KEY_LEN 64 | public_key = ctypes.pointer(ZksPackedPublicKey()) 65 | pk = ctypes.pointer(ZksPrivateKey(data=(c_ubyte * PRIVATE_KEY_LEN)(*private_key))) 66 | self.lib.zks_crypto_private_key_to_public_key(pk, public_key) 67 | return bytes(public_key.contents.data) 68 | 69 | def get_pubkey_hash(self, public_key: bytes): 70 | assert len(public_key) == PUBLIC_KEY_LEN 71 | public_key_hash = ctypes.pointer(ZksPubkeyHash()) 72 | public_key_ptr = ctypes.pointer( 73 | ZksPackedPublicKey(data=(c_ubyte * PUBLIC_KEY_LEN)(*public_key))) 74 | self.lib.zks_crypto_public_key_to_pubkey_hash(public_key_ptr, public_key_hash) 75 | return bytes(public_key_hash.contents.data) 76 | 77 | def sign(self, private_key: bytes, message: bytes): 78 | assert len(private_key) == PRIVATE_KEY_LEN 79 | signature = ctypes.pointer(ZksSignature()) 80 | private_key_ptr = ctypes.pointer( 81 | ZksPrivateKey(data=(c_ubyte * PRIVATE_KEY_LEN)(*private_key))) 82 | self.lib.zks_crypto_sign_musig(private_key_ptr, message, len(message), signature) 83 | return bytes(signature.contents.data) 84 | 85 | def hash_orders(self, orders: bytes): 86 | assert len(orders) == ORDER_LEN * 2 87 | orders_hash = ctypes.pointer(ZksOrdersHash()) 88 | orders_bytes = ctypes.pointer( 89 | ZksOrders(data=(c_ubyte * (ORDER_LEN * 2))(*orders))) 90 | self.lib.rescue_hash_orders(orders_bytes, len(orders), orders_hash) 91 | return bytes(orders_hash.contents.data) 92 | 93 | def is_valid_signature(self, message: bytes, public_key: bytes, zk_sync_signature: bytes) -> bool: 94 | assert len(public_key) == PUBLIC_KEY_LEN 95 | assert len(zk_sync_signature) == PACKED_SIGNATURE_LEN 96 | public_key_ptr = ctypes.pointer( 97 | ZksPackedPublicKey(data=(c_ubyte * PUBLIC_KEY_LEN)(*public_key))) 98 | signature_ptr = ctypes.pointer( 99 | ZksSignature(data=(c_ubyte * PACKED_SIGNATURE_LEN)(*zk_sync_signature))) 100 | ret = self.lib.zks_crypto_verify_musig(message, len(message), public_key_ptr, signature_ptr) 101 | return ret == 0 102 | -------------------------------------------------------------------------------- /zksync_sdk/network.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from zksync_sdk.types import ChainId 4 | 5 | 6 | @dataclass 7 | class Network: 8 | zksync_url: str 9 | chain_id: ChainId 10 | 11 | 12 | rinkeby = Network(zksync_url="https://rinkeby-api.zksync.io/jsrpc", chain_id=ChainId.RINKEBY) 13 | ropsten = Network(zksync_url="https://ropsten-api.zksync.io/jsrpc", chain_id=ChainId.ROPSTEN) 14 | mainnet = Network(zksync_url="https://api.zksync.io/jsrpc", chain_id=ChainId.MAINNET) 15 | goerli = Network(zksync_url="https://goerli-api.zksync.io/jsrpc", chain_id=ChainId.GOERLI) 16 | sepolia = Network(zksync_url="https://sepolia-api.zksync.io/jsrpc", chain_id=ChainId.SEPOLIA) 17 | localhost = Network(zksync_url="http://localhost:3030/jsrpc", chain_id=ChainId.LOCALHOST) 18 | -------------------------------------------------------------------------------- /zksync_sdk/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 2 | -------------------------------------------------------------------------------- /zksync_sdk/serializers.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from math import ceil 3 | 4 | AMOUNT_EXPONENT_BIT_WIDTH = 5 5 | AMOUNT_MANTISSA_BIT_WIDTH = 35 6 | FEE_EXPONENT_BIT_WIDTH = 5 7 | FEE_MANTISSA_BIT_WIDTH = 11 8 | MAX_NUMBER_OF_ACCOUNTS = 2 ** 24 9 | MAX_NUMBER_OF_TOKENS = 2 ** 32 - 1 10 | 11 | 12 | class SerializationError(Exception): 13 | pass 14 | 15 | 16 | class WrongIntegerError(SerializationError): 17 | pass 18 | 19 | 20 | class WrongBitsError(SerializationError): 21 | pass 22 | 23 | 24 | class ValueNotPackedError(SerializationError): 25 | pass 26 | 27 | 28 | class WrongValueError(SerializationError): 29 | pass 30 | 31 | 32 | def int_to_bytes(val: int, length=4): 33 | return val.to_bytes(length, byteorder='big') 34 | 35 | 36 | def num_to_bits(integer: int, bits: int): 37 | """ 38 | INFO: Can't be used without correct input data of value & corresponded amount of bits, take care 39 | """ 40 | results = [] 41 | for i in range(bits): 42 | results.append(integer & 1) 43 | integer //= 2 44 | return results 45 | 46 | 47 | def integer_to_float(integer: int, exp_bits: int, mantissa_bits: int, exp_base: int) -> List[int]: 48 | max_exponent_power = 2 ** exp_bits - 1 49 | max_exponent = exp_base ** max_exponent_power 50 | max_mantissa = 2 ** mantissa_bits - 1 51 | if integer > max_mantissa * max_exponent: 52 | raise WrongIntegerError 53 | 54 | exponent = 0 55 | exponent_temp = 1 56 | while integer > max_mantissa * exponent_temp: 57 | exponent_temp = exponent_temp * exp_base 58 | exponent += 1 59 | mantissa = integer // exponent_temp 60 | if exponent != 0: 61 | variant1 = exponent_temp * mantissa 62 | variant2 = exponent_temp // exp_base * max_mantissa 63 | diff1 = integer - variant1 64 | diff2 = integer - variant2 65 | if diff2 < diff1: 66 | mantissa = max_mantissa 67 | exponent -= 1 68 | 69 | data = num_to_bits(exponent, exp_bits) + num_to_bits(mantissa, mantissa_bits) 70 | data = list(reversed(data)) 71 | result = list(reversed(bits_into_bytes_in_be_order(data))) 72 | 73 | return result 74 | 75 | 76 | def integer_to_float_up(integer: int, exp_bits: int, mantissa_bits: int, exp_base) -> List[int]: 77 | max_exponent_power = 2 ** exp_bits - 1 78 | max_exponent = exp_base ** max_exponent_power 79 | max_mantissa = 2 ** mantissa_bits - 1 80 | 81 | if integer > max_mantissa * max_exponent: 82 | raise WrongIntegerError("Integer is too big") 83 | 84 | exponent = 0 85 | exponent_temp = 1 86 | while integer > max_mantissa * exponent_temp: 87 | exponent_temp = exponent_temp * exp_base 88 | exponent += 1 89 | 90 | mantissa = int(ceil(integer / exponent_temp)) 91 | encoding = num_to_bits(exponent, exp_bits) + num_to_bits(mantissa, mantissa_bits) 92 | data = list(reversed(encoding)) 93 | result = list(reversed(bits_into_bytes_in_be_order(data))) 94 | 95 | return result 96 | 97 | 98 | def bits_into_bytes_in_be_order(bits: List[int]): 99 | if len(bits) % 8 != 0: 100 | raise WrongBitsError("wrong number of bits") 101 | size = len(bits) // 8 102 | result = [0] * size 103 | for i in range(size): 104 | value = 0 105 | for j in range(8): 106 | value |= bits[i * 8 + j] * 2 ** (7 - j) 107 | result[i] = value 108 | 109 | return result 110 | 111 | 112 | def reverse_bit(b): 113 | b = ((b & 0xf0) >> 4) | ((b & 0x0f) << 4) 114 | b = ((b & 0xcc) >> 2) | ((b & 0x33) << 2) 115 | b = ((b & 0xaa) >> 1) | ((b & 0x55) << 1) 116 | return b 117 | 118 | 119 | def reverse_bits(buffer: List[int]): 120 | return list(reversed(buffer)) 121 | 122 | 123 | def buffer_to_bits_be(buff): 124 | res = [0] * len(buff) * 8 125 | for i, b in enumerate(buff): 126 | for j in range(8): 127 | res[i * 8 + j] = (b >> (7 - j)) & 1 128 | return res 129 | 130 | 131 | def pack_fee(amount: int): 132 | return bytes(reverse_bits( 133 | integer_to_float(amount, FEE_EXPONENT_BIT_WIDTH, FEE_MANTISSA_BIT_WIDTH, 10) 134 | )) 135 | 136 | 137 | def pack_amount(amount: int) -> bytes: 138 | return bytes(reverse_bits( 139 | integer_to_float(amount, AMOUNT_EXPONENT_BIT_WIDTH, AMOUNT_MANTISSA_BIT_WIDTH, 10) 140 | )) 141 | 142 | 143 | def float_to_integer(float_bytes: bytes, exp_bits, mantissa_bits, exp_base_number): 144 | bits = list(reversed(buffer_to_bits_be(list(float_bytes)))) 145 | exponent = 0 146 | exp_pow2 = 1 147 | for i in range(exp_bits): 148 | if bits[i] == 1: 149 | exponent += exp_pow2 150 | exp_pow2 *= 2 151 | exponent = exp_base_number ** exponent 152 | mantissa = 0 153 | mantissa_pow2 = 1 154 | for i in range(exp_bits, exp_bits + mantissa_bits): 155 | if bits[i] == 1: 156 | mantissa += mantissa_pow2 157 | mantissa_pow2 *= 2 158 | 159 | return exponent * mantissa 160 | 161 | 162 | def pack_amount_up(amount: int): 163 | return bytes(reverse_bits( 164 | integer_to_float_up(amount, AMOUNT_EXPONENT_BIT_WIDTH, AMOUNT_MANTISSA_BIT_WIDTH, 10) 165 | )) 166 | 167 | 168 | def pack_fee_up(fee: int): 169 | return bytes(reverse_bits( 170 | integer_to_float_up(fee, FEE_EXPONENT_BIT_WIDTH, FEE_MANTISSA_BIT_WIDTH, 10) 171 | )) 172 | 173 | 174 | def closest_packable_amount(amount: int) -> int: 175 | packed_amount = pack_amount(amount) 176 | return float_to_integer( 177 | packed_amount, 178 | AMOUNT_EXPONENT_BIT_WIDTH, 179 | AMOUNT_MANTISSA_BIT_WIDTH, 180 | 10 181 | ) 182 | 183 | 184 | def closest_greater_or_eq_packable_amount(amount: int) -> int: 185 | packed_amount = pack_amount_up(amount) 186 | return float_to_integer(packed_amount, AMOUNT_EXPONENT_BIT_WIDTH, AMOUNT_MANTISSA_BIT_WIDTH, 10) 187 | 188 | 189 | def closest_packable_transaction_fee(fee: int) -> int: 190 | packed_fee = pack_fee(fee) 191 | return float_to_integer( 192 | packed_fee, 193 | FEE_EXPONENT_BIT_WIDTH, 194 | FEE_MANTISSA_BIT_WIDTH, 195 | 10 196 | ) 197 | 198 | 199 | def closest_greater_or_eq_packable_fee(fee: int) -> int: 200 | packed_fee = pack_fee_up(fee) 201 | return float_to_integer(packed_fee, FEE_EXPONENT_BIT_WIDTH, FEE_MANTISSA_BIT_WIDTH, 10) 202 | 203 | 204 | def packed_fee_checked(fee: int): 205 | if closest_packable_transaction_fee(fee) != fee: 206 | raise ValueNotPackedError 207 | return pack_fee(fee) 208 | 209 | 210 | def packed_amount_checked(amount: int): 211 | if closest_packable_amount(amount) != amount: 212 | raise ValueNotPackedError 213 | return pack_amount(amount) 214 | 215 | 216 | def serialize_nonce(nonce: int): 217 | if nonce < 0: 218 | raise WrongValueError 219 | return int_to_bytes(nonce, 4) 220 | 221 | 222 | def serialize_timestamp(timestamp: int): 223 | if timestamp < 0: 224 | raise WrongValueError 225 | return b"\x00" * 4 + int_to_bytes(timestamp, 4) 226 | 227 | 228 | def serialize_token_id(token_id: int): 229 | if token_id < 0: 230 | raise WrongValueError 231 | if token_id > MAX_NUMBER_OF_TOKENS: 232 | raise WrongValueError 233 | return int_to_bytes(token_id, 4) 234 | 235 | 236 | def serialize_account_id(account_id: int): 237 | if account_id < 0: 238 | raise WrongValueError 239 | if account_id > MAX_NUMBER_OF_ACCOUNTS: 240 | raise WrongValueError 241 | return int_to_bytes(account_id, 4) 242 | 243 | 244 | def remove_address_prefix(address: str) -> str: 245 | if address.startswith('0x'): 246 | return address[2:] 247 | 248 | if address.startswith('sync:'): 249 | return address[5:] 250 | 251 | return address 252 | 253 | 254 | def serialize_address(address: str) -> bytes: 255 | address = remove_address_prefix(address) 256 | address_bytes = bytes.fromhex(address) 257 | if len(address_bytes) != 20: 258 | raise WrongValueError 259 | return address_bytes 260 | 261 | 262 | def serialize_content_hash(content_hash: str) -> bytes: 263 | if content_hash.startswith('0x'): 264 | content_hash = content_hash[2:] 265 | return bytes.fromhex(content_hash) 266 | 267 | 268 | def serialize_ratio_part(part: int) -> bytes: 269 | # turn the number into bytes and 0-pad to length 15 270 | return bytes.fromhex(hex(part)[2:].zfill(15 * 2)) 271 | -------------------------------------------------------------------------------- /zksync_sdk/transport/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, List, Optional 3 | 4 | 5 | class ProviderError(Exception): 6 | def __init__(self, basic_response, *args): 7 | self.basic_response = basic_response 8 | super().__init__(*args) 9 | 10 | 11 | class ResponseError(Exception): 12 | def __init__(self, code, text, *args): 13 | self.code = code 14 | self.text = text 15 | super().__init__(*args) 16 | 17 | def __str__(self): 18 | return f"Response error with code {self.code} \n {self.text}" 19 | 20 | 21 | class JsonRPCTransport(ABC): 22 | @abstractmethod 23 | async def request(self, method: str, params: Optional[List]) -> Any: 24 | pass 25 | 26 | def create_request(self, method: str, params=None): 27 | return { 28 | "id": 1, 29 | "jsonrpc": '2.0', 30 | "method": method, 31 | "params": params 32 | } 33 | -------------------------------------------------------------------------------- /zksync_sdk/transport/http.py: -------------------------------------------------------------------------------- 1 | from http.client import OK 2 | from typing import List, Optional 3 | 4 | import httpx 5 | 6 | from . import JsonRPCTransport, ProviderError, ResponseError 7 | from ..network import Network 8 | 9 | 10 | class HttpJsonRPCTransport(JsonRPCTransport): 11 | def __init__(self, network: Network): 12 | self.network = network 13 | 14 | async def request(self, method: str, params: Optional[List]): 15 | async with httpx.AsyncClient() as client: 16 | response = await client.post(self.network.zksync_url, json=self.create_request(method, params)) 17 | if response.status_code == OK: 18 | result = response.json() 19 | if "error" in result: 20 | data = result["error"] 21 | raise ResponseError(data['code'], data['message']) 22 | else: 23 | return result['result'] 24 | else: 25 | raise ProviderError(response, "Unexpected status code") 26 | -------------------------------------------------------------------------------- /zksync_sdk/types/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from .responses import * 4 | from .signatures import * 5 | from .transactions import * 6 | from .auth_types import * 7 | 8 | 9 | class ChainId(IntEnum): 10 | MAINNET = 1 11 | RINKEBY = 4 12 | ROPSTEN = 3 13 | GOERLI = 420 14 | SEPOLIA = 11155111 15 | LOCALHOST = 9 16 | -------------------------------------------------------------------------------- /zksync_sdk/types/auth_types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from dataclasses import dataclass 3 | from typing import Optional 4 | from zksync_sdk.types.signatures import TxEthSignature 5 | 6 | 7 | class ChangePubKeyTypes(Enum): 8 | onchain = "Onchain" 9 | ecdsa = "ECDSA" 10 | create2 = "CREATE2" 11 | 12 | 13 | @dataclass 14 | class ChangePubKeyEcdsa: 15 | batch_hash: bytes = b"\x00" * 32 16 | 17 | def encode_message(self) -> bytes: 18 | return self.batch_hash 19 | 20 | def dict(self, signature: str): 21 | return {"type": "ECDSA", 22 | "ethSignature": signature, 23 | "batchHash": f"0x{self.batch_hash.hex()}"} 24 | 25 | 26 | @dataclass 27 | class ChangePubKeyCREATE2: 28 | creator_address: str 29 | salt_arg: bytes 30 | code_hash: bytes 31 | 32 | def encode_message(self) -> bytes: 33 | return self.salt_arg 34 | 35 | def dict(self): 36 | return {"type": "CREATE2", 37 | "saltArg": f"0x{self.salt_arg.hex()}", 38 | "codeHash": f"0x{self.code_hash.hex()}"} 39 | 40 | 41 | @dataclass 42 | class Toggle2FA: 43 | enable: bool 44 | account_id: int 45 | time_stamp_milliseconds: int 46 | signature: TxEthSignature 47 | pub_key_hash: Optional[str] 48 | 49 | def dict(self): 50 | if self.pub_key_hash is not None: 51 | return { 52 | "enable": self.enable, 53 | "accountId": self.account_id, 54 | "timestamp": self.time_stamp_milliseconds, 55 | "signature": self.signature.dict(), 56 | "pubKeyHash": self.pub_key_hash 57 | } 58 | else: 59 | return { 60 | "enable": self.enable, 61 | "accountId": self.account_id, 62 | "timestamp": self.time_stamp_milliseconds, 63 | "signature": self.signature.dict(), 64 | } 65 | 66 | 67 | def get_toggle_message(require_2fa: bool, time_stamp: int) -> str: 68 | if require_2fa: 69 | msg = f"By signing this message, you are opting into Two-factor Authentication protection by the zkSync " \ 70 | f"Server.\n" \ 71 | f"Transactions now require signatures by both your L1 and L2 private key.\n" \ 72 | f"Timestamp: {time_stamp}" 73 | else: 74 | msg = f"You are opting out of Two-factor Authentication protection by the zkSync Server.\n" \ 75 | f"Transactions now only require signatures by your L2 private key.\n" \ 76 | f"BY SIGNING THIS MESSAGE, YOU ARE TRUSTING YOUR WALLET CLIENT TO KEEP YOUR L2 PRIVATE KEY SAFE!\n" \ 77 | f"Timestamp: {time_stamp}" 78 | return msg 79 | 80 | 81 | def get_toggle_message_with_pub(require_2fa: bool, time_stamp: int, pub_key_hash: str) -> str: 82 | msg = get_toggle_message(require_2fa, time_stamp) 83 | msg += f"\nPubKeyHash: {pub_key_hash}" 84 | return msg 85 | -------------------------------------------------------------------------------- /zksync_sdk/types/responses.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | from enum import Enum 3 | from decimal import Decimal 4 | from zksync_sdk.types.transactions import Token 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | def to_camel(string: str) -> str: 10 | first, *others = string.split('_') 11 | return ''.join([first.lower(), *map(str.title, others)]) 12 | 13 | 14 | class Balance(BaseModel): 15 | amount: int 16 | expected_accept_block: int 17 | 18 | class Config: 19 | alias_generator = to_camel 20 | 21 | 22 | class Depositing(BaseModel): 23 | balances: Dict[str, Balance] 24 | 25 | 26 | class NFT(Token): 27 | creator_id: int 28 | content_hash: str 29 | creator_address: str 30 | serial_id: int 31 | decimals = 0 32 | 33 | def decimal_amount(self, amount: int) -> Decimal: 34 | return Decimal(amount) 35 | 36 | class Config: 37 | alias_generator = to_camel 38 | 39 | 40 | class State(BaseModel): 41 | nonce: int 42 | pub_key_hash: str 43 | balances: Dict[str, int] 44 | nfts: Dict[str, NFT] 45 | minted_nfts: Dict[str, NFT] 46 | 47 | class Config: 48 | alias_generator = to_camel 49 | 50 | 51 | class AccountTypes(str, Enum): 52 | OWNED = "Owned", 53 | CREATE2 = "CREATE2", 54 | NO_2FA = "No2FA" 55 | 56 | 57 | class AccountState(BaseModel): 58 | address: str 59 | id: Optional[int] 60 | account_type: Optional[AccountTypes] 61 | depositing: Optional[Depositing] 62 | committed: Optional[State] 63 | verified: Optional[State] 64 | 65 | class Config: 66 | alias_generator = to_camel 67 | 68 | def get_nonce(self) -> int: 69 | assert self.committed is not None, "`get_nonce` needs `committed` to be set" 70 | return self.committed.nonce 71 | 72 | 73 | class Fee(BaseModel): 74 | fee_type: Any 75 | gas_tx_amount: int 76 | gas_price_wei: int 77 | gas_fee: int 78 | zkp_fee: int 79 | total_fee: int 80 | 81 | class Config: 82 | alias_generator = to_camel 83 | 84 | 85 | class ContractAddress(BaseModel): 86 | main_contract: str 87 | gov_contract: str 88 | 89 | class Config: 90 | alias_generator = to_camel 91 | 92 | 93 | class BlockInfo(BaseModel): 94 | block_number: int 95 | committed: bool 96 | verified: bool 97 | 98 | class Config: 99 | alias_generator = to_camel 100 | 101 | 102 | class EthOpInfo(BaseModel): 103 | executed: bool 104 | block: BlockInfo 105 | 106 | 107 | class TransactionDetails(BaseModel): 108 | executed: bool 109 | success: bool 110 | fail_reason: Optional[str] = None 111 | block: BlockInfo 112 | 113 | class Config: 114 | alias_generator = to_camel 115 | -------------------------------------------------------------------------------- /zksync_sdk/types/signatures.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | class SignatureType(Enum): 6 | ethereum_signature = "EthereumSignature" 7 | EIP1271_signature = "EIP1271Signature" 8 | 9 | 10 | @dataclass 11 | class TxEthSignature: 12 | sig_type: SignatureType 13 | signature: str 14 | 15 | @classmethod 16 | def from_dict(cls, json: dict): 17 | """ 18 | Only the difference from __init__ that signature is already in hex format 19 | """ 20 | obj = cls(sig_type=SignatureType(json["type"]), signature=b"") 21 | obj.signature = json["signature"] 22 | return obj 23 | 24 | def __init__(self, sig_type: SignatureType, signature: bytes): 25 | self.signature = signature.hex() 26 | self.sig_type = sig_type 27 | 28 | def dict(self): 29 | return { 30 | "type": self.sig_type.value, 31 | "signature": self.signature 32 | } 33 | 34 | 35 | @dataclass 36 | class TxSignature: 37 | public_key: str 38 | signature: str 39 | 40 | @classmethod 41 | def from_dict(cls, json: dict): 42 | """ 43 | Only the difference from __init__ is that values are already in hex format 44 | """ 45 | obj = cls(public_key=b"", signature=b"") 46 | obj.public_key = json["pubKey"] 47 | obj.signature = json["signature"] 48 | return obj 49 | 50 | def __init__(self, public_key: bytes, signature: bytes): 51 | self.public_key = public_key.hex() 52 | self.signature = signature.hex() 53 | 54 | def dict(self): 55 | return { 56 | "pubKey": self.public_key, 57 | "signature": self.signature 58 | } 59 | -------------------------------------------------------------------------------- /zksync_sdk/types/transactions.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from dataclasses import dataclass 3 | from decimal import Decimal 4 | from fractions import Fraction 5 | from enum import Enum, IntEnum 6 | from typing import List, Optional, Union, Tuple 7 | 8 | from pydantic import BaseModel 9 | from zksync_sdk.lib import ZkSyncLibrary 10 | from zksync_sdk.serializers import (int_to_bytes, packed_amount_checked, packed_fee_checked, 11 | serialize_account_id, 12 | serialize_address, serialize_content_hash, 13 | serialize_nonce, serialize_timestamp, 14 | serialize_token_id, serialize_ratio_part) 15 | from zksync_sdk.types.signatures import TxEthSignature, TxSignature 16 | from zksync_sdk.types.auth_types import ChangePubKeyCREATE2, ChangePubKeyEcdsa 17 | 18 | DEFAULT_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000" 19 | 20 | TokenLike = Union[str, int] 21 | 22 | TRANSACTION_VERSION = 0x01 23 | 24 | 25 | class EncodedTxType(IntEnum): 26 | CHANGE_PUB_KEY = 7 27 | TRANSFER = 5 28 | WITHDRAW = 3 29 | FORCED_EXIT = 8 30 | SWAP = 11 31 | MINT_NFT = 9 32 | WITHDRAW_NFT = 10 33 | 34 | 35 | class RatioType(Enum): 36 | # ratio that represents the lowest denominations of tokens (wei for ETH, satoshi for BTC etc.) 37 | wei = 'Wei', 38 | # ratio that represents tokens themselves 39 | token = 'Token' 40 | 41 | 42 | class Token(BaseModel): 43 | address: str 44 | id: int 45 | symbol: str 46 | decimals: int 47 | 48 | @classmethod 49 | def eth(cls): 50 | return cls(id=0, 51 | address=DEFAULT_TOKEN_ADDRESS, 52 | symbol="ETH", 53 | decimals=18) 54 | 55 | def is_eth(self) -> bool: 56 | return self.symbol == "ETH" and self.address == DEFAULT_TOKEN_ADDRESS 57 | 58 | def decimal_amount(self, amount: int) -> Decimal: 59 | return Decimal(amount).scaleb(-self.decimals) 60 | 61 | def from_decimal(self, amount: Decimal) -> int: 62 | return int(amount.scaleb(self.decimals)) 63 | 64 | def decimal_str_amount(self, amount: int) -> str: 65 | d = self.decimal_amount(amount) 66 | 67 | # Creates a string with `self.decimals` numbers after decimal point. 68 | # Prevents scientific notation (string values like '1E-8'). 69 | # Prevents integral numbers having no decimal point in the string representation. 70 | d_str = f"{d:.{self.decimals}f}" 71 | 72 | d_str = d_str.rstrip("0") 73 | if d_str[-1] == ".": 74 | return d_str + "0" 75 | 76 | if '.' not in d_str: 77 | return d_str + '.0' 78 | 79 | return d_str 80 | 81 | 82 | def token_ratio_to_wei_ratio(token_ratio: Fraction, token_sell: Token, token_buy: Token) -> Fraction: 83 | num = token_sell.from_decimal(Decimal(token_ratio.numerator)) 84 | den = token_buy.from_decimal(Decimal(token_ratio.denominator)) 85 | return Fraction(num, den, _normalize = False) 86 | 87 | 88 | class Tokens(BaseModel): 89 | tokens: List[Token] 90 | 91 | def find_by_address(self, address: str) -> Optional[Token]: 92 | found_token = [token for token in self.tokens if token.address == address] 93 | if found_token: 94 | return found_token[0] 95 | else: 96 | return None 97 | 98 | def find_by_id(self, token_id: int) -> Optional[Token]: 99 | found_token = [token for token in self.tokens if token.id == token_id] 100 | if found_token: 101 | return found_token[0] 102 | else: 103 | return None 104 | 105 | def find_by_symbol(self, symbol: str) -> Optional[Token]: 106 | found_token = [token for token in self.tokens if token.symbol == symbol] 107 | if found_token: 108 | return found_token[0] 109 | else: 110 | return None 111 | 112 | def find(self, token: TokenLike) -> Optional[Token]: 113 | result = None 114 | if isinstance(token, int): 115 | result = self.find_by_id(token) 116 | 117 | if isinstance(token, str): 118 | result = self.find_by_address(address=token) 119 | if result is None: 120 | result = self.find_by_symbol(symbol=token) 121 | return result 122 | 123 | 124 | class EncodedTx(abc.ABC): 125 | @abc.abstractmethod 126 | def encoded_message(self) -> bytes: 127 | pass 128 | 129 | @abc.abstractmethod 130 | def human_readable_message(self) -> str: 131 | pass 132 | 133 | @abc.abstractmethod 134 | def tx_type(self) -> int: 135 | pass 136 | 137 | @abc.abstractmethod 138 | def dict(self): 139 | pass 140 | 141 | @abc.abstractmethod 142 | def batch_message_part(self) -> str: 143 | pass 144 | 145 | 146 | @dataclass 147 | class ChangePubKey(EncodedTx): 148 | account_id: int 149 | account: str 150 | new_pk_hash: str 151 | token: Token 152 | fee: int 153 | nonce: int 154 | valid_from: int 155 | valid_until: int 156 | eth_auth_data: Union[ChangePubKeyCREATE2, ChangePubKeyEcdsa, None] = None 157 | eth_signature: Optional[TxEthSignature] = None 158 | signature: Optional[TxSignature] = None 159 | 160 | def human_readable_message(self) -> str: 161 | message = f"Set signing key: {self.new_pk_hash.replace('sync:', '').lower()}" 162 | if self.fee: 163 | message += f"\nFee: {self.fee} {self.token.symbol}" 164 | return message 165 | 166 | def batch_message_part(self) -> str: 167 | message = f"Set signing key: {self.new_pk_hash.replace('sync:', '').lower()}\n" 168 | if self.fee: 169 | message += f"Fee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\n" 170 | return message 171 | 172 | def encoded_message(self) -> bytes: 173 | return b"".join([ 174 | int_to_bytes(0xff - self.tx_type(), 1), 175 | int_to_bytes(TRANSACTION_VERSION, 1), 176 | serialize_account_id(self.account_id), 177 | serialize_address(self.account), 178 | serialize_address(self.new_pk_hash), 179 | serialize_token_id(self.token.id), 180 | packed_fee_checked(self.fee), 181 | serialize_nonce(self.nonce), 182 | serialize_timestamp(self.valid_from), 183 | serialize_timestamp(self.valid_until) 184 | ]) 185 | 186 | def get_eth_tx_bytes(self) -> bytes: 187 | data = b"".join([ 188 | serialize_address(self.new_pk_hash), 189 | serialize_nonce(self.nonce), 190 | serialize_account_id(self.account_id), 191 | ]) 192 | if self.eth_auth_data is not None: 193 | data += self.eth_auth_data.encode_message() 194 | return data 195 | 196 | def get_auth_data(self, signature: str): 197 | if self.eth_auth_data is None: 198 | return {"type": "Onchain"} 199 | elif isinstance(self.eth_auth_data, ChangePubKeyEcdsa): 200 | return self.eth_auth_data.dict(signature) 201 | elif isinstance(self.eth_auth_data, ChangePubKeyCREATE2): 202 | return self.eth_auth_data.dict() 203 | 204 | def dict(self): 205 | return { 206 | "type": "ChangePubKey", 207 | "accountId": self.account_id, 208 | "account": self.account, 209 | "newPkHash": self.new_pk_hash, 210 | "fee_token": self.token.id, 211 | "fee": str(self.fee), 212 | "nonce": self.nonce, 213 | "ethAuthData": self.eth_auth_data, 214 | "signature": self.signature.dict(), 215 | "validFrom": self.valid_from, 216 | "validUntil": self.valid_until, 217 | } 218 | 219 | @classmethod 220 | def tx_type(cls): 221 | return EncodedTxType.CHANGE_PUB_KEY 222 | 223 | 224 | @dataclass 225 | class Transfer(EncodedTx): 226 | account_id: int 227 | from_address: str 228 | to_address: str 229 | token: Token 230 | amount: int 231 | fee: int 232 | nonce: int 233 | valid_from: int 234 | valid_until: int 235 | signature: Optional[TxSignature] = None 236 | 237 | def tx_type(self) -> int: 238 | return EncodedTxType.TRANSFER 239 | 240 | def human_readable_message(self) -> str: 241 | msg = "" 242 | 243 | if self.amount != 0: 244 | msg += f"Transfer {self.token.decimal_str_amount(self.amount)} {self.token.symbol} to: {self.to_address.lower()}\n" 245 | if self.fee != 0: 246 | msg += f"Fee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\n" 247 | 248 | return msg + f"Nonce: {self.nonce}" 249 | 250 | def batch_message_part(self) -> str: 251 | msg = "" 252 | if self.amount != 0: 253 | msg += f"Transfer {self.token.decimal_str_amount(self.amount)} {self.token.symbol} to: {self.to_address.lower()}\n" 254 | if self.fee != 0: 255 | msg += f"Fee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\n" 256 | return msg 257 | 258 | def encoded_message(self) -> bytes: 259 | return b"".join([ 260 | int_to_bytes(0xff - self.tx_type(), 1), 261 | int_to_bytes(TRANSACTION_VERSION, 1), 262 | serialize_account_id(self.account_id), 263 | serialize_address(self.from_address), 264 | serialize_address(self.to_address), 265 | serialize_token_id(self.token.id), 266 | packed_amount_checked(self.amount), 267 | packed_fee_checked(self.fee), 268 | serialize_nonce(self.nonce), 269 | serialize_timestamp(self.valid_from), 270 | serialize_timestamp(self.valid_until) 271 | ]) 272 | 273 | def dict(self): 274 | return { 275 | "type": "Transfer", 276 | "accountId": self.account_id, 277 | "from": self.from_address, 278 | "to": self.to_address, 279 | "token": self.token.id, 280 | "fee": str(self.fee), 281 | "nonce": self.nonce, 282 | "signature": self.signature.dict(), 283 | "amount": str(self.amount), 284 | "validFrom": self.valid_from, 285 | "validUntil": self.valid_until, 286 | } 287 | 288 | 289 | @dataclass 290 | class Withdraw(EncodedTx): 291 | account_id: int 292 | from_address: str 293 | to_address: str 294 | amount: int 295 | fee: int 296 | nonce: int 297 | valid_from: int 298 | valid_until: int 299 | token: Token 300 | signature: Optional[TxSignature] = None 301 | 302 | def tx_type(self) -> int: 303 | return EncodedTxType.WITHDRAW 304 | 305 | def human_readable_message(self) -> str: 306 | msg = "" 307 | if self.amount != 0: 308 | msg += f"Withdraw {self.token.decimal_str_amount(self.amount)} {self.token.symbol} to: {self.to_address.lower()}\n" 309 | if self.fee != 0: 310 | msg += f"Fee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\n" 311 | return msg + f"Nonce: {self.nonce}" 312 | 313 | def batch_message_part(self) -> str: 314 | msg = "" 315 | if self.amount != 0: 316 | msg += f"Withdraw {self.token.decimal_str_amount(self.amount)} {self.token.symbol} to: {self.to_address.lower()}\n" 317 | if self.fee != 0: 318 | msg += f"Fee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\n" 319 | return msg 320 | 321 | def encoded_message(self) -> bytes: 322 | return b"".join([ 323 | int_to_bytes(0xff - self.tx_type(), 1), 324 | int_to_bytes(TRANSACTION_VERSION, 1), 325 | serialize_account_id(self.account_id), 326 | serialize_address(self.from_address), 327 | serialize_address(self.to_address), 328 | serialize_token_id(self.token.id), 329 | int_to_bytes(self.amount, length=16), 330 | packed_fee_checked(self.fee), 331 | serialize_nonce(self.nonce), 332 | serialize_timestamp(self.valid_from), 333 | serialize_timestamp(self.valid_until) 334 | ]) 335 | 336 | def dict(self): 337 | return { 338 | "type": "Withdraw", 339 | "accountId": self.account_id, 340 | "from": self.from_address, 341 | "to": self.to_address, 342 | "token": self.token.id, 343 | "fee": str(self.fee), 344 | "nonce": self.nonce, 345 | "signature": self.signature.dict(), 346 | "amount": str(self.amount), 347 | "validFrom": self.valid_from, 348 | "validUntil": self.valid_until, 349 | } 350 | 351 | 352 | @dataclass 353 | class ForcedExit(EncodedTx): 354 | initiator_account_id: int 355 | target: str 356 | token: Token 357 | fee: int 358 | nonce: int 359 | valid_from: int 360 | valid_until: int 361 | signature: Optional[TxSignature] = None 362 | 363 | def tx_type(self) -> int: 364 | return EncodedTxType.FORCED_EXIT 365 | 366 | def encoded_message(self) -> bytes: 367 | return b"".join([ 368 | int_to_bytes(0xff - self.tx_type(), 1), 369 | int_to_bytes(TRANSACTION_VERSION, 1), 370 | serialize_account_id(self.initiator_account_id), 371 | serialize_address(self.target), 372 | serialize_token_id(self.token.id), 373 | packed_fee_checked(self.fee), 374 | serialize_nonce(self.nonce), 375 | serialize_timestamp(self.valid_from), 376 | serialize_timestamp(self.valid_until) 377 | ]) 378 | 379 | def human_readable_message(self) -> str: 380 | message = f"ForcedExit {self.token.symbol} to: {self.target.lower()}\nFee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\nNonce: {self.nonce}" 381 | return message 382 | 383 | def batch_message_part(self) -> str: 384 | message = f"ForcedExit {self.token.symbol} to: {self.target.lower()}\n" \ 385 | f"Fee: {self.token.decimal_str_amount(self.fee)} {self.token.symbol}\n" 386 | return message 387 | 388 | def dict(self): 389 | return { 390 | "type": "ForcedExit", 391 | "initiatorAccountId": self.initiator_account_id, 392 | "target": self.target, 393 | "token": self.token.id, 394 | "fee": str(self.fee), 395 | "nonce": self.nonce, 396 | "signature": self.signature.dict(), 397 | "validFrom": self.valid_from, 398 | "validUntil": self.valid_until, 399 | } 400 | 401 | 402 | @dataclass 403 | class Order(EncodedTx): 404 | account_id: int 405 | recipient: str 406 | nonce: int 407 | token_sell: Token 408 | token_buy: Token 409 | amount: int 410 | ratio: Fraction 411 | valid_from: int 412 | valid_until: int 413 | signature: Optional[TxSignature] = None 414 | eth_signature: Optional[TxEthSignature] = None 415 | 416 | @classmethod 417 | def from_json(cls, json: dict, tokens: Tokens): 418 | 419 | def from_optional(value: Optional[Token]) -> Token: 420 | if value is None: 421 | raise ValueError(f"Token None value should not be used") 422 | return value 423 | 424 | token_sell_id: int = json["tokenSell"] 425 | token_buy_id: int = json["tokenBuy"] 426 | token_sell = from_optional(tokens.find_by_id(token_sell_id)) 427 | token_buy = from_optional(tokens.find_by_id(token_buy_id)) 428 | ratio = json["ratio"] 429 | 430 | # INFO: could be None 431 | signature = json.get("signature") 432 | if signature is not None: 433 | signature = TxSignature.from_dict(signature) 434 | ether_sig = json.get("ethSignature") 435 | if ether_sig is not None: 436 | ether_sig = TxEthSignature.from_dict(ether_sig) 437 | return cls( 438 | account_id=json["accountId"], 439 | recipient=json["recipient"], 440 | nonce=json["nonce"], 441 | token_sell=token_sell, 442 | token_buy=token_buy, 443 | amount=int(json["amount"]), 444 | ratio=Fraction(int(ratio[0]), int(ratio[1]), _normalize=False), 445 | valid_from=json["validFrom"], 446 | valid_until=json["validUntil"], 447 | signature=signature, 448 | eth_signature=ether_sig 449 | ) 450 | 451 | def tx_type(self) -> int: 452 | raise NotImplementedError 453 | 454 | def msg_type(self) -> int: 455 | return b'o'[0] 456 | 457 | def encoded_message(self) -> bytes: 458 | return b"".join([ 459 | int_to_bytes(self.msg_type(), 1), 460 | int_to_bytes(TRANSACTION_VERSION, 1), 461 | serialize_account_id(self.account_id), 462 | serialize_address(self.recipient), 463 | serialize_nonce(self.nonce), 464 | serialize_token_id(self.token_sell.id), 465 | serialize_token_id(self.token_buy.id), 466 | serialize_ratio_part(self.ratio.numerator), 467 | serialize_ratio_part(self.ratio.denominator), 468 | packed_amount_checked(self.amount), 469 | serialize_timestamp(self.valid_from), 470 | serialize_timestamp(self.valid_until) 471 | ]) 472 | 473 | def batch_message_part(self) -> str: 474 | pass 475 | 476 | def human_readable_message(self) -> str: 477 | if self.amount == 0: 478 | header = f'Limit order for {self.token_sell.symbol} -> {self.token_buy.symbol}' 479 | else: 480 | amount = self.token_sell.decimal_str_amount(self.amount) 481 | header = f'Order for {amount} {self.token_sell.symbol} -> {self.token_buy.symbol}' 482 | 483 | message = '\n'.join([ 484 | header, 485 | f'Ratio: {self.ratio.numerator}:{self.ratio.denominator}', 486 | f'Address: {self.recipient.lower()}', 487 | f'Nonce: {self.nonce}' 488 | ]) 489 | return message 490 | 491 | def dict(self): 492 | return { 493 | "accountId": self.account_id, 494 | "recipient": self.recipient, 495 | "nonce": self.nonce, 496 | "tokenSell": self.token_sell.id, 497 | "tokenBuy": self.token_buy.id, 498 | "amount": str(self.amount), 499 | "ratio": (str(self.ratio.numerator), str(self.ratio.denominator)), 500 | "validFrom": self.valid_from, 501 | "validUntil": self.valid_until, 502 | "signature": self.signature.dict() if self.signature else None, 503 | "ethSignature": self.eth_signature.dict() if self.eth_signature else None, 504 | } 505 | 506 | def is_valid_eth_signature(self, signer_address: str) -> bool: 507 | address = self._recover_signer_address() 508 | return signer_address == address 509 | 510 | def _recover_signer_address(self) -> str: 511 | """ 512 | INFO: particular case implementation with dependency from Web3 interface 513 | if it's needed to generelize for all Obejct type(Transfer, Swap etc) move to etherium_signer module 514 | with interface & implemnetation for Web3 as Validator class( Visitor pattern ) 515 | """ 516 | from web3.auto import w3 517 | from eth_account.messages import encode_defunct 518 | 519 | msg = self.human_readable_message().encode() 520 | encoded_message = encode_defunct(msg) 521 | 522 | def get_sig(opt_value: Optional[TxEthSignature]) -> TxEthSignature: 523 | if opt_value is None: 524 | raise ValueError() 525 | return opt_value 526 | 527 | # INFO: remove prefix 0x 528 | eth_sig = get_sig(self.eth_signature) 529 | sig = bytes.fromhex(eth_sig.signature[2:]) 530 | return w3.eth.account.recover_message(encoded_message, signature=sig) 531 | 532 | 533 | @dataclass 534 | class Swap(EncodedTx): 535 | submitter_id: int 536 | submitter_address: str 537 | amounts: Tuple[int, int] 538 | orders: Tuple[Order, Order] 539 | fee_token: Token 540 | fee: int 541 | nonce: int 542 | signature: Optional[TxSignature] = None 543 | 544 | def tx_type(self) -> int: 545 | return EncodedTxType.SWAP 546 | 547 | def human_readable_message(self) -> str: 548 | if self.fee != 0: 549 | message = f'Swap fee: {self.fee_token.decimal_str_amount(self.fee)} {self.fee_token.symbol}\n' 550 | else: 551 | message = '' 552 | message += f'Nonce: {self.nonce}' 553 | return message 554 | 555 | def batch_message_part(self) -> str: 556 | if self.fee != 0: 557 | message = f'Swap fee: {self.fee_token.decimal_str_amount(self.fee)} {self.fee_token.symbol}\n' 558 | else: 559 | message = '' 560 | return message 561 | 562 | def encoded_message(self) -> bytes: 563 | order_bytes = b''.join([ 564 | self.orders[0].encoded_message(), 565 | self.orders[1].encoded_message(), 566 | ]) 567 | return b"".join([ 568 | int_to_bytes(0xff - self.tx_type(), 1), 569 | int_to_bytes(TRANSACTION_VERSION, 1), 570 | serialize_account_id(self.submitter_id), 571 | serialize_address(self.submitter_address), 572 | serialize_nonce(self.nonce), 573 | ZkSyncLibrary().hash_orders(order_bytes), 574 | serialize_token_id(self.fee_token.id), 575 | packed_fee_checked(self.fee), 576 | packed_amount_checked(self.amounts[0]), 577 | packed_amount_checked(self.amounts[1]), 578 | ]) 579 | 580 | def dict(self): 581 | return { 582 | "type": "Swap", 583 | "submitterId": self.submitter_id, 584 | "submitterAddress": self.submitter_address, 585 | "feeToken": self.fee_token.id, 586 | "fee": str(self.fee), 587 | "nonce": self.nonce, 588 | "signature": self.signature.dict() if self.signature else None, 589 | "amounts": (str(self.amounts[0]), str(self.amounts[1])), 590 | "orders": (self.orders[0].dict(), self.orders[1].dict()) 591 | } 592 | 593 | 594 | @dataclass 595 | class MintNFT(EncodedTx): 596 | creator_id: int 597 | creator_address: str 598 | content_hash: str 599 | recipient: str 600 | fee: int 601 | fee_token: Token 602 | nonce: int 603 | signature: Optional[TxSignature] = None 604 | 605 | def tx_type(self) -> int: 606 | return EncodedTxType.MINT_NFT 607 | 608 | def encoded_message(self) -> bytes: 609 | return b"".join([ 610 | int_to_bytes(0xff - self.tx_type(), 1), 611 | int_to_bytes(TRANSACTION_VERSION, 1), 612 | serialize_account_id(self.creator_id), 613 | serialize_address(self.creator_address), 614 | serialize_content_hash(self.content_hash), 615 | serialize_address(self.recipient), 616 | serialize_token_id(self.fee_token.id), 617 | packed_fee_checked(self.fee), 618 | serialize_nonce(self.nonce), 619 | ]) 620 | 621 | def human_readable_message(self) -> str: 622 | message = f"MintNFT {self.content_hash} for: {self.recipient.lower()}\n" \ 623 | f"Fee: {self.fee_token.decimal_str_amount(self.fee)} {self.fee_token.symbol}\nNonce: {self.nonce}" 624 | return message 625 | 626 | def batch_message_part(self) -> str: 627 | message = f"MintNFT {self.content_hash} for: {self.recipient.lower()}\n" \ 628 | f"Fee: {self.fee_token.decimal_str_amount(self.fee)} {self.fee_token.symbol}\n" 629 | return message 630 | 631 | def dict(self): 632 | return { 633 | "type": "MintNFT", 634 | "creatorId": self.creator_id, 635 | "creatorAddress": self.creator_address, 636 | "contentHash": self.content_hash, 637 | "recipient": self.recipient, 638 | "feeToken": self.fee_token.id, 639 | "fee": str(self.fee), 640 | "nonce": self.nonce, 641 | "signature": self.signature.dict(), 642 | } 643 | 644 | 645 | @dataclass 646 | class WithdrawNFT(EncodedTx): 647 | account_id: int 648 | from_address: str 649 | to_address: str 650 | fee_token: Token 651 | fee: int 652 | nonce: int 653 | valid_from: int 654 | valid_until: int 655 | token_id: int 656 | signature: Optional[TxSignature] = None 657 | 658 | def tx_type(self) -> int: 659 | return EncodedTxType.WITHDRAW_NFT 660 | 661 | def encoded_message(self) -> bytes: 662 | return b"".join([ 663 | int_to_bytes(0xff - self.tx_type(), 1), 664 | int_to_bytes(TRANSACTION_VERSION, 1), 665 | serialize_account_id(self.account_id), 666 | serialize_address(self.from_address), 667 | serialize_address(self.to_address), 668 | serialize_token_id(self.token_id), 669 | serialize_token_id(self.fee_token.id), 670 | packed_fee_checked(self.fee), 671 | serialize_nonce(self.nonce), 672 | serialize_timestamp(self.valid_from), 673 | serialize_timestamp(self.valid_until) 674 | ]) 675 | 676 | def human_readable_message(self) -> str: 677 | message = f"WithdrawNFT {self.token_id} to: {self.to_address.lower()}\nFee: {self.fee_token.decimal_str_amount(self.fee)} {self.fee_token.symbol}\nNonce: {self.nonce}" 678 | return message 679 | 680 | def batch_message_part(self) -> str: 681 | message = f"WithdrawNFT {self.token_id} to: {self.to_address.lower()}\n" \ 682 | f"Fee: {self.fee_token.decimal_str_amount(self.fee)} {self.fee_token.symbol}\n" 683 | return message 684 | 685 | def dict(self): 686 | return { 687 | "type": "WithdrawNFT", 688 | "accountId": self.account_id, 689 | "from": self.from_address, 690 | "to": self.to_address, 691 | "feeToken": self.fee_token.id, 692 | "fee": str(self.fee), 693 | "nonce": self.nonce, 694 | "validFrom": self.valid_from, 695 | "validUntil": self.valid_until, 696 | "token": self.token_id, 697 | "signature": self.signature.dict(), 698 | } 699 | 700 | 701 | class EncodedTxValidator: 702 | def __init__(self, library: ZkSyncLibrary): 703 | self.library = library 704 | 705 | def is_valid_signature(self, tx): 706 | zk_sync_signature: TxSignature = tx.signature 707 | if zk_sync_signature is None: 708 | return False 709 | bytes_signature = bytes.fromhex(zk_sync_signature.signature) 710 | pubkey = bytes.fromhex(zk_sync_signature.public_key) 711 | return self.library.is_valid_signature(tx.encoded_message(), pubkey, bytes_signature) 712 | 713 | 714 | @dataclass 715 | class TransactionWithSignature: 716 | tx: EncodedTx 717 | signature: TxEthSignature 718 | 719 | def dict(self): 720 | return { 721 | 'tx': self.tx.dict(), 722 | 'signature': self.signature.dict(), 723 | } 724 | 725 | 726 | @dataclass() 727 | class TransactionWithOptionalSignature: 728 | tx: EncodedTx 729 | signature: Union[None, TxEthSignature, List[TxSignature]] = None 730 | 731 | def dict(self): 732 | if self.signature is None: 733 | null_value = None 734 | return { 735 | 'signature': null_value, 736 | 'tx': self.tx.dict() 737 | } 738 | else: 739 | if isinstance(self.signature, list): 740 | null_value = None 741 | value = [] 742 | for sig in self.signature: 743 | if sig is None: 744 | value.append(null_value) 745 | else: 746 | value.append(sig.dict()) 747 | return { 748 | 'signature': value, 749 | 'tx': self.tx.dict() 750 | } 751 | else: 752 | return { 753 | 'signature': self.signature.dict(), 754 | 'tx': self.tx.dict() 755 | } 756 | -------------------------------------------------------------------------------- /zksync_sdk/wallet.py: -------------------------------------------------------------------------------- 1 | import time 2 | from decimal import Decimal 3 | from fractions import Fraction 4 | from typing import List, Optional, Tuple, Union 5 | 6 | from zksync_sdk.ethereum_provider import EthereumProvider 7 | from zksync_sdk.ethereum_signer import EthereumSignerInterface 8 | from zksync_sdk.types import (ChangePubKey, ChangePubKeyCREATE2, ChangePubKeyEcdsa, 9 | ChangePubKeyTypes, EncodedTx, ForcedExit, Token, TokenLike, 10 | Tokens, TransactionWithSignature, Transfer, TxEthSignature, 11 | Withdraw, MintNFT, WithdrawNFT, NFT, Order, Swap, RatioType, 12 | token_ratio_to_wei_ratio, get_toggle_message, get_toggle_message_with_pub, Toggle2FA) 13 | from zksync_sdk.zksync_provider import FeeTxType, ZkSyncProviderInterface 14 | from zksync_sdk.zksync_signer import ZkSyncSigner 15 | from zksync_sdk.zksync_provider.transaction import Transaction 16 | 17 | DEFAULT_VALID_FROM = 0 18 | DEFAULT_VALID_UNTIL = 2 ** 32 - 1 19 | 20 | 21 | class WalletError(Exception): 22 | pass 23 | 24 | 25 | class TokenNotFoundError(WalletError): 26 | pass 27 | 28 | 29 | class AmountsMissing(WalletError): 30 | pass 31 | 32 | 33 | class Wallet: 34 | def __init__(self, ethereum_provider: EthereumProvider, zk_signer: ZkSyncSigner, 35 | eth_signer: EthereumSignerInterface, provider: ZkSyncProviderInterface): 36 | self.ethereum_provider = ethereum_provider 37 | self.zk_signer = zk_signer 38 | self.eth_signer = eth_signer 39 | self.zk_provider = provider 40 | self.account_id = None 41 | self.tokens = Tokens(tokens=[]) 42 | 43 | async def get_account_id(self): 44 | if self.account_id is None: 45 | state = await self.zk_provider.get_state(self.address()) 46 | if isinstance(state.id, int): 47 | self.account_id = state.id 48 | return self.account_id 49 | 50 | async def send_signed_transaction(self, tx: EncodedTx, 51 | eth_signature: Union[Optional[TxEthSignature], List[Optional[TxEthSignature]]], 52 | fast_processing: bool = False) -> Transaction: 53 | return await self.zk_provider.submit_tx(tx, eth_signature, fast_processing) 54 | 55 | async def send_txs_batch(self, transactions: List[TransactionWithSignature], 56 | signatures: Optional[ 57 | Union[List[TxEthSignature], TxEthSignature] 58 | ] = None) -> List[Transaction]: 59 | return await self.zk_provider.submit_txs_batch(transactions, signatures) 60 | 61 | async def set_signing_key(self, fee_token: TokenLike, *, 62 | eth_auth_data: Union[ChangePubKeyCREATE2, ChangePubKeyEcdsa, None] = None, 63 | fee: Optional[Decimal] = None, nonce: Optional[int] = None, 64 | valid_from=DEFAULT_VALID_FROM, valid_until=DEFAULT_VALID_UNTIL): 65 | if nonce is None: 66 | nonce = await self.zk_provider.get_account_nonce(self.address()) 67 | fee_token_obj = await self.resolve_token(fee_token) 68 | if isinstance(eth_auth_data, ChangePubKeyEcdsa): 69 | eth_auth_type = ChangePubKeyTypes.ecdsa 70 | elif isinstance(eth_auth_data, ChangePubKeyCREATE2): 71 | eth_auth_type = ChangePubKeyTypes.create2 72 | else: 73 | eth_auth_type = ChangePubKeyTypes.onchain 74 | 75 | if fee is None: 76 | if eth_auth_type == ChangePubKeyTypes.ecdsa: 77 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.change_pub_key_ecdsa, 78 | self.address(), 79 | fee_token_obj.id) 80 | elif eth_auth_type == ChangePubKeyTypes.onchain: 81 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.change_pub_key_onchain, 82 | self.address(), 83 | fee_token_obj.id) 84 | else: 85 | assert eth_auth_type == ChangePubKeyTypes.create2, "invalid eth_auth_type" 86 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.change_pub_key_create2, 87 | self.address(), 88 | fee_token_obj.id) 89 | fee_int = fee_obj.total_fee 90 | else: 91 | fee_int = fee_token_obj.from_decimal(fee) 92 | 93 | change_pub_key, eth_signature = await self.build_change_pub_key(fee_token_obj, 94 | eth_auth_data, fee_int, 95 | nonce, 96 | valid_from, 97 | valid_until) 98 | 99 | return await self.send_signed_transaction(change_pub_key, eth_signature) 100 | 101 | # This function takes as a parameter the integer fee of 102 | # lowest token denominations (wei, satoshi, etc.) 103 | async def build_change_pub_key( 104 | self, 105 | fee_token: Token, 106 | eth_auth_data: Union[ChangePubKeyCREATE2, ChangePubKeyEcdsa, None], 107 | fee: int, 108 | nonce: Optional[int] = None, 109 | valid_from=DEFAULT_VALID_FROM, 110 | valid_until=DEFAULT_VALID_UNTIL): 111 | if nonce is None: 112 | nonce = await self.zk_provider.get_account_nonce(self.address()) 113 | account_id = await self.get_account_id() 114 | 115 | new_pubkey_hash = self.zk_signer.pubkey_hash_str() 116 | change_pub_key = ChangePubKey( 117 | account=self.address(), 118 | account_id=account_id, 119 | new_pk_hash=new_pubkey_hash, 120 | token=fee_token, 121 | fee=fee, 122 | nonce=nonce, 123 | valid_until=valid_until, 124 | valid_from=valid_from, 125 | eth_auth_data=eth_auth_data 126 | ) 127 | 128 | eth_signature = self.eth_signer.sign(change_pub_key.get_eth_tx_bytes()) 129 | eth_auth_data = change_pub_key.get_auth_data(eth_signature.signature) 130 | 131 | change_pub_key.eth_auth_data = eth_auth_data 132 | zk_signature = self.zk_signer.sign_tx(change_pub_key) 133 | change_pub_key.signature = zk_signature 134 | 135 | return change_pub_key, eth_signature 136 | 137 | async def forced_exit(self, target: str, token: TokenLike, fee: Optional[Decimal] = None, 138 | valid_from=DEFAULT_VALID_FROM, valid_until=DEFAULT_VALID_UNTIL) -> Transaction: 139 | nonce = await self.zk_provider.get_account_nonce(self.address()) 140 | token_obj = await self.resolve_token(token) 141 | if fee is None: 142 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.withdraw, target, token_obj.id) 143 | fee_int = fee_obj.total_fee 144 | else: 145 | fee_int = token_obj.from_decimal(fee) 146 | 147 | transfer, eth_signature = await self.build_forced_exit(target, token_obj, fee_int, nonce, 148 | valid_from, valid_until) 149 | 150 | return await self.send_signed_transaction(transfer, eth_signature) 151 | 152 | # This function takes as a parameter the integer fee of 153 | # lowest token denominations (wei, satoshi, etc.) 154 | async def build_forced_exit( 155 | self, 156 | target: str, 157 | token: Token, 158 | fee: int, 159 | nonce: Optional[int] = None, 160 | valid_from=DEFAULT_VALID_FROM, 161 | valid_until=DEFAULT_VALID_UNTIL) -> Tuple[ForcedExit, TxEthSignature]: 162 | if nonce is None: 163 | nonce = await self.zk_provider.get_account_nonce(self.address()) 164 | account_id = await self.get_account_id() 165 | 166 | forced_exit = ForcedExit(initiator_account_id=account_id, 167 | target=target, 168 | fee=fee, 169 | nonce=nonce, 170 | valid_from=valid_from, 171 | valid_until=valid_until, 172 | token=token) 173 | eth_signature = self.eth_signer.sign_tx(forced_exit) 174 | zk_signature = self.zk_signer.sign_tx(forced_exit) 175 | forced_exit.signature = zk_signature 176 | 177 | return forced_exit, eth_signature 178 | 179 | async def mint_nft(self, content_hash: str, recipient: str,token: TokenLike, fee: Optional[Decimal] = None) -> Transaction: 180 | token_obj = await self.resolve_token(token) 181 | 182 | nonce = await self.zk_provider.get_account_nonce(self.address()) 183 | if fee is None: 184 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.mint_nft, recipient, token_obj.id) 185 | fee_int = fee_obj.total_fee 186 | else: 187 | fee_int = token_obj.from_decimal(fee) 188 | 189 | mint_nft, eth_signature = await self.build_mint_nft(content_hash, recipient, token_obj, fee_int, nonce) 190 | return await self.send_signed_transaction(mint_nft, eth_signature) 191 | 192 | # This function takes as a parameter the integer fee of 193 | # lowest token denominations (wei, satoshi, etc.) 194 | async def build_mint_nft( 195 | self, 196 | content_hash: str, 197 | recipient: str, 198 | token: Token, 199 | fee: int, 200 | nonce: Optional[int] = None 201 | ) -> Tuple[MintNFT, TxEthSignature]: 202 | if nonce is None: 203 | nonce = await self.zk_provider.get_account_nonce(self.address()) 204 | account_id = await self.get_account_id() 205 | 206 | mint_nft = MintNFT(creator_id=account_id, 207 | creator_address=self.address(), 208 | content_hash=content_hash, 209 | recipient=recipient, 210 | fee=fee, 211 | fee_token=token, 212 | nonce=nonce) 213 | eth_signature = self.eth_signer.sign_tx(mint_nft) 214 | zk_signature = self.zk_signer.sign_tx(mint_nft) 215 | mint_nft.signature = zk_signature 216 | 217 | return mint_nft, eth_signature 218 | 219 | async def withdraw_nft( 220 | self, 221 | to_address: str, 222 | nft_token: NFT, 223 | fee_token: TokenLike, 224 | fee: Optional[Decimal] = None, 225 | valid_from=DEFAULT_VALID_FROM, 226 | valid_until=DEFAULT_VALID_UNTIL 227 | ) -> Transaction: 228 | nonce = await self.zk_provider.get_account_nonce(self.address()) 229 | fee_token_obj = await self.resolve_token(fee_token) 230 | 231 | if fee is None: 232 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.withdraw_nft, to_address, fee_token_obj.id) 233 | fee_int = fee_obj.total_fee 234 | else: 235 | fee_int = fee_token_obj.from_decimal(fee) 236 | withdraw_nft, eth_signature = await self.build_withdraw_nft(to_address, nft_token, fee_token_obj, fee_int, 237 | nonce, valid_from, valid_until) 238 | return await self.send_signed_transaction(withdraw_nft, eth_signature) 239 | 240 | # This function takes as a parameter the integer fee of 241 | # lowest token denominations (wei, satoshi, etc.) 242 | async def build_withdraw_nft( 243 | self, 244 | to_address: str, 245 | nft_token: NFT, 246 | fee_token: Token, 247 | fee: int, 248 | nonce: Optional[int] = None, 249 | valid_from=DEFAULT_VALID_FROM, 250 | valid_until=DEFAULT_VALID_UNTIL 251 | ) -> Tuple[WithdrawNFT, TxEthSignature]: 252 | if nonce is None: 253 | nonce = await self.zk_provider.get_account_nonce(self.address()) 254 | account_id = await self.get_account_id() 255 | 256 | withdraw_nft = WithdrawNFT( 257 | account_id=account_id, 258 | from_address=self.address(), 259 | to_address=to_address, 260 | fee_token=fee_token, 261 | fee=fee, 262 | nonce=nonce, 263 | valid_from=valid_from, 264 | valid_until=valid_until, 265 | token_id=nft_token.id) 266 | eth_signature = self.eth_signer.sign_tx(withdraw_nft) 267 | zk_signature = self.zk_signer.sign_tx(withdraw_nft) 268 | withdraw_nft.signature = zk_signature 269 | 270 | return withdraw_nft, eth_signature 271 | 272 | def address(self): 273 | return self.eth_signer.address() 274 | 275 | async def build_transfer( 276 | self, 277 | to: str, 278 | amount: int, 279 | token: Token, 280 | fee: int, 281 | nonce: Optional[int] = None, 282 | valid_from: int = DEFAULT_VALID_FROM, 283 | valid_until: int = DEFAULT_VALID_UNTIL, 284 | ) -> Tuple[Transfer, TxEthSignature]: 285 | """ 286 | This function takes as a parameter the integer amount/fee of lowest token denominations (wei, satoshi, etc.) 287 | """ 288 | if nonce is None: 289 | nonce = await self.zk_provider.get_account_nonce(self.address()) 290 | account_id = await self.get_account_id() 291 | 292 | transfer = Transfer(account_id=account_id, from_address=self.address(), 293 | to_address=to.lower(), 294 | amount=amount, fee=fee, 295 | nonce=nonce, 296 | valid_from=valid_from, 297 | valid_until=valid_until, 298 | token=token) 299 | eth_signature = self.eth_signer.sign_tx(transfer) 300 | zk_signature = self.zk_signer.sign_tx(transfer) 301 | transfer.signature = zk_signature 302 | return transfer, eth_signature 303 | 304 | async def transfer(self, to: str, amount: Decimal, token: TokenLike, 305 | fee: Optional[Decimal] = None, 306 | valid_from=DEFAULT_VALID_FROM, valid_until=DEFAULT_VALID_UNTIL) -> Transaction: 307 | nonce = await self.zk_provider.get_account_nonce(self.address()) 308 | token_obj = await self.resolve_token(token) 309 | 310 | if fee is None: 311 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.transfer, to, token_obj.id) 312 | fee_int = fee_obj.total_fee 313 | else: 314 | fee_int = token_obj.from_decimal(fee) 315 | 316 | amount_int = token_obj.from_decimal(amount) 317 | 318 | transfer, eth_signature = await self.build_transfer(to, amount_int, token_obj, fee_int, nonce, valid_from, valid_until) 319 | return await self.send_signed_transaction(transfer, eth_signature) 320 | 321 | async def transfer_nft(self, to: str, nft: NFT, fee_token: TokenLike, 322 | fee: Optional[Decimal] = None, 323 | valid_from=DEFAULT_VALID_FROM, 324 | valid_until=DEFAULT_VALID_UNTIL 325 | ) -> List[Transaction]: 326 | nonce = await self.zk_provider.get_account_nonce(self.address()) 327 | fee_token_obj = await self.resolve_token(fee_token) 328 | 329 | if fee is None: 330 | fee_int = await self.zk_provider.get_transactions_batch_fee( 331 | [FeeTxType.transfer, FeeTxType.transfer], 332 | [to, self.address()], 333 | fee_token_obj.symbol 334 | ) 335 | else: 336 | fee_int = fee_token_obj.from_decimal(fee) 337 | 338 | nft_tx = await self.build_transfer(to, 1, nft, 0, nonce, valid_from, valid_until) 339 | fee_tx = await self.build_transfer(self.address(), 0, fee_token_obj, fee_int, nonce + 1, valid_from, valid_until) 340 | batch = [ 341 | TransactionWithSignature(nft_tx[0], nft_tx[1]), 342 | TransactionWithSignature(fee_tx[0], fee_tx[1]) 343 | ] 344 | return await self.send_txs_batch(batch) 345 | 346 | async def get_order(self, token_sell: TokenLike, token_buy: TokenLike, 347 | ratio: Fraction, ratio_type: RatioType, amount: Decimal, 348 | recipient: Optional[str] = None, 349 | nonce: Optional[int] = None, 350 | valid_from=DEFAULT_VALID_FROM, 351 | valid_until=DEFAULT_VALID_UNTIL) -> Order: 352 | if nonce is None: 353 | nonce = await self.zk_provider.get_account_nonce(self.address()) 354 | token_sell_obj = await self.resolve_token(token_sell) 355 | token_buy_obj = await self.resolve_token(token_buy) 356 | recipient = recipient or self.address() 357 | 358 | if ratio_type == RatioType.token: 359 | ratio = token_ratio_to_wei_ratio(ratio, token_sell_obj, token_buy_obj) 360 | 361 | account_id = await self.get_account_id() 362 | 363 | order = Order(account_id=account_id, recipient=recipient, 364 | token_sell=token_sell_obj, 365 | token_buy=token_buy_obj, 366 | ratio=ratio, 367 | amount=token_sell_obj.from_decimal(amount), 368 | nonce=nonce, 369 | valid_from=valid_from, 370 | valid_until=valid_until) 371 | 372 | order.eth_signature = self.eth_signer.sign_tx(order) 373 | order.signature = self.zk_signer.sign_tx(order) 374 | 375 | return order 376 | 377 | async def get_limit_order(self, token_sell: TokenLike, token_buy: TokenLike, 378 | ratio: Fraction, ratio_type: RatioType, 379 | recipient: Optional[str] = None, 380 | valid_from=DEFAULT_VALID_FROM, 381 | valid_until=DEFAULT_VALID_UNTIL): 382 | return await self.get_order(token_sell, token_buy, ratio, ratio_type, Decimal(0), recipient, valid_from, 383 | valid_until) 384 | 385 | # This function takes as a parameter the integer amounts/fee of 386 | # lowest token denominations (wei, satoshi, etc.) 387 | async def build_swap(self, orders: Tuple[Order, Order], fee_token: Token, 388 | amounts: Tuple[int, int], fee: int, nonce: Optional[int] = None): 389 | if nonce is None: 390 | nonce = await self.zk_provider.get_account_nonce(self.address()) 391 | account_id = await self.get_account_id() 392 | 393 | swap = Swap( 394 | orders=orders, fee_token=fee_token, amounts=amounts, fee=fee, nonce=nonce, 395 | submitter_id=account_id, submitter_address=self.address() 396 | ) 397 | eth_signature = self.eth_signer.sign_tx(swap) 398 | swap.signature = self.zk_signer.sign_tx(swap) 399 | return swap, eth_signature 400 | 401 | async def swap(self, orders: Tuple[Order, Order], fee_token: TokenLike, 402 | amounts: Optional[Tuple[Decimal, Decimal]] = None, fee: Optional[Decimal] = None): 403 | nonce = await self.zk_provider.get_account_nonce(self.address()) 404 | 405 | fee_token_obj = await self.resolve_token(fee_token) 406 | if fee is None: 407 | fee_obj = await self.zk_provider.get_transaction_fee(FeeTxType.swap, self.address(), fee_token_obj.id) 408 | fee_int = fee_obj.total_fee 409 | else: 410 | fee_int = fee_token_obj.from_decimal(fee) 411 | 412 | if amounts is None: 413 | amounts_int = (orders[0].amount, orders[1].amount) 414 | if amounts_int[0] == 0 or amounts_int[1] == 0: 415 | raise AmountsMissing("in this case you must specify amounts explicitly") 416 | else: 417 | amounts_int = ( 418 | orders[0].token_sell.from_decimal(amounts[0]), 419 | orders[1].token_sell.from_decimal(amounts[1]) 420 | ) 421 | 422 | swap, eth_signature = await self.build_swap(orders, fee_token_obj, amounts_int, fee_int, nonce) 423 | eth_signatures = [eth_signature, swap.orders[0].eth_signature, swap.orders[1].eth_signature] 424 | return await self.send_signed_transaction(swap, eth_signatures) 425 | 426 | # This function takes as a parameter the integer amount/fee of 427 | # lowest token denominations (wei, satoshi, etc.) 428 | async def build_withdraw(self, eth_address: str, amount: int, token: Token, 429 | fee: int, 430 | nonce: Optional[int] = None, 431 | valid_from=DEFAULT_VALID_FROM, 432 | valid_until=DEFAULT_VALID_UNTIL): 433 | if nonce is None: 434 | nonce = await self.zk_provider.get_account_nonce(self.address()) 435 | account_id = await self.get_account_id() 436 | 437 | withdraw = Withdraw(account_id=account_id, from_address=self.address(), 438 | to_address=eth_address, 439 | amount=amount, fee=fee, 440 | nonce=nonce, 441 | valid_from=valid_from, 442 | valid_until=valid_until, 443 | token=token) 444 | eth_signature = self.eth_signer.sign_tx(withdraw) 445 | zk_signature = self.zk_signer.sign_tx(withdraw) 446 | withdraw.signature = zk_signature 447 | return withdraw, eth_signature 448 | 449 | async def withdraw(self, eth_address: str, amount: Decimal, token: TokenLike, 450 | fee: Optional[Decimal] = None, fast: bool = False, 451 | valid_from=DEFAULT_VALID_FROM, valid_until=DEFAULT_VALID_UNTIL) -> Transaction: 452 | nonce = await self.zk_provider.get_account_nonce(self.address()) 453 | token_obj = await self.resolve_token(token) 454 | if fee is None: 455 | tx_type = FeeTxType.fast_withdraw if fast else FeeTxType.withdraw 456 | fee_obj = await self.zk_provider.get_transaction_fee(tx_type, eth_address, token_obj.id) 457 | fee_int = fee_obj.total_fee 458 | else: 459 | fee_int = token_obj.from_decimal(fee) 460 | amount_int = token_obj.from_decimal(amount) 461 | 462 | withdraw, eth_signature = await self.build_withdraw(eth_address, amount_int, token_obj, fee_int, nonce, 463 | valid_from, valid_until) 464 | return await self.send_signed_transaction(withdraw, eth_signature, fast) 465 | 466 | async def get_balance(self, token: TokenLike, type: str): 467 | account_state = await self.get_account_state() 468 | token_obj = await self.resolve_token(token) 469 | 470 | if type == "committed": 471 | token_balance = account_state.committed.balances.get(token_obj.symbol) 472 | else: 473 | token_balance = account_state.verified.balances.get(token_obj.symbol) 474 | if token_balance is None: 475 | token_balance = 0 476 | return token_balance 477 | 478 | async def get_account_state(self): 479 | return await self.zk_provider.get_state(self.address()) 480 | 481 | async def is_signing_key_set(self) -> bool: 482 | account_state = await self.get_account_state() 483 | signer_pub_key_hash = self.zk_signer.pubkey_hash_str() 484 | return account_state.id is not None and \ 485 | account_state.committed.pub_key_hash == signer_pub_key_hash 486 | 487 | async def resolve_token(self, token: TokenLike) -> Token: 488 | resolved_token = self.tokens.find(token) 489 | if resolved_token is not None: 490 | return resolved_token 491 | self.tokens = await self.zk_provider.get_tokens() 492 | resolved_token = self.tokens.find(token) 493 | if resolved_token is None: 494 | raise TokenNotFoundError 495 | return resolved_token 496 | 497 | async def enable_2fa(self) -> bool: 498 | mil_seconds = int(time.time() * 1000) 499 | msg = get_toggle_message(True, mil_seconds) 500 | eth_sig = self.eth_signer.sign(msg.encode()) 501 | account_id = await self.get_account_id() 502 | toggle = Toggle2FA(True, 503 | account_id, 504 | mil_seconds, 505 | eth_sig, 506 | None 507 | ) 508 | return await self.zk_provider.toggle_2fa(toggle) 509 | 510 | async def disable_2fa(self, pub_key_hash: Optional[str]) -> bool: 511 | mil_seconds = int(time.time() * 1000) 512 | if pub_key_hash is None: 513 | msg = get_toggle_message(False, mil_seconds) 514 | else: 515 | msg = get_toggle_message_with_pub(False, mil_seconds, pub_key_hash) 516 | eth_sig = self.eth_signer.sign(msg.encode()) 517 | account_id = await self.get_account_id() 518 | toggle = Toggle2FA(False, 519 | account_id, 520 | mil_seconds, 521 | eth_sig, 522 | pub_key_hash) 523 | return await self.zk_provider.toggle_2fa(toggle) 524 | 525 | async def disable_2fa_with_pub_key(self): 526 | pub_key_hash = self.zk_signer.pubkey_hash_str() 527 | return await self.disable_2fa(pub_key_hash) 528 | -------------------------------------------------------------------------------- /zksync_sdk/zksync.py: -------------------------------------------------------------------------------- 1 | from eth_account.signers.base import BaseAccount 2 | from web3 import Web3 3 | 4 | from zksync_sdk.contract_utils import erc20_abi, zksync_abi 5 | 6 | MAX_ERC20_APPROVE_AMOUNT = 115792089237316195423570985008687907853269984665640564039457584007913129639935 # 2^256 - 1 7 | ERC20_APPROVE_THRESHOLD = 57896044618658097711785492504343953926634992332820282019728792003956564819968 # 2^255 8 | 9 | 10 | class Contract: 11 | def __init__(self, contract_address: str, web3: Web3, account: BaseAccount, abi): 12 | self.contract_address = contract_address 13 | self.web3 = web3 14 | self.contract = self.web3.eth.contract(self.contract_address, abi=abi) # type: ignore[call-overload] 15 | self.account = account 16 | 17 | def _call_method(self, method_name, *args, amount=None, **kwargs): 18 | params = {} 19 | if amount is not None: 20 | params['value'] = amount 21 | params['from'] = self.account.address 22 | transaction = getattr(self.contract.functions, method_name)( 23 | *args, 24 | **kwargs 25 | ).buildTransaction(params) 26 | 27 | transaction.update({'nonce': self.web3.eth.get_transaction_count(self.account.address)}) 28 | signed_tx = self.account.sign_transaction(transaction) 29 | txn_hash = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction) 30 | txn_receipt = self.web3.eth.waitForTransactionReceipt(txn_hash) 31 | return txn_receipt 32 | 33 | 34 | class ZkSync(Contract): 35 | def __init__(self, web3: Web3, zksync_contract_address: str, account: BaseAccount): 36 | super().__init__(zksync_contract_address, web3, account, zksync_abi()) 37 | 38 | def deposit_eth(self, address: str, amount: int): 39 | return self._call_method("depositETH", address, amount=amount) 40 | 41 | def deposit_erc20(self, token_address: str, address: str, amount: int): 42 | return self._call_method("depositERC20", token_address, amount, address) 43 | 44 | def full_exit(self, account_id: int, token_address: str, ): 45 | return self._call_method("requestFullExit", account_id, token_address) 46 | 47 | def full_exit_nft(self, account_id: int, token_id: int): 48 | return self._call_method("requestFullExitNFT", account_id, token_id) 49 | 50 | def set_auth_pub_key_hash(self, pub_key_hash: bytes, nonce: int): 51 | return self._call_method("setAuthPubkeyHash", pub_key_hash, nonce) 52 | 53 | def auth_facts(self, sender_address: str, nonce: int) -> bytes: 54 | return self.contract.caller.authFacts(sender_address, nonce) 55 | 56 | 57 | class ERC20Contract(Contract): 58 | def __init__(self, web3: Web3, zksync_address: str, contract_address: str, 59 | account: BaseAccount): 60 | self.zksync_address = zksync_address 61 | super().__init__(contract_address, web3, account, erc20_abi()) 62 | 63 | def approve_deposit(self, max_erc20_approve_amount=MAX_ERC20_APPROVE_AMOUNT): 64 | return self._call_method('approve', self.zksync_address, max_erc20_approve_amount) 65 | 66 | def is_deposit_approved(self, erc20_approve_threshold=ERC20_APPROVE_THRESHOLD): 67 | allowance = self.contract.functions.allowance(self.account.address, 68 | self.zksync_address).call() 69 | 70 | return allowance >= erc20_approve_threshold 71 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/__init__.py: -------------------------------------------------------------------------------- 1 | from .interface import * 2 | from .types import * 3 | from .v01 import * 4 | from .transaction import * 5 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/batch_builder.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from zksync_sdk.zksync_provider import FeeTxType 4 | from zksync_sdk.wallet import Wallet, DEFAULT_VALID_FROM, DEFAULT_VALID_UNTIL, AmountsMissing 5 | from zksync_sdk.types import (ChangePubKey, ChangePubKeyCREATE2, ChangePubKeyEcdsa, 6 | ChangePubKeyTypes, EncodedTx, ForcedExit, TokenLike, 7 | TransactionWithOptionalSignature, 8 | Transfer, TxEthSignature, 9 | Withdraw, MintNFT, WithdrawNFT, NFT, EncodedTxType, Order, Swap) 10 | from typing import List, Union, Tuple, Optional 11 | from decimal import Decimal 12 | 13 | 14 | @dataclass 15 | class BatchResult: 16 | transactions: list 17 | signature: TxEthSignature 18 | total_fees: dict 19 | 20 | 21 | class BatchBuilder: 22 | IS_ENCODED_TRANSACTION = "is_encoded_trx" 23 | ENCODED_TRANSACTION_TYPE = "internal_type" 24 | 25 | TRANSACTIONS_ENTRY = "transactions" 26 | SIGNATURE_ENTRY = "signature" 27 | 28 | @classmethod 29 | def from_wallet(cls, wallet: Wallet, nonce: int, txs: Optional[List[EncodedTx]] = None): 30 | obj = BatchBuilder(wallet, nonce, txs) 31 | return obj 32 | 33 | def __init__(self, wallet: Wallet, nonce: int, txs: Optional[List[EncodedTx]] = None): 34 | if txs is None: 35 | txs = [] 36 | self.wallet = wallet 37 | self.nonce = nonce 38 | self.batch_nonce = nonce 39 | self.transactions: List[dict] = [] 40 | for tx in txs: 41 | value = tx.dict() 42 | value[self.IS_ENCODED_TRANSACTION] = True 43 | value[self.ENCODED_TRANSACTION_TYPE] = tx.tx_type() 44 | self.transactions.append(value) 45 | 46 | async def build(self) -> BatchResult: 47 | if not self.transactions: 48 | raise RuntimeError("Transaction batch cannot be empty") 49 | res = await self._process_transactions() 50 | trans = res["trans"] 51 | signature = self.wallet.eth_signer.sign(res["msg"].encode()) 52 | return BatchResult(trans, signature, res["total_fee"]) 53 | 54 | def add_withdraw(self, 55 | eth_address: str, 56 | token: TokenLike, 57 | amount: Decimal, 58 | fee: Optional[Decimal] = None, 59 | valid_from=DEFAULT_VALID_FROM, 60 | valid_until=DEFAULT_VALID_UNTIL 61 | ): 62 | withdraw = { 63 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.WITHDRAW, 64 | self.IS_ENCODED_TRANSACTION: False, 65 | "eth_address": eth_address, 66 | "token": token, 67 | "amount": amount, 68 | "fee": fee, 69 | "valid_from": valid_from, 70 | "valid_until": valid_until 71 | } 72 | self.transactions.append(withdraw) 73 | 74 | def add_mint_nft(self, 75 | content_hash: str, 76 | recipient: str, 77 | fee_token: TokenLike, 78 | fee: Optional[Decimal] = None 79 | ): 80 | mint_nft = { 81 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.MINT_NFT, 82 | self.IS_ENCODED_TRANSACTION: False, 83 | "content_hash": content_hash, 84 | "recipient": recipient, 85 | "fee_token": fee_token, 86 | "fee": fee 87 | } 88 | self.transactions.append(mint_nft) 89 | 90 | def add_withdraw_nft(self, 91 | to_address: str, 92 | nft_token: NFT, 93 | fee_token: TokenLike, 94 | fee: Optional[Decimal] = None, 95 | valid_from=DEFAULT_VALID_FROM, 96 | valid_until=DEFAULT_VALID_UNTIL 97 | ): 98 | withdraw_nft = { 99 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.WITHDRAW_NFT, 100 | self.IS_ENCODED_TRANSACTION: False, 101 | "to_address": to_address, 102 | "nft_token": nft_token, 103 | "fee_token": fee_token, 104 | "fee": fee, 105 | "valid_from": valid_from, 106 | "valid_until": valid_until 107 | } 108 | self.transactions.append(withdraw_nft) 109 | 110 | def add_swap(self, 111 | orders: Tuple[Order, Order], 112 | fee_token: TokenLike, 113 | amounts: Optional[Tuple[Decimal, Decimal]] = None, 114 | fee: Optional[Decimal] = None 115 | ): 116 | if amounts is None: 117 | if orders[0].amount == 0 or orders[1].amount == 0: 118 | raise AmountsMissing("in this case you must specify amounts explicitly") 119 | swap = { 120 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.SWAP, 121 | self.IS_ENCODED_TRANSACTION: False, 122 | "orders": orders, 123 | "fee_token": fee_token, 124 | "amounts": amounts, 125 | "fee": fee 126 | } 127 | self.transactions.append(swap) 128 | 129 | def add_transfer(self, 130 | address_to: str, 131 | token: TokenLike, 132 | amount: Decimal, 133 | fee: Optional[Decimal] = None, 134 | valid_from=DEFAULT_VALID_FROM, 135 | valid_until=DEFAULT_VALID_UNTIL 136 | ): 137 | transfer = { 138 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.TRANSFER, 139 | self.IS_ENCODED_TRANSACTION: False, 140 | "from_address": self.wallet.address(), 141 | "to_address": address_to.lower(), 142 | "token": token, 143 | "amount": amount, 144 | "fee": fee, 145 | "valid_from": valid_from, 146 | "valid_until": valid_until 147 | } 148 | self.transactions.append(transfer) 149 | 150 | def add_change_pub_key(self, 151 | fee_token: TokenLike, 152 | eth_auth_type: Union[ChangePubKeyCREATE2, ChangePubKeyEcdsa, None], 153 | fee: Optional[Decimal] = None, 154 | valid_from=DEFAULT_VALID_FROM, 155 | valid_until=DEFAULT_VALID_UNTIL 156 | ): 157 | new_pubkey_hash = self.wallet.zk_signer.pubkey_hash_str() 158 | change_pub_key = { 159 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.CHANGE_PUB_KEY, 160 | self.IS_ENCODED_TRANSACTION: False, 161 | "account": self.wallet.address(), 162 | "new_pk_hash": new_pubkey_hash, 163 | "fee_token": fee_token, 164 | "fee": fee, 165 | "eth_auth_type": eth_auth_type, 166 | "valid_from": valid_from, 167 | "valid_until": valid_until 168 | } 169 | self.transactions.append(change_pub_key) 170 | 171 | def add_force_exit(self, 172 | target_address: str, 173 | token: TokenLike, 174 | fee: Optional[Decimal] = None, 175 | valid_from=DEFAULT_VALID_FROM, 176 | valid_until=DEFAULT_VALID_UNTIL 177 | ): 178 | forced_exit = { 179 | self.ENCODED_TRANSACTION_TYPE: EncodedTxType.FORCED_EXIT, 180 | self.IS_ENCODED_TRANSACTION: False, 181 | "target": target_address, 182 | "token": token, 183 | "fee": fee, 184 | "valid_from": valid_from, 185 | "valid_until": valid_until 186 | } 187 | self.transactions.append(forced_exit) 188 | 189 | async def _process_change_pub_key(self, obj: dict): 190 | if not obj[self.IS_ENCODED_TRANSACTION]: 191 | account_id = await self.wallet.get_account_id() 192 | token = await self.wallet.resolve_token(obj["fee_token"]) 193 | eth_auth_type = obj["eth_auth_type"] 194 | if isinstance(eth_auth_type, ChangePubKeyEcdsa): 195 | eth_auth_type = ChangePubKeyTypes.ecdsa 196 | elif isinstance(eth_auth_type, ChangePubKeyCREATE2): 197 | eth_auth_type = ChangePubKeyTypes.create2 198 | else: 199 | eth_auth_type = ChangePubKeyTypes.onchain 200 | fee = obj["fee"] 201 | if fee is None: 202 | if eth_auth_type == ChangePubKeyTypes.ecdsa: 203 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.change_pub_key_ecdsa, 204 | self.wallet.address(), 205 | token.id) 206 | elif eth_auth_type == ChangePubKeyTypes.onchain: 207 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.change_pub_key_onchain, 208 | self.wallet.address(), 209 | token.id) 210 | elif eth_auth_type == ChangePubKeyTypes.create2: 211 | fee = await self.wallet.zk_provider.get_transaction_fee( 212 | FeeTxType.change_pub_key_create2, 213 | self.wallet.address(), 214 | token.id) 215 | fee = fee.total_fee 216 | else: 217 | fee = token.from_decimal(fee) 218 | change_pub_key = ChangePubKey( 219 | account=obj["account"], 220 | account_id=account_id, 221 | new_pk_hash=obj["new_pk_hash"], 222 | token=token, 223 | fee=fee, 224 | nonce=self.nonce, 225 | valid_from=obj["valid_from"], 226 | valid_until=obj["valid_until"], 227 | eth_auth_data=obj["eth_auth_type"] 228 | ) 229 | eth_signature = self.wallet.eth_signer.sign(change_pub_key.get_eth_tx_bytes()) 230 | eth_auth_data = change_pub_key.get_auth_data(eth_signature.signature) 231 | change_pub_key.eth_auth_data = eth_auth_data 232 | 233 | zk_signature = self.wallet.zk_signer.sign_tx(change_pub_key) 234 | change_pub_key.signature = zk_signature 235 | else: 236 | change_pub_key = ChangePubKey( 237 | account_id=obj["accountId"], 238 | account=obj["account"], 239 | new_pk_hash=obj["newPkHash"], 240 | token=obj["fee_token"], 241 | fee=obj["fee"], 242 | nonce=self.nonce, 243 | eth_auth_data=obj["ethAuthData"], 244 | signature=obj["signature"], 245 | valid_from=obj["validFrom"], 246 | valid_until=obj["validUntil"] 247 | ) 248 | self.nonce += 1 249 | return change_pub_key 250 | 251 | async def _process_withdraw(self, obj: dict): 252 | if not obj[self.IS_ENCODED_TRANSACTION]: 253 | account_id = await self.wallet.get_account_id() 254 | token = await self.wallet.resolve_token(obj["token"]) 255 | 256 | fee = obj["fee"] 257 | if fee is None: 258 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.withdraw, 259 | obj["eth_address"], 260 | token.id) 261 | fee = fee.total_fee 262 | else: 263 | fee = token.from_decimal(fee) 264 | amount = token.from_decimal(obj["amount"]) 265 | 266 | withdraw = Withdraw(account_id=account_id, 267 | from_address=self.wallet.address(), 268 | to_address=obj["eth_address"], 269 | amount=amount, 270 | fee=fee, 271 | nonce=self.nonce, 272 | valid_from=obj["valid_from"], 273 | valid_until=obj["valid_until"], 274 | token=token) 275 | zk_signature = self.wallet.zk_signer.sign_tx(withdraw) 276 | withdraw.signature = zk_signature 277 | else: 278 | token = await self.wallet.resolve_token(obj["token"]) 279 | withdraw = Withdraw(account_id=obj["accountId"], 280 | from_address=obj["from"], 281 | to_address=obj["to"], 282 | amount=obj["amount"], 283 | fee=obj["fee"], 284 | nonce=self.nonce, 285 | valid_from=obj["validFrom"], 286 | valid_until=obj["validUntil"], 287 | token=token, 288 | signature=obj["signature"] 289 | ) 290 | self.nonce += 1 291 | return withdraw 292 | 293 | async def _process_transfer(self, obj): 294 | if not obj[self.IS_ENCODED_TRANSACTION]: 295 | account_id = await self.wallet.get_account_id() 296 | token = await self.wallet.resolve_token(obj["token"]) 297 | 298 | fee = obj["fee"] 299 | if fee is None: 300 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.transfer, 301 | obj["to_address"], 302 | token.id) 303 | fee = fee.total_fee 304 | else: 305 | fee = token.from_decimal(fee) 306 | 307 | amount = token.from_decimal(obj["amount"]) 308 | transfer = Transfer( 309 | account_id=account_id, 310 | from_address=obj["from_address"].lower(), 311 | to_address=obj["to_address"].lower(), 312 | token=token, 313 | amount=amount, 314 | fee=fee, 315 | nonce=self.nonce, 316 | valid_from=obj["valid_from"], 317 | valid_until=obj["valid_until"] 318 | ) 319 | zk_signature = self.wallet.zk_signer.sign_tx(transfer) 320 | transfer.signature = zk_signature 321 | else: 322 | token = await self.wallet.resolve_token(obj["token"]) 323 | transfer = Transfer( 324 | account_id=obj["accountId"], 325 | from_address=obj["from"], 326 | to_address=obj["to"], 327 | token=token, 328 | amount=obj["amount"], 329 | fee=obj["fee"], 330 | nonce=self.nonce, 331 | valid_from=obj["validFrom"], 332 | valid_until=obj["validUntil"], 333 | signature=obj["signature"] 334 | ) 335 | self.nonce += 1 336 | return transfer 337 | 338 | async def _process_forced_exit(self, obj): 339 | if not obj[self.IS_ENCODED_TRANSACTION]: 340 | account_id = await self.wallet.get_account_id() 341 | token = await self.wallet.resolve_token(obj["token"]) 342 | fee = obj["fee"] 343 | if fee is None: 344 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.transfer, 345 | obj["to_address"], 346 | token.id) 347 | fee = fee.total_fee 348 | else: 349 | fee = token.from_decimal(fee) 350 | forced_exit = ForcedExit(initiator_account_id=account_id, 351 | target=obj["target"], 352 | fee=fee, 353 | nonce=self.nonce, 354 | valid_from=obj["valid_from"], 355 | valid_until=obj["valid_until"], 356 | token=token) 357 | zk_signature = self.wallet.zk_signer.sign_tx(forced_exit) 358 | forced_exit.signature = zk_signature 359 | else: 360 | token = await self.wallet.resolve_token(obj["token"]) 361 | forced_exit = ForcedExit(initiator_account_id=obj["initiatorAccountId"], 362 | target=obj["target"], 363 | fee=obj["fee"], 364 | nonce=self.nonce, 365 | valid_from=obj["valid_from"], 366 | valid_until=obj["valid_until"], 367 | token=token, 368 | signature=obj["signature"]) 369 | self.nonce += 1 370 | return forced_exit 371 | 372 | async def _process_swap(self, obj): 373 | if not obj[self.IS_ENCODED_TRANSACTION]: 374 | fee_token = await self.wallet.resolve_token(obj["fee_token"]) 375 | fee = obj["fee"] 376 | if fee is None: 377 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.swap, 378 | self.wallet.address(), 379 | fee_token.id) 380 | fee = fee.total_fee 381 | else: 382 | fee = fee_token.from_decimal(fee) 383 | 384 | amounts = obj["amounts"] 385 | orders = obj["orders"] 386 | if amounts is None: 387 | amounts = (orders[0].amount, orders[1].amount) 388 | else: 389 | amounts = ( 390 | orders[0].token_sell.from_decimal(amounts[0]), 391 | orders[1].token_sell.from_decimal(amounts[1]) 392 | ) 393 | account_id = await self.wallet.get_account_id() 394 | swap = Swap( 395 | orders=orders, 396 | fee_token=fee_token, 397 | amounts=amounts, 398 | fee=fee, 399 | nonce=self.nonce, 400 | submitter_id=account_id, 401 | submitter_address=self.wallet.address() 402 | ) 403 | swap.signature = self.wallet.zk_signer.sign_tx(swap) 404 | else: 405 | fee_token = await self.wallet.resolve_token(obj["feeToken"]) 406 | swap = Swap( 407 | orders=obj["orders"], 408 | fee_token=fee_token, 409 | amounts=obj["amounts"], 410 | fee=obj["fee"], 411 | nonce=self.nonce, 412 | submitter_id=obj["submitterId"], 413 | submitter_address=obj["submitterAddress"], 414 | signature=obj["signature"] 415 | ) 416 | self.nonce += 1 417 | return swap 418 | 419 | async def _process_mint_nft(self, obj): 420 | if not obj[self.IS_ENCODED_TRANSACTION]: 421 | fee_token = await self.wallet.resolve_token(obj["fee_token"]) 422 | account_id = await self.wallet.get_account_id() 423 | 424 | fee = obj["fee"] 425 | if fee is None: 426 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.mint_nft, 427 | obj["recipient"], 428 | fee_token.id) 429 | fee = fee.total_fee 430 | else: 431 | fee = fee_token.from_decimal(fee) 432 | mint_nft = MintNFT(creator_id=account_id, 433 | creator_address=self.wallet.address(), 434 | content_hash=obj["content_hash"], 435 | recipient=obj["recipient"], 436 | fee=fee, 437 | fee_token=fee_token, 438 | nonce=self.nonce) 439 | zk_signature = self.wallet.zk_signer.sign_tx(mint_nft) 440 | mint_nft.signature = zk_signature 441 | else: 442 | fee_token = await self.wallet.resolve_token(obj["fee_token"]) 443 | mint_nft = MintNFT(creator_id=obj["creatorId"], 444 | creator_address=obj["creatorAddress"], 445 | content_hash=obj["content_hash"], 446 | recipient=obj["recipient"], 447 | fee=obj["fee"], 448 | fee_token=fee_token, 449 | nonce=self.nonce, 450 | signature=obj["signature"] 451 | ) 452 | self.nonce += 1 453 | return mint_nft 454 | 455 | async def _process_withdraw_nft(self, obj): 456 | if not obj[self.IS_ENCODED_TRANSACTION]: 457 | fee_token = await self.wallet.resolve_token(obj["fee_token"]) 458 | 459 | fee = obj["fee"] 460 | if fee is None: 461 | fee = await self.wallet.zk_provider.get_transaction_fee(FeeTxType.withdraw_nft, 462 | obj["to_address"], 463 | fee_token.id 464 | ) 465 | fee = fee.total_fee 466 | else: 467 | fee = fee_token.from_decimal(fee) 468 | 469 | account_id = await self.wallet.get_account_id() 470 | 471 | withdraw_nft = WithdrawNFT( 472 | account_id=account_id, 473 | from_address=self.wallet.address(), 474 | to_address=obj["to_address"], 475 | fee_token=fee_token, 476 | fee=fee, 477 | nonce=self.nonce, 478 | valid_from=obj["valid_from"], 479 | valid_until=obj["valid_until"], 480 | token_id=obj["nft_token"].id 481 | ) 482 | zk_signature = self.wallet.zk_signer.sign_tx(withdraw_nft) 483 | withdraw_nft.signature = zk_signature 484 | else: 485 | fee_token = await self.wallet.resolve_token(obj["feeToken"]) 486 | withdraw_nft = WithdrawNFT( 487 | account_id=obj["accountId"], 488 | from_address=obj["from"], 489 | to_address=obj["to"], 490 | fee_token=fee_token, 491 | fee=obj["fee"], 492 | nonce=obj["nonce"], 493 | valid_from=obj["validFrom"], 494 | valid_until=obj["validUntil"], 495 | token_id=obj["nft_token"].id, 496 | signature=obj["signature"] 497 | ) 498 | self.nonce += 1 499 | return withdraw_nft 500 | 501 | async def _process_transactions(self): 502 | message = "" 503 | trs = [] 504 | total_fee_map = dict() 505 | for obj in self.transactions: 506 | if obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.CHANGE_PUB_KEY: 507 | tr = await self._process_change_pub_key(obj) 508 | 509 | prev_value = total_fee_map.get(tr.token.symbol, Decimal(0)) 510 | dec_fee = tr.token.decimal_amount(tr.fee) 511 | total_fee_map[tr.token.symbol] = dec_fee + prev_value 512 | 513 | message += tr.batch_message_part() 514 | trs.append(TransactionWithOptionalSignature(tr)) 515 | elif obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.TRANSFER: 516 | tr = await self._process_transfer(obj) 517 | 518 | prev_value = total_fee_map.get(tr.token.symbol, Decimal(0)) 519 | dec_fee = tr.token.decimal_amount(tr.fee) 520 | total_fee_map[tr.token.symbol] = dec_fee + prev_value 521 | 522 | message += tr.batch_message_part() 523 | trs.append(TransactionWithOptionalSignature(tr)) 524 | elif obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.WITHDRAW: 525 | tr = await self._process_withdraw(obj) 526 | 527 | prev_value = total_fee_map.get(tr.token.symbol, Decimal(0)) 528 | dec_fee = tr.token.decimal_amount(tr.fee) 529 | total_fee_map[tr.token.symbol] = dec_fee + prev_value 530 | 531 | message += tr.batch_message_part() 532 | trs.append(TransactionWithOptionalSignature(tr)) 533 | elif obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.FORCED_EXIT: 534 | tr = await self._process_forced_exit(obj) 535 | 536 | prev_value = total_fee_map.get(tr.token.symbol, Decimal(0)) 537 | dec_fee = tr.token.decimal_amount(tr.fee) 538 | total_fee_map[tr.token.symbol] = dec_fee + prev_value 539 | 540 | message += tr.batch_message_part() 541 | trs.append(TransactionWithOptionalSignature(tr)) 542 | elif obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.MINT_NFT: 543 | tr = await self._process_mint_nft(obj) 544 | 545 | prev_value = total_fee_map.get(tr.fee_token.symbol, Decimal(0)) 546 | dec_fee = tr.fee_token.decimal_amount(tr.fee) 547 | total_fee_map[tr.fee_token.symbol] = dec_fee + prev_value 548 | 549 | message += tr.batch_message_part() 550 | trs.append(TransactionWithOptionalSignature(tr)) 551 | elif obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.WITHDRAW_NFT: 552 | tr = await self._process_withdraw_nft(obj) 553 | 554 | prev_value = total_fee_map.get(tr.fee_token.symbol, Decimal(0)) 555 | dec_fee = tr.fee_token.decimal_amount(tr.fee) 556 | total_fee_map[tr.fee_token.symbol] = dec_fee + prev_value 557 | 558 | message += tr.batch_message_part() 559 | trs.append(TransactionWithOptionalSignature(tr)) 560 | elif obj[self.ENCODED_TRANSACTION_TYPE] == EncodedTxType.SWAP: 561 | tr = await self._process_swap(obj) 562 | 563 | prev_value = total_fee_map.get(tr.fee_token.symbol, Decimal(0)) 564 | dec_fee = tr.fee_token.decimal_amount(tr.fee) 565 | total_fee_map[tr.fee_token.symbol] = dec_fee + prev_value 566 | message += tr.batch_message_part() 567 | trs.append(TransactionWithOptionalSignature(tr, [None, 568 | tr.orders[0].eth_signature, 569 | tr.orders[1].eth_signature] 570 | )) 571 | else: 572 | raise TypeError("_process_transactions is trying to process unimplemented type") 573 | message += f"Nonce: {self.batch_nonce}" 574 | result = dict(trans=trs, msg=message, total_fee=total_fee_map) 575 | return result 576 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/error.py: -------------------------------------------------------------------------------- 1 | class ZkSyncProviderError(Exception): 2 | pass 3 | 4 | 5 | class AccountDoesNotExist(ZkSyncProviderError): 6 | def __init__(self, address, *args): 7 | self.address = address 8 | super().__init__(*args) 9 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from decimal import Decimal 3 | from typing import List, Optional, Union 4 | from zksync_sdk.transport import JsonRPCTransport 5 | from zksync_sdk.types import (AccountState, ContractAddress, EncodedTx, EthOpInfo, Fee, Token, 6 | TokenLike, Tokens, TransactionDetails, TransactionWithSignature, 7 | TransactionWithOptionalSignature, 8 | TxEthSignature, Toggle2FA, ) 9 | from zksync_sdk.zksync_provider.types import FeeTxType 10 | from zksync_sdk.zksync_provider.transaction import Transaction 11 | 12 | __all__ = ['ZkSyncProviderInterface'] 13 | 14 | 15 | class ZkSyncProviderInterface(ABC): 16 | def __init__(self, provider: JsonRPCTransport): 17 | self.provider = provider 18 | 19 | @abstractmethod 20 | async def submit_tx(self, tx: EncodedTx, signature: Union[Optional[TxEthSignature], List[Optional[TxEthSignature]]], 21 | fast_processing: bool = False) -> Transaction: 22 | raise NotImplementedError 23 | 24 | @abstractmethod 25 | async def get_tokens(self) -> Tokens: 26 | raise NotImplementedError 27 | 28 | @abstractmethod 29 | async def submit_txs_batch(self, transactions: List[TransactionWithSignature], 30 | signatures: Optional[ 31 | Union[List[TxEthSignature], TxEthSignature] 32 | ] = None) -> List[Transaction]: 33 | raise NotImplementedError 34 | 35 | @abstractmethod 36 | async def submit_batch_builder_txs_batch(self, transactions: List[TransactionWithOptionalSignature], 37 | signature: TxEthSignature) -> List[Transaction]: 38 | raise NotImplementedError 39 | 40 | @abstractmethod 41 | async def get_contract_address(self) -> ContractAddress: 42 | raise NotImplementedError 43 | 44 | @abstractmethod 45 | async def get_state(self, address: str) -> AccountState: 46 | raise NotImplementedError 47 | 48 | @abstractmethod 49 | async def get_confirmations_for_eth_op_amount(self) -> int: 50 | raise NotImplementedError 51 | 52 | @abstractmethod 53 | async def get_account_nonce(self, address: str) -> int: 54 | raise NotImplementedError 55 | 56 | @abstractmethod 57 | async def get_tx_receipt(self, address: str) -> TransactionDetails: 58 | raise NotImplementedError 59 | 60 | @abstractmethod 61 | async def get_eth_tx_for_withdrawal(self, withdrawal_hash: str) -> str: 62 | raise NotImplementedError 63 | 64 | @abstractmethod 65 | async def get_priority_op_status(self, serial_id: int) -> EthOpInfo: 66 | raise NotImplementedError 67 | 68 | @abstractmethod 69 | async def get_transactions_batch_fee(self, tx_types: List[FeeTxType], addresses: List[str], 70 | token_like) -> int: 71 | raise NotImplementedError 72 | 73 | @abstractmethod 74 | async def get_transaction_fee(self, tx_type: FeeTxType, address: str, 75 | token_like: TokenLike) -> Fee: 76 | raise NotImplementedError 77 | 78 | @abstractmethod 79 | async def get_token_price(self, token: Token) -> Decimal: 80 | raise NotImplementedError 81 | 82 | @abstractmethod 83 | async def toggle_2fa(self, toggle2fa: Toggle2FA) -> bool: 84 | raise NotImplementedError 85 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/transaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from dataclasses import dataclass 3 | from enum import Enum, auto 4 | from typing import Optional 5 | 6 | 7 | class TransactionStatus(Enum): 8 | FAILED = auto() 9 | COMMITTED = auto() 10 | VERIFIED = auto() 11 | 12 | 13 | @dataclass 14 | class TransactionResult: 15 | status: TransactionStatus 16 | fail_reason: str 17 | 18 | 19 | class Transaction: 20 | 21 | @classmethod 22 | def build_transaction(cls, provider, transaction_id: str): 23 | transaction = cls(provider, transaction_id) 24 | return transaction 25 | 26 | def __init__(self, provider, transaction_hash: str): 27 | self.provider = provider 28 | self.transaction_hash = transaction_hash 29 | 30 | async def await_committed(self, attempts: Optional[int] = None, attempts_timeout: Optional[int] = None) \ 31 | -> TransactionResult: 32 | status = TransactionResult(TransactionStatus.FAILED, 33 | f"Transaction has not been executed with amount of attempts {attempts}" 34 | f"and timeout {attempts_timeout}") 35 | while True: 36 | if attempts is not None: 37 | if attempts <= 0: 38 | return status 39 | transaction_details = await self.provider.get_tx_receipt(self.transaction_hash) 40 | if attempts is not None: 41 | attempts -= 1 42 | if "failReason" in transaction_details and transaction_details["failReason"] is not None: 43 | return TransactionResult(TransactionStatus.FAILED, transaction_details['failReason']) 44 | 45 | if "block" in transaction_details: 46 | block = transaction_details["block"] 47 | if block is not None and "committed" in block and block["committed"]: 48 | return TransactionResult(TransactionStatus.COMMITTED, "") 49 | if attempts_timeout is not None: 50 | await asyncio.sleep(attempts_timeout / 1000) 51 | 52 | async def await_verified(self, attempts: Optional[int] = None, attempts_timeout: Optional[int] = None): 53 | intermediate_status = TransactionResult( 54 | TransactionStatus.FAILED, 55 | f"Transaction has not been executed with amount of attempts {attempts}" 56 | f"and timeout {attempts_timeout}") 57 | while True: 58 | if attempts is not None: 59 | if attempts <= 0: 60 | return intermediate_status 61 | 62 | transaction_details = await self.provider.get_tx_receipt(self.transaction_hash) 63 | if attempts is not None: 64 | attempts -= 1 65 | if "failReason" in transaction_details and transaction_details["failReason"] is not None: 66 | return TransactionResult(TransactionStatus.FAILED, transaction_details['failReason']) 67 | 68 | if "block" in transaction_details: 69 | block = transaction_details["block"] 70 | if block is not None and "committed" in block and block["committed"]: 71 | intermediate_status = TransactionResult(TransactionStatus.COMMITTED, "") 72 | 73 | if "block" in transaction_details: 74 | block = transaction_details["block"] 75 | if block is not None and \ 76 | "verified" in block and \ 77 | block["verified"]: 78 | return TransactionResult(TransactionStatus.VERIFIED, "") 79 | 80 | if attempts_timeout is not None: 81 | await asyncio.sleep(attempts_timeout / 1000) 82 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | __all__ = ['FeeTxType'] 4 | 5 | 6 | class FeeTxType(Enum): 7 | withdraw = "Withdraw" 8 | transfer = "Transfer" 9 | swap = "Swap" 10 | fast_withdraw = "FastWithdraw" 11 | change_pub_key_onchain = {"ChangePubKey": "Onchain"} 12 | change_pub_key_ecdsa = {"ChangePubKey": "ECDSA"} 13 | change_pub_key_create2 = {"ChangePubKey": "CREATE2"} 14 | mint_nft = "MintNFT" 15 | withdraw_nft = "WithdrawNFT" 16 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_provider/v01.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | from decimal import Decimal 3 | from typing import List, Optional, Union 4 | from web3 import Web3 5 | 6 | from zksync_sdk.types import (AccountState, ContractAddress, EncodedTx, EthOpInfo, Fee, Token, 7 | TokenLike, Tokens, TransactionDetails, TransactionWithSignature, 8 | TransactionWithOptionalSignature, 9 | TxEthSignature, Toggle2FA, ) 10 | from zksync_sdk.zksync_provider.error import AccountDoesNotExist 11 | from zksync_sdk.zksync_provider.interface import ZkSyncProviderInterface 12 | from zksync_sdk.zksync_provider.types import FeeTxType 13 | from zksync_sdk.zksync_provider.transaction import Transaction 14 | 15 | __all__ = ['ZkSyncProviderV01'] 16 | 17 | 18 | class ZkSyncProviderV01(ZkSyncProviderInterface): 19 | async def submit_tx(self, tx: EncodedTx, signature: Union[Optional[TxEthSignature], List[Optional[TxEthSignature]]], 20 | fast_processing: bool = False) -> Transaction: 21 | if isinstance(signature, List): 22 | signature = [s.dict() if s is not None else None for s in signature] 23 | else: 24 | signature = signature.dict() if signature is not None else None 25 | trans_id = await self.provider.request("tx_submit", 26 | [tx.dict(), signature, fast_processing]) 27 | return Transaction.build_transaction(self, trans_id) 28 | 29 | async def get_tokens(self) -> Tokens: 30 | data = await self.provider.request("tokens", None) 31 | tokens = [Token(address=Web3.toChecksumAddress(token['address']), 32 | id=token['id'], 33 | symbol=token['symbol'], 34 | decimals=token['decimals'] 35 | ) for token in data.values()] 36 | return Tokens(tokens=tokens) 37 | 38 | async def submit_txs_batch(self, transactions: List[TransactionWithSignature], 39 | signatures: Optional[ 40 | Union[List[TxEthSignature], TxEthSignature] 41 | ] = None) -> List[Transaction]: 42 | if signatures is None: 43 | signatures = [] 44 | elif isinstance(signatures, TxEthSignature): 45 | signatures = [signatures] 46 | transactions = [tr.dict() for tr in transactions] 47 | signatures = [sig.dict() for sig in signatures] 48 | trans_ids: List[str] = await self.provider.request("submit_txs_batch", [transactions, signatures]) 49 | return [Transaction.build_transaction(self, trans_id) for trans_id in trans_ids] 50 | 51 | async def submit_batch_builder_txs_batch(self, transactions: List[TransactionWithOptionalSignature], 52 | signature: TxEthSignature) -> List[Transaction]: 53 | trans = [tr.dict() for tr in transactions] 54 | params = [trans, signature.dict()] 55 | trans_ids: List[str] = await self.provider.request("submit_txs_batch", params) 56 | return [Transaction.build_transaction(self, trans_id) for trans_id in trans_ids] 57 | 58 | async def get_contract_address(self) -> ContractAddress: 59 | data = await self.provider.request("contract_address", None) 60 | return ContractAddress(**data) 61 | 62 | async def get_state(self, address: str) -> AccountState: 63 | data = await self.provider.request("account_info", [address]) 64 | if data is None: 65 | raise AccountDoesNotExist(address=address) 66 | if "accountType" in data and isinstance(data["accountType"], dict) and \ 67 | list(data["accountType"].keys())[0] == 'No2FA': 68 | data["accountType"] = 'No2FA' 69 | return AccountState(**data) 70 | 71 | async def get_confirmations_for_eth_op_amount(self) -> int: 72 | return await self.provider.request("get_confirmations_for_eth_op_amount", None) 73 | 74 | async def get_account_nonce(self, address: str) -> int: 75 | state = await self.get_state(address) 76 | return state.get_nonce() 77 | 78 | async def get_tx_receipt(self, address: str) -> TransactionDetails: 79 | return await self.provider.request("tx_info", [address]) 80 | 81 | async def get_eth_tx_for_withdrawal(self, withdrawal_hash: str) -> str: 82 | return await self.provider.request("get_eth_tx_for_withdrawal", [withdrawal_hash]) 83 | 84 | async def get_priority_op_status(self, serial_id: int) -> EthOpInfo: 85 | data = await self.provider.request("ethop_info", [serial_id]) 86 | return EthOpInfo(**data) 87 | 88 | # Please note that the batch fee returns the fee of the transaction in int and not in Fee 89 | # This is a server-side feature 90 | async def get_transactions_batch_fee(self, tx_types: List[FeeTxType], addresses: List[str], 91 | token_like) -> int: 92 | 93 | data = await self.provider.request('get_txs_batch_fee_in_wei', 94 | [[tx_type.value for tx_type in tx_types], 95 | addresses, token_like]) 96 | return int(data["totalFee"]) 97 | 98 | async def get_transaction_fee(self, tx_type: FeeTxType, address: str, 99 | token_like: TokenLike) -> Fee: 100 | 101 | data = await self.provider.request('get_tx_fee', [tx_type.value, address, token_like]) 102 | return Fee(**data) 103 | 104 | async def get_token_price(self, token: Token) -> Decimal: 105 | data = await self.provider.request('get_token_price', [token.symbol]) 106 | return Decimal(data) 107 | 108 | async def toggle_2fa(self, toggle2fa: Toggle2FA) -> bool: 109 | data = await self.provider.request('toggle_2fa', [toggle2fa.dict()]) 110 | return 'success' in data and data['success'] 111 | -------------------------------------------------------------------------------- /zksync_sdk/zksync_signer.py: -------------------------------------------------------------------------------- 1 | from eth_account.messages import encode_defunct 2 | from eth_account.signers.base import BaseAccount 3 | 4 | from zksync_sdk import ZkSyncLibrary 5 | from zksync_sdk.types import ChainId, EncodedTx, TxSignature 6 | 7 | 8 | def derive_private_key(library: ZkSyncLibrary, message: str, account: BaseAccount, 9 | chain_id: ChainId): 10 | if chain_id != ChainId.MAINNET: 11 | message = f"{message}\nChain ID: {chain_id}." 12 | signable_message = encode_defunct(message.encode()) 13 | signature = account.sign_message(signable_message) 14 | private_key = library.private_key_from_seed(signature.signature) 15 | return private_key 16 | 17 | 18 | class ZkSyncSigner: 19 | MESSAGE = "Access zkSync account.\n\nOnly sign this message for a trusted client!" 20 | 21 | def __init__(self, library: ZkSyncLibrary, private_key: bytes): 22 | self.library = library 23 | self.private_key = private_key 24 | self.public_key = self.library.get_public_key(self.private_key) 25 | 26 | @classmethod 27 | def from_account(cls, account: BaseAccount, library: ZkSyncLibrary, chain_id: ChainId): 28 | private_key = derive_private_key(library, cls.MESSAGE, account, chain_id) 29 | return cls( 30 | library=library, 31 | private_key=private_key, 32 | ) 33 | 34 | @classmethod 35 | def from_seed(cls, library: ZkSyncLibrary, seed: bytes): 36 | private_key = library.private_key_from_seed(seed) 37 | return cls( 38 | library=library, 39 | private_key=private_key, 40 | ) 41 | 42 | def pubkey_hash_str(self): 43 | return f"sync:{self.pubkey_hash().hex()}" 44 | 45 | def pubkey_hash(self): 46 | return self.library.get_pubkey_hash(self.public_key) 47 | 48 | def sign_tx(self, message: EncodedTx) -> TxSignature: 49 | signature = self.library.sign(self.private_key, message.encoded_message()) 50 | return TxSignature(signature=signature, public_key=self.public_key) 51 | --------------------------------------------------------------------------------