├── neo3 ├── py.typed ├── api │ ├── helpers │ │ ├── __init__.py │ │ ├── signing.py │ │ ├── stdlib.py │ │ ├── txbuilder.py │ │ └── unwrap.py │ └── __init__.py ├── network │ ├── payloads │ │ ├── __init__.py │ │ ├── empty.py │ │ ├── ping.py │ │ ├── consensus.py │ │ ├── inventory.py │ │ ├── filter.py │ │ ├── version.py │ │ └── extensible.py │ ├── convenience │ │ ├── __init__.py │ │ ├── flightinfo.py │ │ ├── requestinfo.py │ │ └── nodeweight.py │ ├── __init__.py │ ├── relaycache.py │ ├── ipfilter.py │ ├── capabilities.py │ └── message.py ├── wallet │ ├── __init__.py │ ├── types.py │ ├── scrypt_parameters.py │ └── utils.py ├── core │ ├── types │ │ └── __init__.py │ ├── interfaces.py │ ├── __init__.py │ ├── cryptography │ │ ├── __init__.py │ │ ├── bloomfilter.py │ │ ├── ecc.py │ │ └── merkletree.py │ └── utils.py ├── __init__.py ├── contracts │ ├── __init__.py │ ├── findoptions.py │ ├── callflags.py │ └── contract.py ├── singleton.py └── settings.py ├── tests ├── __init__.py ├── api │ ├── __init__.py │ ├── helpers │ │ ├── __init__.py │ │ └── test_stdlib.py │ └── test_wrappers.py ├── core │ ├── __init__.py │ ├── test_utils.py │ ├── test_cryptography.py │ └── test_biginteger.py ├── contracts │ ├── __init__.py │ ├── test_contractstate.py │ ├── test_utils.py │ ├── test_callflags.py │ └── test_nef.py ├── network │ ├── __init__.py │ ├── convenience │ │ ├── __init__.py │ │ └── test_various.py │ ├── test_issues.py │ ├── test_ipfilter.py │ ├── test_capabilities.py │ └── test_message.py ├── wallet │ ├── __init__.py │ ├── rc2-wallet.json │ └── test_account.py └── helpers.py ├── docs ├── source │ ├── advanced.md │ ├── index.md │ ├── mamba-logo.png │ ├── wrapper-hierarchy.png │ ├── getting-started.md │ └── faq.md ├── mkdocs.yml └── migrate_v2_v3.md ├── logo.png ├── .github ├── resources │ └── images │ │ ├── logo.png │ │ └── platformbadge.svg └── workflows │ ├── release-to-pypi.yml │ └── validate-pr-commit.yml ├── examples ├── shared │ ├── nep11-token │ │ ├── nep11-token.nef │ │ ├── compile_instructions.txt │ │ ├── go.mod │ │ ├── go.sum │ │ ├── nft.yml │ │ └── nep11-token.manifest.json │ ├── nep17-token │ │ ├── nep17token.nef │ │ └── nep17token.manifest.json │ ├── deploy-update-destroy │ │ ├── contract_v1.nef │ │ ├── contract_v2.nef │ │ ├── contract_v1.py │ │ ├── contract_v2.py │ │ ├── contract_v1.manifest.json │ │ └── contract_v2.manifest.json │ ├── coz-wallet.json │ ├── user-wallet.json │ └── __init__.py ├── README.md ├── vote.py ├── nep17-airdrop.py ├── nep17-transfer.py ├── nep-11-airdrop.py └── contract-deploy-update-destroy.py ├── .gitignore ├── .git-blame-ignore-revs ├── MANIFEST.in ├── LICENSE.md ├── pyproject.toml ├── Makefile └── README.rst /neo3/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /neo3/api/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/contracts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/network/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/wallet/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/api/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/network/convenience/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/advanced.md: -------------------------------------------------------------------------------- 1 | Please provide suggestions on Github! 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/logo.png -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: home.html 3 | title: MAMBA 4 | --- 5 | -------------------------------------------------------------------------------- /neo3/network/payloads/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | P2P network payloads and related classes. 3 | """ 4 | -------------------------------------------------------------------------------- /docs/source/mamba-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/docs/source/mamba-logo.png -------------------------------------------------------------------------------- /.github/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/.github/resources/images/logo.png -------------------------------------------------------------------------------- /docs/source/wrapper-hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/docs/source/wrapper-hierarchy.png -------------------------------------------------------------------------------- /neo3/network/convenience/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper classes to sync the chain over the P2P network and manage nodes. 3 | """ 4 | -------------------------------------------------------------------------------- /neo3/wallet/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wallet & account base classes. Utilities to validate and convert from/to addresses. 3 | """ 4 | -------------------------------------------------------------------------------- /examples/shared/nep11-token/nep11-token.nef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/examples/shared/nep11-token/nep11-token.nef -------------------------------------------------------------------------------- /examples/shared/nep17-token/nep17token.nef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/examples/shared/nep17-token/nep17token.nef -------------------------------------------------------------------------------- /examples/shared/nep11-token/compile_instructions.txt: -------------------------------------------------------------------------------- 1 | neo-go contract compile -i nft.go -c nft.yml -m nep11-token.manifest.json -o nep11-token 2 | -------------------------------------------------------------------------------- /neo3/core/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .uint import * 2 | from pybiginteger import BigInteger 3 | 4 | __all__ = ["UInt160", "UInt256", "BigInteger"] 5 | -------------------------------------------------------------------------------- /examples/shared/deploy-update-destroy/contract_v1.nef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/examples/shared/deploy-update-destroy/contract_v1.nef -------------------------------------------------------------------------------- /examples/shared/deploy-update-destroy/contract_v2.nef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CityOfZion/neo-mamba/HEAD/examples/shared/deploy-update-destroy/contract_v2.nef -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .idea/ 3 | .vscode/ 4 | .mypy_cache/ 5 | dist/ 6 | docs/site 7 | build/ 8 | htmlcov/ 9 | neo_mamba.egg-info/ 10 | *__pycache__* 11 | venv/ -------------------------------------------------------------------------------- /neo3/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | __version__ = "3.2.2" 4 | 5 | core_logger = logging.getLogger("neo3.core") 6 | network_logger = logging.getLogger("neo3.network") 7 | -------------------------------------------------------------------------------- /examples/shared/nep11-token/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nspcc-dev/neo-go/examples/nft-nd 2 | 3 | go 1.17 4 | 5 | require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262 6 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # .git-blame-ignore-revs 2 | # apply black to code base 3 | 008c07f606365e624e95ff25d66f72d36625e708 4 | # apply black to tests 5 | 260f4731126a501283498b6868fea82448ebf4a7 6 | -------------------------------------------------------------------------------- /neo3/contracts/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core components for handling smart contracts. Contains the Manifest, Neo Executable Format (NEF) and utilities for 3 | obtaining a contract hash, extracting public keys etc. 4 | """ 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include Makefile 3 | include README.rst 4 | graft examples/* 5 | graft docs/* 6 | recursive-exclude docs/build * 7 | include neo3/py.typed 8 | graft neo3/* 9 | graft tests/* 10 | global-exclude *.py[cod] 11 | -------------------------------------------------------------------------------- /examples/shared/nep11-token/go.sum: -------------------------------------------------------------------------------- 1 | github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262 h1:UTmSLZw5OpD/JPE1B5Vf98GF0zu2/Hsqq1lGLtStTUE= 2 | github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262/go.mod h1:23bBw0v6pBYcrWs8CBEEDIEDJNbcFoIh8pGGcf2Vv8s= 3 | -------------------------------------------------------------------------------- /neo3/core/interfaces.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class IJson(abc.ABC): 5 | @abc.abstractmethod 6 | def to_json(self) -> dict: 7 | """Convert object into JSON representation.""" 8 | 9 | @classmethod 10 | @abc.abstractmethod 11 | def from_json(cls, json: dict): 12 | """Create object from JSON""" 13 | -------------------------------------------------------------------------------- /neo3/network/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | P2P network classes. Holds `Block` and `Transaction` payloads (among others), a network node and helper classes for 3 | syncing the chain. 4 | """ 5 | 6 | import sys 7 | import asyncio 8 | 9 | if sys.platform == "win32": 10 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 11 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from neo3.core import serialization 2 | from neo3.core.serialization import BinaryReader, BinaryWriter 3 | 4 | 5 | class SerializableObject(serialization.ISerializable): 6 | def serialize(self, writer: BinaryWriter) -> None: 7 | pass 8 | 9 | def deserialize(self, reader: BinaryReader) -> None: 10 | pass 11 | 12 | def __len__(self): 13 | return 0 14 | -------------------------------------------------------------------------------- /neo3/core/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from enum import IntEnum 3 | from events import Events # type: ignore 4 | 5 | 6 | msgrouter = Events() 7 | 8 | # :noindex: 9 | 10 | 11 | class Size(IntEnum): 12 | """ 13 | Explicit bytes of memory consumed 14 | """ 15 | 16 | uint8 = 1 17 | uint16 = 2 18 | uint32 = 4 19 | uint64 = 8 20 | uint160 = 20 21 | uint256 = 32 22 | -------------------------------------------------------------------------------- /neo3/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes to interact with the network such as a specialised RPC Client for NEO Node RPC API and a facade for interacting 3 | with smart contracts over RPC. 4 | """ 5 | 6 | from .noderpc import ( 7 | NeoRpcClient, 8 | JsonRpcError, 9 | StackItem, 10 | StackItemType, 11 | ) 12 | 13 | __all__ = [ 14 | "NeoRpcClient", 15 | "JsonRpcError", 16 | "StackItem", 17 | "StackItemType", 18 | ] 19 | -------------------------------------------------------------------------------- /neo3/wallet/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias 2 | 3 | # Note that a NeoAddress is just a base58check encoded (address version + script hash). 4 | # * The address version is a fixed value since the inception of the chain. 5 | # * The script hash is the public key of an account (ECPair) wrapped with some extra data and hashed 6 | # with ripemd160. It is represented in the code with the UInt160 type from the `core` package 7 | NeoAddress: TypeAlias = str 8 | -------------------------------------------------------------------------------- /neo3/network/payloads/empty.py: -------------------------------------------------------------------------------- 1 | from neo3.core import serialization 2 | 3 | 4 | class EmptyPayload(serialization.ISerializable): 5 | def serialize(self, writer: serialization.BinaryWriter) -> None: 6 | """we don't have to do anything, because it should stay empty.""" 7 | 8 | def deserialize(self, reader: serialization.BinaryReader) -> None: 9 | """we don't have to do anything, because it has no attributes.""" 10 | 11 | def __len__(self): 12 | return 0 13 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | The modules in the root of this directory contain examples how to perform common actions on the NEO blockchain. 2 | The `shared` package contains all the test data and wallets that are used to set up the private network and can be ignored. 3 | 4 | **Requirements** 5 | 6 | Each example creates an isolated private chain allowing you to play with the code. This requires the `examples` depencencies 7 | to be installed. 8 | 9 | ```shell 10 | pip install neo-mamba[examples] 11 | ``` 12 | -------------------------------------------------------------------------------- /neo3/contracts/findoptions.py: -------------------------------------------------------------------------------- 1 | from enum import IntFlag 2 | 3 | 4 | class FindOptions(IntFlag): 5 | """ 6 | Possible search options when using the `System.Storage.Find` SYSCALL. 7 | """ 8 | 9 | NONE = 0 10 | KEYS_ONLY = 1 << 0 11 | REMOVE_PREFIX = 1 << 1 12 | VALUES_ONLY = 1 << 2 13 | DESERIALIZE_VALUES = 1 << 3 14 | PICK_FIELD0 = 1 << 4 15 | PICK_FIELD1 = 1 << 5 16 | BACKWARDS = 1 << 7 17 | ALL = ( 18 | KEYS_ONLY 19 | | REMOVE_PREFIX 20 | | VALUES_ONLY 21 | | DESERIALIZE_VALUES 22 | | PICK_FIELD0 23 | | PICK_FIELD1 24 | | BACKWARDS 25 | ) 26 | -------------------------------------------------------------------------------- /examples/shared/nep11-token/nft.yml: -------------------------------------------------------------------------------- 1 | name: "HASHY NFT" 2 | sourceurl: https://github.com/nspcc-dev/neo-go/ 3 | supportedstandards: ["NEP-11"] 4 | safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties"] 5 | events: 6 | - name: Transfer 7 | parameters: 8 | - name: from 9 | type: Hash160 10 | - name: to 11 | type: Hash160 12 | - name: amount 13 | type: Integer 14 | - name: tokenId 15 | type: ByteArray 16 | permissions: 17 | - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd 18 | methods: ["update", "destroy"] 19 | - methods: ["onNEP11Payment"] 20 | -------------------------------------------------------------------------------- /tests/wallet/rc2-wallet.json: -------------------------------------------------------------------------------- 1 | {"name":null,"version":"1.0","scrypt":{"n":16384,"r":8,"p":8},"accounts":[{"address":"NY9qiu8YScTM9oAc3nnaeNjaX5fnraaRTA","label":null,"isDefault":false,"lock":false,"key":"6PYMvxK89M59ZzxVwrgFKL1AQLYzVWXA7DgwEJ1i2A4dzEF7omth5Ae4Ry","contract":{"script":"DCEDpgwd6vFHsQaRw0THbl89rIO1Vf3Vo/jZ4vYjs9GvjfZBVuezJw==","parameters":[{"name":"signature","type":"Signature"}],"deployed":false},"extra":null},{"address":"NcmoFiYqThZJFiEYVF1BjYEk6YwF5vtkFA","label":null,"isDefault":false,"lock":false,"key":"6PYMvxK89M59ZzxVwrgFKL1AQLYzVWXA7DgwEJ1i2A4dzEF7omth5Ae4Ry","contract":{"script":"EQwhA6YMHerxR7EGkcNEx25fPayDtVX91aP42eL2I7PRr432EUGe0Nw6","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"extra":null}],"extra":null} -------------------------------------------------------------------------------- /examples/shared/coz-wallet.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coz", 3 | "version": "1.0", 4 | "scrypt": { 5 | "n": 16384, 6 | "r": 8, 7 | "p": 8 8 | }, 9 | "accounts": [ 10 | { 11 | "address": "NUVaphUShQPD82yoXcbvFkedjHX6rUF7QQ", 12 | "label": null, 13 | "isDefault": false, 14 | "lock": false, 15 | "key": "6PYWVHQmKFE8Q4knLWeYdtW8kjeFLgVyQjkwbEK5tHey2nDbm7NdnmFZQ8", 16 | "contract": { 17 | "script": "DCECkELfhVx2XN7+bmHqIrKcoPcdPT0y2JwWqreh4Ik7XdxBVuezJw==", 18 | "parameters": [ 19 | { 20 | "name": "parameter0", 21 | "type": "Signature" 22 | } 23 | ], 24 | "deployed": false 25 | }, 26 | "extra": {"password_to_unlock": "123"} 27 | } 28 | ], 29 | "extra": null 30 | } -------------------------------------------------------------------------------- /examples/shared/user-wallet.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "version": "1.0", 4 | "scrypt": { 5 | "n": 16384, 6 | "r": 8, 7 | "p": 8 8 | }, 9 | "accounts": [ 10 | { 11 | "address": "Nh5bER811LF2XYBKVkw1YDQmk8pSp15B2U", 12 | "label": null, 13 | "isDefault": false, 14 | "lock": false, 15 | "key": "6PYPZFtVe7RcCgxwBQ7q15FSBSVYiTz3RfYW1ri31ey8ckxAmJA6m3tMta", 16 | "contract": { 17 | "script": "DCECeImeFFgJrHqTjm3/MFlx8F/RSQDX91E24ilhj5EtBdhBVuezJw==", 18 | "parameters": [ 19 | { 20 | "name": "parameter0", 21 | "type": "Signature" 22 | } 23 | ], 24 | "deployed": false 25 | }, 26 | "extra": {"password_to_unlock": "123"} 27 | } 28 | ], 29 | "extra": null 30 | } -------------------------------------------------------------------------------- /tests/contracts/test_contractstate.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from neo3.contracts import contract, nef, manifest 3 | from neo3.core import types 4 | 5 | 6 | class ContractStateTestCase(unittest.TestCase): 7 | def test_equals(self): 8 | manifest_ = manifest.ContractManifest() 9 | nef_ = nef.NEF() 10 | state = contract.ContractState(1, nef_, manifest_, 0, types.UInt160.zero()) 11 | clone = contract.ContractState(1, nef_, manifest_, 0, types.UInt160.zero()) 12 | self.assertEqual(state, clone) 13 | 14 | nef2 = nef.NEF() 15 | state2 = contract.ContractState( 16 | 2, nef2, manifest_, 0, types.UInt160(b"\x01" * 20) 17 | ) 18 | self.assertNotEqual(state, state2) 19 | self.assertNotEqual(state, None) 20 | self.assertNotEqual(state, object()) 21 | -------------------------------------------------------------------------------- /neo3/singleton.py: -------------------------------------------------------------------------------- 1 | class _Singleton(object): 2 | """ 3 | Courtesy of Guido: https://www.python.org/download/releases/2.2/descrintro/#__new__ 4 | 5 | To create a singleton class, you subclass from Singleton; each subclass will have a single instance, 6 | no matter how many times its constructor is called. To further initialize the subclass instance, subclasses should 7 | override 'init' instead of __init__ - the __init__ method is called each time the constructor is called. 8 | """ 9 | 10 | def __new__(cls, *args, **kwds): 11 | it = cls.__dict__.get("__it__") 12 | if it is not None: 13 | return it 14 | cls.__it__ = it = object.__new__(cls) 15 | it.init(*args, **kwds) 16 | return it 17 | 18 | def init(self, *args, **kwds): 19 | # override this instead of implementing your own __init__ 20 | pass 21 | -------------------------------------------------------------------------------- /examples/shared/deploy-update-destroy/contract_v1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This version of the contract has an `add` method that increases the value by 1. 3 | """ 4 | 5 | from typing import Any 6 | from boa3.builtin import NeoMetadata, metadata, public 7 | from boa3.builtin.nativecontract.contractmanagement import ContractManagement 8 | 9 | 10 | @metadata 11 | def manifest_metadata() -> NeoMetadata: 12 | """ 13 | Defines this smart contract's metadata information. 14 | """ 15 | meta = NeoMetadata() 16 | meta.name = "Example Contract" 17 | return meta 18 | 19 | 20 | @public(safe=False) 21 | def update(nef_file: bytes, manifest: bytes, data: Any = None): 22 | ContractManagement.update(nef_file, manifest, data) 23 | 24 | 25 | @public 26 | def add(number: int) -> int: 27 | return number + 1 28 | 29 | 30 | @public(safe=False) 31 | def destroy(): 32 | ContractManagement.destroy() 33 | -------------------------------------------------------------------------------- /examples/shared/deploy-update-destroy/contract_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | This updated version of the contract has an `add` method that increases the value by 2. 3 | """ 4 | 5 | from typing import Any 6 | from boa3.builtin import NeoMetadata, metadata, public 7 | from boa3.builtin.nativecontract.contractmanagement import ContractManagement 8 | 9 | 10 | @metadata 11 | def manifest_metadata() -> NeoMetadata: 12 | """ 13 | Defines this smart contract's metadata information. 14 | """ 15 | meta = NeoMetadata() 16 | meta.name = "Example Contract" 17 | return meta 18 | 19 | 20 | @public(safe=False) 21 | def update(nef_file: bytes, manifest: bytes, data: Any = None): 22 | ContractManagement.update(nef_file, manifest, data) 23 | 24 | 25 | @public 26 | def add(number: int) -> int: 27 | return number + 2 28 | 29 | 30 | @public(safe=False) 31 | def destroy(): 32 | ContractManagement.destroy() 33 | -------------------------------------------------------------------------------- /tests/contracts/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from neo3.contracts import utils, nef 3 | from neo3.core import types 4 | 5 | 6 | class TestContractUtils(unittest.TestCase): 7 | def test_get_contract_hash(self): 8 | nef_ = nef.NEF("test", b"\x01\x02\x03") 9 | actual = utils.get_contract_hash(types.UInt160.zero(), nef_.checksum, "test") 10 | expected = types.UInt160.from_string( 11 | "0x576c9c6f22eea8fd823155b00141a4327bac8263" 12 | ) 13 | self.assertEqual(expected, actual) 14 | 15 | actual = utils.get_contract_hash( 16 | types.UInt160.from_string("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01"), 17 | nef_.checksum, 18 | "test", 19 | ) 20 | expected = types.UInt160.from_string( 21 | "0x55f776130883b2d486dec295ca74533663d0f8ea" 22 | ) 23 | self.assertEqual(expected, actual) 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-present COZ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /tests/network/test_issues.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import base64 3 | from neo3.network.payloads.transaction import Transaction 4 | 5 | 6 | class TestPayloadIssues(unittest.TestCase): 7 | def shortDescription(self): 8 | # disable docstring printing in test runner 9 | return None 10 | 11 | def test_witnesscondition_deserialization_of_tx_in_block_3367174(self): 12 | # ref: https://app.clickup.com/t/861mv391t 13 | # should not raise a ValueError 14 | try: 15 | raw_tx = base64.b64decode( 16 | "AE02bF6kD0ABAAAAAMjsAQAAAAAAJGEzAAG2cn7NBD7czWw0a+Oa4gSfyK4ROkABAQMCIBh8ns+7w1/Ttac3ifPmgPd4UTc4uQBeEQMtL9OkMAAAAAwUl6Y6Dqqt8iuCFxjZeiafFLqIGLkMFLZyfs0EPtzNbDRr45riBJ/IrhE6FMAfDAh0cmFuc2ZlcgwU6aNc0l5Z4RDGaj8d715RgxKfBJtBYn1bUgFCDEBEnZaxHmL9kNpS5XlIjdiJwiJWgw0GB9O1D4y0sGRe+rSaO0fZzaN2VfkJnxqR60oVEuf8xiE7cZD28cDifLUXKAwhA5FClptV/FtBBClyJ1f/+HKtWWUXMw6H3NcOZdzd2gcuQVbnsyc=" 17 | ) 18 | tx = Transaction.deserialize_from_bytes(raw_tx) 19 | except ValueError as e: 20 | if "Deserialization error - unknown witness condition" in str(e): 21 | self.assertTrue(False) 22 | -------------------------------------------------------------------------------- /.github/resources/images/platformbadge.svg: -------------------------------------------------------------------------------- 1 | platformsplatformslinux | macos (+ARM) | windowslinux | macos (+ARM) | windows -------------------------------------------------------------------------------- /neo3/wallet/scrypt_parameters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from jsonschema import validate # type: ignore 3 | from neo3.core import interfaces 4 | 5 | 6 | class ScryptParameters(interfaces.IJson): 7 | json_schema = { 8 | "type": "object", 9 | "properties": { 10 | "n": {"type": "integer"}, 11 | "r": {"type": "integer"}, 12 | "p": {"type": "integer"}, 13 | }, 14 | "required": ["n", "r", "p"], 15 | } 16 | 17 | def __init__(self, n: int = 16384, r: int = 8, p: int = 8): 18 | self.n = n 19 | self.r = r 20 | self.p = p 21 | 22 | def to_json(self) -> dict: 23 | """ 24 | Convert object into JSON representation. 25 | """ 26 | return {"n": self.n, "r": self.r, "p": self.p} 27 | 28 | @classmethod 29 | def from_json(cls, json: dict) -> ScryptParameters: 30 | """ 31 | Parse object out of JSON data. 32 | 33 | Args: 34 | json: a dictionary. 35 | 36 | Raises: 37 | KeyError: if the data supplied does not contain the necessary key. 38 | """ 39 | validate(json, schema=cls.json_schema) 40 | 41 | return cls(n=json["n"], r=json["r"], p=json["p"]) 42 | -------------------------------------------------------------------------------- /neo3/network/convenience/flightinfo.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class FlightInfo: 5 | """ 6 | An internal class for tracking a single outstanding header or block request from a specific node. 7 | 8 | It is used as part of ``SyncManager`` for syncing the chain over the P2P network in combination with the global data 9 | tracking :class:`RequestInfo ` class. 10 | """ 11 | 12 | def __init__(self, node_id: int, height: int): 13 | """ 14 | Args: 15 | node_id: the :attr:`~neo3.network.node.NeoNode.id` of the node the data is requested from 16 | height: the header or block height being requested 17 | """ 18 | 19 | #: The :attr:`~neo3.network.node.NeoNode.id` of the node the data is requested from. 20 | #: Defaults to `node_id` parameter. 21 | self.node_id = node_id 22 | 23 | #: The header or block height being requested. 24 | #: Defaults to `height` parameter. 25 | self.height = height 26 | 27 | #: float: UTC timestamp when the instance was created. 28 | self.start_time: float = datetime.utcnow().timestamp() 29 | 30 | def reset_start_time(self) -> None: 31 | """Reset the flight start time.""" 32 | self.start_time = datetime.utcnow().timestamp() 33 | -------------------------------------------------------------------------------- /examples/vote.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to vote for your favourite consensus node 3 | """ 4 | 5 | import asyncio 6 | from neo3.api.wrappers import ChainFacade, NeoToken 7 | from neo3.api.helpers.signing import sign_with_account 8 | from neo3.network.payloads.verification import Signer 9 | from examples import shared 10 | 11 | 12 | async def example_vote(node: shared.ExampleNode): 13 | wallet = shared.user_wallet 14 | account = wallet.account_default 15 | 16 | # This is your interface for talking to the blockchain 17 | facade = ChainFacade(rpc_host=node.rpc_host) 18 | facade.add_signer( 19 | sign_with_account(account), 20 | Signer(account.script_hash), 21 | ) 22 | 23 | # Dedicated Neo native contract wrapper 24 | neo = NeoToken() 25 | # get a list of candidates that can be voted on 26 | receipt = await facade.test_invoke(neo.candidates_registered()) 27 | candidates = receipt.result 28 | # the example chain only has 1 candidate, use that 29 | candidate_pk = candidates[0].public_key 30 | 31 | voter = account.address 32 | 33 | print("Casting vote and waiting for receipt...") 34 | receipt = await facade.invoke(neo.candidate_vote(voter, candidate_pk)) 35 | print(f"Success? {receipt.result}") 36 | 37 | 38 | if __name__ == "__main__": 39 | with shared.ExampleNode() as local_node: 40 | asyncio.run(example_vote(local_node)) 41 | -------------------------------------------------------------------------------- /tests/contracts/test_callflags.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from neo3.contracts.callflags import CallFlags 3 | 4 | 5 | class CallFlagsTestCase(unittest.TestCase): 6 | def test_parsing_from_string(self): 7 | tests = [ 8 | ("None", CallFlags.NONE), 9 | ("ReadStates", CallFlags.READ_STATES), 10 | ("WriteStates", CallFlags.WRITE_STATES), 11 | ("AllowCall", CallFlags.ALLOW_CALL), 12 | ("AllowNotify", CallFlags.ALLOW_NOTIFY), 13 | ("States", CallFlags.STATES), 14 | ("ReadOnly", CallFlags.READ_ONLY), 15 | ("All", CallFlags.ALL), 16 | ] 17 | for input, expected in tests: 18 | self.assertEqual(expected, CallFlags.from_csharp_name(input)) 19 | 20 | def test_parsing_invalid_input(self): 21 | with self.assertRaises(ValueError) as context: 22 | CallFlags.from_csharp_name("bla") 23 | self.assertEqual( 24 | "bla is not a valid member of CallFlags", str(context.exception) 25 | ) 26 | 27 | def test_parsing_from_string_with_multiple_flags(self): 28 | input = "ReadStates, AllowCall" 29 | cf = CallFlags.from_csharp_name(input) 30 | expected = CallFlags.READ_STATES | CallFlags.ALLOW_CALL 31 | self.assertNotEqual(CallFlags.READ_STATES, cf) 32 | self.assertNotEqual(CallFlags.ALLOW_CALL, cf) 33 | self.assertEqual(expected, cf) 34 | -------------------------------------------------------------------------------- /neo3/core/cryptography/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from .merkletree import MerkleTree 3 | from .bloomfilter import BloomFilter 4 | from .ecc import ECCCurve, ECPoint, KeyPair, ECCException, ecdsa_verify, ecdsa_sign 5 | import hashlib 6 | 7 | __all__ = [ 8 | "MerkleTree", 9 | "BloomFilter", 10 | "ECCCurve", 11 | "ECPoint", 12 | "KeyPair", 13 | "sign", 14 | "verify_signature", 15 | "ECCException", 16 | ] 17 | 18 | 19 | def sign( 20 | message: bytes, 21 | private_key: bytes, 22 | curve=ECCCurve.SECP256R1, 23 | hash_func=hashlib.sha256, 24 | ) -> bytes: 25 | return ecdsa_sign(private_key, message, curve, hash_func) 26 | 27 | 28 | def verify_signature( 29 | message: bytes, 30 | signature: bytes, 31 | public_key: bytes, 32 | curve: ECCCurve = ECCCurve.SECP256R1, 33 | hash_func=hashlib.sha256, 34 | ) -> bool: 35 | """ 36 | Test is the `signature` is signed by `public_key` valid for `message`. 37 | 38 | Args: 39 | message: the data the signature was created over 40 | signature: the signature to validate for `message` 41 | public_key: the public key the message was signed with 42 | curve: the ECC curve to use for verifying 43 | 44 | Raises: 45 | ValueError: for the Secp256r1 curve if the public key has an invalid format 46 | 47 | """ 48 | pub_key = ECPoint(public_key, curve, True) 49 | return ecdsa_verify(signature, message, pub_key, hash_func) 50 | -------------------------------------------------------------------------------- /neo3/network/payloads/ping.py: -------------------------------------------------------------------------------- 1 | """ 2 | Heartbeat payload with chain height information. 3 | """ 4 | 5 | from __future__ import annotations 6 | from datetime import datetime 7 | from random import randint 8 | from neo3.core import Size as s, serialization 9 | 10 | 11 | class PingPayload(serialization.ISerializable): 12 | def __init__(self, height: int = 0) -> None: 13 | #: The current local chain height 14 | self.current_height = height 15 | #: The local time in UTC as a timestamp 16 | self.timestamp = int(datetime.utcnow().timestamp()) 17 | #: Random number 18 | self.nonce = randint(100, 10000) 19 | 20 | def __len__(self) -> int: 21 | """Get the total size in bytes of the object.""" 22 | return s.uint32 + s.uint32 + s.uint32 23 | 24 | def serialize(self, writer: serialization.BinaryWriter) -> None: 25 | """ 26 | Serialize the object into a binary stream. 27 | 28 | Args: 29 | writer: instance. 30 | """ 31 | writer.write_uint32(self.current_height) 32 | writer.write_uint32(self.timestamp) 33 | writer.write_uint32(self.nonce) 34 | 35 | def deserialize(self, reader: serialization.BinaryReader) -> None: 36 | """ 37 | Deserialize the object from a binary stream. 38 | 39 | Args: 40 | reader: instance. 41 | """ 42 | self.current_height = reader.read_uint32() 43 | self.timestamp = reader.read_uint32() 44 | self.nonce = reader.read_uint32() 45 | -------------------------------------------------------------------------------- /neo3/contracts/callflags.py: -------------------------------------------------------------------------------- 1 | from enum import IntFlag 2 | 3 | 4 | class CallFlags(IntFlag): 5 | """ 6 | Describes the required call permissions for contract functions. 7 | """ 8 | 9 | NONE = 0 10 | READ_STATES = 0x1 11 | WRITE_STATES = 0x02 12 | ALLOW_CALL = 0x04 13 | ALLOW_NOTIFY = 0x08 14 | STATES = READ_STATES | WRITE_STATES 15 | READ_ONLY = READ_STATES | ALLOW_CALL 16 | ALL = STATES | ALLOW_CALL | ALLOW_NOTIFY 17 | 18 | @classmethod 19 | def from_csharp_name(cls, input: str): 20 | def get(input): 21 | if input == "None": 22 | return CallFlags.NONE 23 | elif input == "ReadStates": 24 | return CallFlags.READ_STATES 25 | elif input == "WriteStates": 26 | return CallFlags.WRITE_STATES 27 | elif input == "AllowCall": 28 | return CallFlags.ALLOW_CALL 29 | elif input == "AllowNotify": 30 | return CallFlags.ALLOW_NOTIFY 31 | elif input == "States": 32 | return CallFlags.STATES 33 | elif input == "ReadOnly": 34 | return CallFlags.READ_ONLY 35 | elif input == "All": 36 | return CallFlags.ALL 37 | else: 38 | raise ValueError(f"{input} is not a valid member of {cls.__name__}") 39 | 40 | flags = [get(flag.strip()) for flag in input.split(",")] 41 | 42 | result = flags[0] 43 | for flag in flags[1:]: 44 | result |= flag 45 | return result 46 | -------------------------------------------------------------------------------- /neo3/network/payloads/consensus.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from enum import IntEnum 3 | from neo3.core import Size as s, serialization 4 | from typing import TypeVar 5 | 6 | ConsensusMessage_t = TypeVar("ConsensusMessage_t", bound="ConsensusMessage") 7 | 8 | 9 | class ConsensusMessageType(IntEnum): 10 | CHANGE_VIEW = 0x00 11 | PREPARE_REQUEST = 0x20 12 | PREPARE_RESPONSE = 0x21 13 | COMMIT = 0x30 14 | RECOVERY_REQUEST = 0x40 15 | RECOVERY_MESSAGE = 0x41 16 | 17 | 18 | class ConsensusMessage(serialization.ISerializable): 19 | """ 20 | Base class for the various consensus messages 21 | """ 22 | 23 | def __init__(self, type: ConsensusMessageType): 24 | self.type = type 25 | self.view_number: int = 0 26 | 27 | def __len__(self): 28 | return s.uint8 + s.uint8 29 | 30 | def serialize(self, writer: serialization.BinaryWriter) -> None: 31 | """ 32 | Serialize the object into a binary stream. 33 | 34 | Args: 35 | writer: instance. 36 | """ 37 | writer.write_uint8(self.type) 38 | writer.write_uint8(self.view_number) 39 | 40 | def deserialize(self, reader: serialization.BinaryReader) -> None: 41 | """ 42 | Deserialize the object from a binary stream. 43 | 44 | Args: 45 | reader: instance. 46 | """ 47 | self.type = ConsensusMessageType(reader.read_uint8()) 48 | self.view_number = reader.read_uint8() 49 | 50 | @classmethod 51 | def _serializable_init(cls): 52 | return cls(ConsensusMessageType.CHANGE_VIEW) 53 | -------------------------------------------------------------------------------- /examples/shared/deploy-update-destroy/contract_v1.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example Contract", 3 | "groups": [], 4 | "abi": { 5 | "methods": [ 6 | { 7 | "name": "update", 8 | "offset": 0, 9 | "parameters": [ 10 | { 11 | "name": "nef_file", 12 | "type": "ByteArray" 13 | }, 14 | { 15 | "name": "manifest", 16 | "type": "ByteArray" 17 | }, 18 | { 19 | "name": "data", 20 | "type": "Any" 21 | } 22 | ], 23 | "returntype": "Void", 24 | "safe": false 25 | }, 26 | { 27 | "name": "add", 28 | "offset": 48, 29 | "parameters": [ 30 | { 31 | "name": "number", 32 | "type": "Integer" 33 | } 34 | ], 35 | "returntype": "Integer", 36 | "safe": false 37 | }, 38 | { 39 | "name": "destroy", 40 | "offset": 55, 41 | "parameters": [], 42 | "returntype": "Void", 43 | "safe": false 44 | } 45 | ], 46 | "events": [] 47 | }, 48 | "permissions": [ 49 | { 50 | "contract": "*", 51 | "methods": "*" 52 | } 53 | ], 54 | "trusts": [], 55 | "features": {}, 56 | "supportedstandards": [], 57 | "extra": null 58 | } -------------------------------------------------------------------------------- /examples/shared/deploy-update-destroy/contract_v2.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example Contract", 3 | "groups": [], 4 | "abi": { 5 | "methods": [ 6 | { 7 | "name": "update", 8 | "offset": 0, 9 | "parameters": [ 10 | { 11 | "name": "nef_file", 12 | "type": "ByteArray" 13 | }, 14 | { 15 | "name": "manifest", 16 | "type": "ByteArray" 17 | }, 18 | { 19 | "name": "data", 20 | "type": "Any" 21 | } 22 | ], 23 | "returntype": "Void", 24 | "safe": false 25 | }, 26 | { 27 | "name": "add", 28 | "offset": 48, 29 | "parameters": [ 30 | { 31 | "name": "number", 32 | "type": "Integer" 33 | } 34 | ], 35 | "returntype": "Integer", 36 | "safe": false 37 | }, 38 | { 39 | "name": "destroy", 40 | "offset": 55, 41 | "parameters": [], 42 | "returntype": "Void", 43 | "safe": false 44 | } 45 | ], 46 | "events": [] 47 | }, 48 | "permissions": [ 49 | { 50 | "contract": "*", 51 | "methods": "*" 52 | } 53 | ], 54 | "trusts": [], 55 | "features": {}, 56 | "supportedstandards": [], 57 | "extra": null 58 | } -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json 2 | site_name: mamba 3 | site_url: https://dojo.coz.io/neo3/mamba/ 4 | repo_url: https://github.com/CityOfZion/neo-mamba 5 | copyright: 2019-2022, COZ - Erik van den Brink 6 | docs_dir: 'source' 7 | use_directory_urls: false 8 | extra: 9 | generator: false 10 | plugins: 11 | - search 12 | - mkapi: 13 | src_dirs: ["../neo3"] 14 | theme: 15 | logo: mamba-logo.png 16 | favicon: mamba-logo.png 17 | name: material 18 | custom_dir: source 19 | features: 20 | - navigation.instant 21 | - navigation.tracking 22 | - navigation.tabs 23 | - toc.integrate 24 | - navigation.indexes 25 | icon: 26 | repo: fontawesome/brands/github 27 | palette: 28 | # Palette toggle for light mode 29 | # - scheme: default 30 | # toggle: 31 | # icon: material/toggle-switch 32 | # name: Switch to dark mode 33 | primary: yellow 34 | 35 | # # Palette toggle for dark mode 36 | # - scheme: slate 37 | # toggle: 38 | # icon: material/toggle-switch-off-outline 39 | # name: Switch to light mode 40 | # primary: yellow 41 | nav: 42 | - Home: 'index.md' 43 | - Getting started: 'getting-started.md' 44 | - Smart contracts: 'smart-contracts.md' 45 | - Advanced: 'advanced.md' 46 | - FAQ: 'faq.md' 47 | - API Reference: 'mkapi/api/../neo3' 48 | 49 | markdown_extensions: 50 | - admonition 51 | - pymdownx.highlight: 52 | linenums: true 53 | linenums_style: pymdownx-inline 54 | anchor_linenums: true 55 | # must keep superfences in combination with tabbed or tabs with just code won't work properly 56 | - pymdownx.superfences 57 | - pymdownx.tabbed: 58 | alternate_style: true 59 | - toc: 60 | permalink: true -------------------------------------------------------------------------------- /docs/source/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Mamba is a Python SDK for interacting with the NEO blockchain. It abstracts away the complexities 4 | of creating the data structures required to interact with smart contracts and change blockchain state. At the same time 5 | it is flexible enough that you can handcraft transactions or even the instructions to be executed by the virtual machine. 6 | Communication with the network is done through JSON-RPC servers. A list of public RPC servers can be found 7 | [here](https://dora.coz.io/monitor). 8 | 9 | Let's get setup and get a little taste of what using it looks like before diving into how it is structured and how to 10 | work with it to achieve your goals. 11 | 12 | ## Requirements 13 | * Python 3.10 14 | * Linux, OSX or Windows 15 | 16 | ## Installation 17 | 18 | === "UNIX" 19 | ```linenums="0" 20 | pip install neo-mamba 21 | ``` 22 | === "Windows" 23 | ```linenums="0" 24 | python -m pip install neo-mamba 25 | ``` 26 | 27 | ### From source 28 | 29 | === "UNIX" 30 | ```linenums="0" 31 | git clone https://github.com/CityOfZion/neo-mamba.git 32 | cd neo-mamba 33 | python -m venv venv 34 | source venv/bin/activate 35 | pip install -e . 36 | ``` 37 | === "Windows" 38 | ```linenums="0" 39 | git clone https://github.com/CityOfZion/neo-mamba.git 40 | cd neo-mamba 41 | python -m venv venv 42 | venv\Scripts\activate 43 | python -m pip install -e . 44 | ``` 45 | 46 | ## Quick example 47 | Get the NEO balance for an account 48 | 49 | ```py3 50 | import asyncio 51 | from neo3.api.wrappers import ChainFacade, NeoToken 52 | 53 | 54 | async def main(): 55 | facade = ChainFacade.node_provider_mainnet() 56 | neo = NeoToken() 57 | print( 58 | await facade.test_invoke(neo.balance_of("Nbsphyrdyz8ufeWKkNR1MUH2fuLABmqtqU")) 59 | ) 60 | 61 | 62 | if __name__ == "__main__": 63 | asyncio.run(main()) 64 | ``` 65 | -------------------------------------------------------------------------------- /examples/shared/nep11-token/nep11-token.manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"HASHY NFT","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"balanceOf","offset":166,"parameters":[{"name":"holder","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":41,"parameters":[],"returntype":"Integer","safe":true},{"name":"destroy","offset":1258,"parameters":[],"returntype":"Void","safe":false},{"name":"onNEP17Payment","offset":771,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"ownerOf","offset":508,"parameters":[{"name":"token","type":"ByteArray"}],"returntype":"Hash160","safe":true},{"name":"properties","offset":1331,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Map","safe":true},{"name":"symbol","offset":33,"parameters":[],"returntype":"String","safe":true},{"name":"tokens","offset":345,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"tokensOf","offset":377,"parameters":[{"name":"holder","type":"Hash160"}],"returntype":"InteropInterface","safe":true},{"name":"totalSupply","offset":43,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":523,"parameters":[{"name":"to","type":"Hash160"},{"name":"token","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"update","offset":1292,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"verify","offset":1251,"parameters":[],"returntype":"Boolean","safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"}]}]},"features":{},"groups":[],"permissions":[{"contract":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","methods":["update","destroy"]},{"contract":"*","methods":["onNEP11Payment"]}],"supportedstandards":["NEP-11"],"trusts":[],"extra":null} -------------------------------------------------------------------------------- /neo3/network/relaycache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Local cache to hold objects for responding to `GETDATA` network payloads. 3 | """ 4 | 5 | from __future__ import annotations 6 | from typing import Optional 7 | from neo3.network.payloads import inventory, block 8 | from neo3.core import types, msgrouter 9 | from neo3 import network_logger as logger, singleton 10 | from contextlib import suppress 11 | 12 | 13 | class RelayCache(singleton._Singleton): 14 | """ 15 | A cache holding transactions broadcast to the network to be included in a block. 16 | 17 | Will be accessed in response to a GETDATA network payload. 18 | """ 19 | 20 | def init(self): 21 | self.cache: dict[types.UInt256, inventory.IInventory] = dict() 22 | msgrouter.on_block_persisted += self.update_cache_for_block_persist 23 | 24 | def add(self, inventory: inventory.IInventory) -> None: 25 | """ 26 | Add an inventory to the cache. 27 | """ 28 | self.cache.update({inventory.hash(): inventory}) 29 | 30 | def get_and_remove( 31 | self, inventory_hash: types.UInt256 32 | ) -> Optional[inventory.IInventory]: 33 | """ 34 | Pop an inventory from the cache if found. 35 | """ 36 | try: 37 | return self.cache.pop(inventory_hash) 38 | except KeyError: 39 | return None 40 | 41 | def try_get(self, inventory_hash: types.UInt256) -> Optional[inventory.IInventory]: 42 | """ 43 | Get an inventory from the cache. 44 | """ 45 | return self.cache.get(inventory_hash, None) 46 | 47 | def update_cache_for_block_persist(self, block: block.Block) -> None: 48 | for tx in block.transactions: 49 | with suppress(KeyError): 50 | self.cache.pop(tx.hash()) 51 | logger.debug( 52 | f"Found {tx.hash()} in last persisted block. Removing from relay cache" 53 | ) 54 | 55 | def reset(self) -> None: 56 | """ 57 | Empty the cache. 58 | """ 59 | self.cache = dict() 60 | -------------------------------------------------------------------------------- /.github/workflows/release-to-pypi.yml: -------------------------------------------------------------------------------- 1 | # Checkout master, create sdist and bdist, and release to pypi 2 | 3 | name: Release to PyPi & Deploy docs 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | pypi-target: 8 | description: Deploy to PyPi [Main] or [Test] 9 | required: true 10 | default: 'Main' 11 | push: 12 | tags: 13 | - v*.* 14 | 15 | jobs: 16 | deploy-pypi: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5.6.0 22 | with: 23 | python-version: '3.13' 24 | - name: Create dist 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install twine build 28 | python -m build 29 | - name: Validate dist 30 | run: twine check dist/* 31 | - if: github.event.inputs.pypi-target == 'Main' || github.event_name == 'push' 32 | name: Publish to PyPi 33 | env: 34 | TWINE_USERNAME: __token__ 35 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 36 | run: | 37 | twine upload dist/* 38 | - if: github.event.inputs.pypi-target == 'Test' 39 | name: Publish to Test-PyPi 40 | env: 41 | TWINE_USERNAME: __token__ 42 | TWINE_PASSWORD: ${{ secrets.PYPI_TEST_API_TOKEN }} 43 | run: | 44 | twine upload --repository testpypi dist/* 45 | - if: github.event.inputs.pypi-target == 'Main' || github.event_name == 'push' 46 | name: Configure AWS credentials 47 | uses: aws-actions/configure-aws-credentials@v4 48 | with: 49 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 50 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 51 | aws-region: eu-north-1 52 | - if: github.event.inputs.pypi-target == 'Main' || github.event_name == 'push' 53 | name: Publish docs 54 | run: | 55 | pip install awscli 56 | pip install .[docs] 57 | mkdocs build -f docs/mkdocs.yml 58 | make docs-deploy 59 | -------------------------------------------------------------------------------- /docs/migrate_v2_v3.md: -------------------------------------------------------------------------------- 1 | v3.0 introduces breaking changes to streamline APIs or based on lessons learned. This document describes those changes and how to migrate 2 | 3 | # Account/wallet password handling 4 | 5 | ## signing functions 6 | - renamed `sign_insecure_with_account` -> `sign_with_account`. 7 | - also removed `password` argument. 8 | - renamed `sign_insecure_with_multisig_account` -> `sign_with_multisig_account`. 9 | - also removed `password` argument. 10 | - removed `sign_secure_with_account`. 11 | 12 | ## Account 13 | - removed `password` argument from `sign`, `sign_tx`, `sign_multisig_tx`, `create_new`, `from_encrypted_key`, `from_private_key`, `from_wif`. 14 | - added `password` argument to `to_json` & `from_json`. 15 | 16 | ## Wallet 17 | - removed `password` argument from `account_new`. 18 | - added `password` argument to `save`, `to_json`. 19 | - removed context manager. 20 | 21 | # NEP17Token `transfer_friendly` additional parameter 22 | In order to fix the `transfer_friendly` function an additional `decimals` parameter is added to the function signature, 23 | indicating the number of decimals the token has. 24 | 25 | # test_invoke* return type change 26 | Relevant if you make use of the `ChainFacade` related classes. 27 | - all `test_invoke*` now return an `InvokeReceipt` just like regular `invoke*` calls. 28 | - `invoke_multi` and `invoke_multi_raw` return `InvokeReceipt[Sequence]` instead of `Sequence`. 29 | 30 | # test_invoke* `signer` parameter type change 31 | Relevant if you make use of the `ChainFacade` related classes. 32 | The old `signers` parameter changed from `Optional[Sequence[verification.Signer]] = None` to `Optional[Sequence[SigningPair]] = None`. 33 | This streamlines the parameter with the persisting `invoke` variants, allowing for easy switching. 34 | 35 | # removal of deprecated functions and parameters 36 | - the `end` argument of `NeoToken.get_unclaimed_gas()` is removed. 37 | - the `balance_of` function of `_NEP11Contract` is renamed to `total_owned_by`. 38 | - the `balance_of_friendly` function of `_NEP11Contract` is renamed to `total_owned_by_friendly`. 39 | - the `candidate_unregister` of `NeoToken` is renamed to `candidate_deregister`. 40 | -------------------------------------------------------------------------------- /tests/network/convenience/test_various.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import unittest 3 | from neo3.network.convenience import nodeweight, requestinfo, flightinfo 4 | 5 | 6 | class RequestInfoTestCase(unittest.TestCase): 7 | def test_most_recent_flight(self): 8 | ri = requestinfo.RequestInfo(0) 9 | self.assertIsNone(ri.most_recent_flight()) 10 | 11 | fi = flightinfo.FlightInfo(1, 0) 12 | ri.add_new_flight(fi) 13 | most_recent = ri.most_recent_flight() 14 | self.assertEqual(ri.last_used_node, fi.node_id) 15 | 16 | def test_mark_failed(self): 17 | ri = requestinfo.RequestInfo(0) 18 | self.assertEqual(0, ri.failed_total) 19 | self.assertEqual(0, len(ri.failed_nodes)) 20 | 21 | ri.mark_failed_node(123) 22 | self.assertEqual(1, ri.failed_total) 23 | self.assertEqual(1, len(ri.failed_nodes)) 24 | 25 | 26 | class NodeWeightTestCase(unittest.TestCase): 27 | @unittest.skipIf(platform.system() == "Windows", "Unstable on CI for Windows") 28 | def test_weight(self): 29 | nw1 = nodeweight.NodeWeight(node_id=123) 30 | self.assertEqual(nodeweight.NodeWeight.SPEED_RECORD_COUNT, len(nw1.speed)) 31 | nw1.append_new_speed(1) 32 | self.assertEqual(nodeweight.NodeWeight.SPEED_RECORD_COUNT, len(nw1.speed)) 33 | 34 | nw2 = nodeweight.NodeWeight(node_id=456) 35 | nw2.append_new_speed(1000) 36 | 37 | # highest speed + longest time since used has best weight. Here nw1 has the worst speed, 38 | # but longest time since use. Therefore NW2 should win 39 | self.assertTrue(nw2 > nw1) 40 | 41 | # now make nw1 fastest, but test for being punished hard for timeouts 42 | nw1.append_new_speed(100_000) 43 | nw1.append_new_speed(100_000) 44 | nw1.append_new_speed(100_000) 45 | self.assertTrue(nw1 > nw2) 46 | nw1.timeout_count += 1 47 | self.assertFalse(nw1 > nw2) 48 | 49 | self.assertIn("=3.11.5", 27 | "pycryptodome==3.23.0", 28 | "pybiginteger==1.3.5", 29 | "pybiginteger-stubs==1.3.5", 30 | ] 31 | 32 | [project.optional-dependencies] 33 | dev = [ 34 | "aioresponses==0.7.6", 35 | "black==25.12.0", 36 | "build==0.10.0", 37 | "bump-my-version==0.12.0", 38 | "coverage>=7.3.2", 39 | "docutils==0.17.1", 40 | "mypy==1.7.1", 41 | "mypy-extensions==1.0.0", 42 | ] 43 | docs = [ 44 | "mkdocs==1.4.1", 45 | "mkdocs-material==8.5.7", 46 | "mkdocs-material-extensions==1.1", 47 | "mkapi-fix-coz==0.1.0", 48 | ] 49 | 50 | examples = [ 51 | "boa-test-constructor==v0.4.0" 52 | ] 53 | 54 | [project.urls] 55 | repository = "https://github.com/CityOfZion/neo-mamba" 56 | documentation = "https://mamba.coz.io/" 57 | 58 | 59 | [build-system] 60 | requires = ["setuptools"] 61 | build-backend = "setuptools.build_meta" 62 | 63 | [tool.black] 64 | target-version = ['py313'] 65 | 66 | [tool.setuptools.dynamic] 67 | version = { attr = "neo3.__version__" } 68 | 69 | [tool.bumpversion] 70 | current_version = "3.2.2" 71 | commit = true 72 | tag = true 73 | 74 | [[tool.bumpversion.files]] 75 | filename = "./neo3/__init__.py" 76 | search = "__version__ = \"{current_version}\"" 77 | replace = "__version__ = \"{new_version}\"" 78 | 79 | [tool.mypy] 80 | check_untyped_defs = true 81 | disable_error_code = "func-returns-value" 82 | 83 | [tool.coverage.run] 84 | source = ["neo3"] 85 | 86 | [tool.coverage.report] 87 | omit = ["neo3/core/cryptography/ecc*"] 88 | -------------------------------------------------------------------------------- /neo3/core/cryptography/bloomfilter.py: -------------------------------------------------------------------------------- 1 | from bitarray import bitarray # type: ignore 2 | from neo3crypto import mmh3_hash # type: ignore 3 | from typing import Optional 4 | 5 | 6 | class BloomFilter: 7 | """ 8 | BloomFilter implementation conforming to NEO's `implementation `_. # noqa 9 | """ 10 | 11 | def __init__(self, m: int, k: int, ntweak: int, elements: Optional[bytes] = None): 12 | """ 13 | 14 | Args: 15 | m: size of bitarray. 16 | k: number of hash functions. 17 | ntweak: correction factor. 18 | elements: hex-escaped bytearray of values to create the bitarray from. 19 | Warning: the bit array is truncated to size `m`. 20 | 21 | """ 22 | self.K = k 23 | self.seeds = [(p * 0xFBA4C795 + ntweak) % 4294967296 for p in range(0, k)] 24 | if elements: 25 | tmp_bits = bitarray(endian="little") 26 | tmp_bits.frombytes(elements) 27 | # truncate to m bits 28 | self.bits = tmp_bits[:m] 29 | else: 30 | self.bits = bitarray(m, endian="little") 31 | self.bits.setall(False) 32 | self.tweak = ntweak 33 | 34 | def add(self, element: bytes) -> None: 35 | """ 36 | Add an element to the filter. 37 | 38 | Args: 39 | element: hex-escaped bytearray. 40 | """ 41 | for s in self.seeds: 42 | h = mmh3_hash(element, s, signed=False) 43 | self.bits[h % len(self.bits)] = True 44 | 45 | def check(self, element: bytes) -> bool: 46 | """ 47 | Check if the element is present 48 | 49 | Args: 50 | element: hex-escaped bytearray 51 | 52 | Returns: True if present. False if not present. 53 | """ 54 | for s in self.seeds: 55 | h = mmh3_hash(element, s, signed=False) 56 | if not self.bits[h % len(self.bits)]: 57 | return False 58 | return True 59 | 60 | def get_bits(self) -> bytes: 61 | """ 62 | Return the filter bits. 63 | """ 64 | return self.bits.tobytes() 65 | -------------------------------------------------------------------------------- /neo3/network/payloads/inventory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import abc 3 | from enum import IntEnum 4 | from neo3.core import types, Size as s, utils, serialization 5 | from neo3.network.payloads import verification 6 | from collections.abc import Sequence 7 | 8 | 9 | class InventoryType(IntEnum): 10 | TX = 0x2B 11 | BLOCK = 0x2C 12 | CONSENSUS = 0x2D 13 | EXTENSIBLE = 0x2E 14 | 15 | 16 | class InventoryPayload(serialization.ISerializable): 17 | """ 18 | A payload used to share inventory hashes. 19 | 20 | Use by `getdata`, `getblocks` and `mempool` message types. 21 | """ 22 | 23 | def __init__(self, type: InventoryType, hashes: Sequence[types.UInt256]): 24 | """ 25 | Create payload. 26 | 27 | Args: 28 | type: indicator to what type of object the hashes of this payload relate to. 29 | hashes: hashes of "type" objects. 30 | """ 31 | self.type = type 32 | self.hashes = hashes 33 | 34 | def __len__(self): 35 | """Get the total size in bytes of the object.""" 36 | return s.uint8 + utils.get_var_size(self.hashes) 37 | 38 | def serialize(self, writer: serialization.BinaryWriter) -> None: 39 | """ 40 | Serialize the object into a binary stream. 41 | 42 | Args: 43 | writer: instance. 44 | """ 45 | writer.write_uint8(self.type) 46 | writer.write_var_int(len(self.hashes)) 47 | for h in self.hashes: # type: types.UInt256 48 | writer.write_bytes(h.to_array()) 49 | 50 | def deserialize(self, reader: serialization.BinaryReader) -> None: 51 | """ 52 | Deserialize the object from a binary stream. 53 | 54 | Args: 55 | reader: instance. 56 | """ 57 | self.type = InventoryType(reader.read_uint8()) 58 | self.hashes = reader.read_serializable_list(types.UInt256) 59 | 60 | @classmethod 61 | def _serializable_init(cls): 62 | return cls(InventoryType.BLOCK, []) 63 | 64 | 65 | class IInventory(verification.IVerifiable): 66 | """ 67 | Inventory interface. 68 | """ 69 | 70 | @abc.abstractmethod 71 | def hash(self) -> types.UInt256: 72 | """""" 73 | 74 | @property 75 | @abc.abstractmethod 76 | def inventory_type(self) -> InventoryType: 77 | """""" 78 | -------------------------------------------------------------------------------- /docs/source/faq.md: -------------------------------------------------------------------------------- 1 | ## Why are consensus committee only functions not wrapped? 2 | The group of users that can make use of this is _very_ limited. By ommitting these functions the API list stays short and 3 | relevant to the biggest group of users. Those who do wish to use these functions can always use the generic 4 | `call_function()` method on the contract of choice to call them. 5 | 6 | ## Why is the native ContractManagement contract not wrapped? 7 | The contract `deploy`, `update` and `destroy` functionality is already part of the `GenericContract` base class used in 8 | all contract wrappers. 9 | 10 | ## Why is the native Ledger contract not wrapped? 11 | All information that can be obtained from the `Ledger` contract can also be obtained using the `NeoRpcClient`. In some 12 | cases the `Ledger` contract returns even incomplete data. For example `Ledger.GetBlock` returns a `TrimmedBlock` without 13 | transactions as opposed to `NeoRpcClient.get_block()` which returns the complete block. The `Ledger` contract is really 14 | intended to be consumed by smart contracts. 15 | 16 | ## Why does the IJson interface consume and produce dictionaries? 17 | This was originally used in the full node version of Mamba. However, it seems like the standard in the Python community 18 | if judged by looking at popular packages/frameworks like `requests` and `aiohttp`. Also, frameworks like `FastAPI`, 19 | `Django` and `Flask` all have ways of consuming a `dict` when returning a json response. It seems like the best choice 20 | for these reasons. 21 | 22 | ## How do I prevent 'error: "as_none" of "StackItem" does not return a value' when type checking with MyPy? 23 | The `mypy` team made an unfortunate choice to include a restriction on something that is actually a code style choice. 24 | They [refuse](https://github.com/python/mypy/issues/6549) to change the default behaviour despite the raised complaints 25 | and other type checkers like pyright not falling over this. The error can be disabled with 26 | `--disable-error-code func-returns-value` or by adding it to your configuration file i.e. for `pyproject.toml` use 27 | ```toml 28 | [tool.mypy] 29 | disable_error_code = "func-returns-value" 30 | ``` 31 | 32 | ## How do I <insert topic>? 33 | Have a look at the examples on GitHub if the documentation doesn't cover your question. If the examples also don't answer 34 | your question then feel free to ask on GitHub or ask in #python on the [NEO Discord server](https://discord.gg/rvZFQ5382k). 35 | -------------------------------------------------------------------------------- /neo3/api/helpers/signing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Signing functions for use with `ChainFacade.invoke`. 3 | """ 4 | 5 | import os 6 | from dataclasses import dataclass 7 | from neo3.network.payloads import transaction 8 | from typing import Callable, Awaitable 9 | from neo3.wallet import account 10 | 11 | 12 | @dataclass 13 | class SigningDetails: 14 | network: int 15 | #: for which Signer we're adding a witness 16 | # witness_index: int 17 | 18 | 19 | # The signing function adds a witness to the provided transaction 20 | # SigningFunction = Callable[[transaction.Transaction, SigningDetails], None] 21 | SigningFunction = Callable[[transaction.Transaction, SigningDetails], Awaitable] 22 | 23 | 24 | def sign_with_account(acc: account.Account) -> SigningFunction: 25 | """ 26 | Sign and add a witness using the account and the provided account password. 27 | """ 28 | 29 | async def account_signer(tx: transaction.Transaction, details: SigningDetails): 30 | # this will automatically add a witness 31 | acc.sign_tx(tx, details.network) 32 | 33 | return account_signer 34 | 35 | 36 | def sign_with_ledger() -> SigningFunction: 37 | raise NotImplementedError 38 | 39 | 40 | def sign_on_remote_server() -> SigningFunction: 41 | async def remote_server_signer( 42 | tx: transaction.Transaction, details: SigningDetails 43 | ): 44 | # call some remote API to get the signature 45 | # and add a witness with the signature 46 | raise NotImplementedError 47 | 48 | return remote_server_signer 49 | 50 | 51 | def sign_with_multisig_account(acc: account.Account) -> SigningFunction: 52 | """ 53 | Sign and add a multi-signature witness. 54 | 55 | This only works for a 1 out of n multi-signature account. 56 | 57 | Args: 58 | acc: a multi-signature account 59 | """ 60 | 61 | async def account_signer(tx: transaction.Transaction, details: SigningDetails): 62 | ctx = account.MultiSigContext() 63 | # this will automatically add a witness 64 | acc.sign_multisig_tx(tx, ctx, details.network) 65 | 66 | return account_signer 67 | 68 | 69 | def no_signing() -> SigningFunction: 70 | """ 71 | Dummy signing function to use with test invocations. 72 | """ 73 | 74 | async def oh_noes(unused: transaction.Transaction, unused2: SigningDetails): 75 | raise Exception( 76 | "can't sign with dummy signing function. Did you add a test_signer to ChainFacade?" 77 | ) 78 | 79 | return oh_noes 80 | -------------------------------------------------------------------------------- /neo3/network/convenience/requestinfo.py: -------------------------------------------------------------------------------- 1 | from neo3.network.convenience import flightinfo 2 | from typing import Optional 3 | 4 | 5 | class RequestInfo: 6 | """An internal class for tracking an outstanding header or block request over all connected nodes. 7 | 8 | It is used as part of ``SyncManager`` for syncing the chain over the P2P network. 9 | For each header or block being requested from the network, 1 RequestInfo object is created. Each RequestInfo stores 10 | the data to track outstanding flights (=requests from connected nodes). 11 | """ 12 | 13 | def __init__(self, height: int): 14 | """ 15 | Args: 16 | height: the header or block height being tracked. 17 | """ 18 | #: The header or block height being tracked. 19 | self.height: int = height 20 | 21 | #: A _dictionary holding node :attr:`~neo3.network.node.NeoNode.id` keys, with UTC timestamp values. 22 | self.failed_nodes: dict[int, int] = dict() 23 | 24 | #: The total count of flights for this request that failed to meet the time requirements. 25 | self.failed_total: int = 0 26 | 27 | #: A _dictionary holding node_id keys with :class:`FlightInfo ` 28 | # object values. 29 | self.flights: dict[int, flightinfo.FlightInfo] = dict() 30 | 31 | #: The :attr:`~neo3.network.node.NeoNode.id` of the node last used for a flight. 32 | self.last_used_node: int = -1 33 | 34 | def add_new_flight(self, flight_info: flightinfo.FlightInfo) -> None: 35 | """ 36 | Store a new flight to the tracking list. 37 | 38 | Args: 39 | flight_info: the flight to add. 40 | """ 41 | self.flights[flight_info.node_id] = flight_info 42 | self.last_used_node = flight_info.node_id 43 | 44 | def most_recent_flight(self) -> Optional[flightinfo.FlightInfo]: 45 | """ 46 | Get the last `FlightInfo` object created for this 47 | request. 48 | """ 49 | try: 50 | return self.flights[self.last_used_node] 51 | except KeyError: 52 | return None 53 | 54 | def mark_failed_node(self, node_id: int) -> None: 55 | """ 56 | Tag a node for failure. 57 | 58 | SyncManager tags nodes that do not return the requested data before a specified timeout. 59 | 60 | Args: 61 | node_id: the `NeoNode.id` of the node the data is requested from. 62 | """ 63 | self.failed_nodes[node_id] = self.failed_nodes.get(node_id, 0) + 1 64 | self.failed_total += 1 65 | -------------------------------------------------------------------------------- /neo3/network/payloads/filter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bloomfilter payloads. 3 | """ 4 | 5 | from __future__ import annotations 6 | from neo3.core import serialization, utils, Size as s, cryptography as crypto 7 | 8 | 9 | class FilterAddPayload(serialization.ISerializable): 10 | def __init__(self, data: bytes): 11 | """ 12 | Create payload. 13 | 14 | Args: 15 | data: the data to add to the configured bloomfilter. 16 | 17 | """ 18 | self.data = data 19 | 20 | def __len__(self): 21 | return utils.get_var_size(self.data) 22 | 23 | def serialize(self, writer: serialization.BinaryWriter) -> None: 24 | """ 25 | Serialize the object into a binary stream. 26 | 27 | Args: 28 | writer: instance. 29 | """ 30 | writer.write_var_bytes(self.data) 31 | 32 | def deserialize(self, reader: serialization.BinaryReader) -> None: 33 | """ 34 | Deserialize the object from a binary stream. 35 | 36 | Args: 37 | reader: instance. 38 | """ 39 | self.data = reader.read_var_bytes(520) 40 | 41 | @classmethod 42 | def _serializable_init(cls): 43 | return cls(b"") 44 | 45 | 46 | class FilterLoadPayload(serialization.ISerializable): 47 | def __init__(self, filter: crypto.BloomFilter): 48 | """ 49 | Create payload. 50 | 51 | Args: 52 | filter: bloom filter to load 53 | """ 54 | self.filter = filter.get_bits() 55 | self.K = filter.K 56 | self.tweak = filter.tweak 57 | 58 | def __len__(self): 59 | return utils.get_var_size(self.filter) + s.uint8 + s.uint32 60 | 61 | def serialize(self, writer: serialization.BinaryWriter) -> None: 62 | """ 63 | Serialize the object into a binary stream. 64 | 65 | Args: 66 | writer: instance. 67 | """ 68 | writer.write_var_bytes(self.filter) 69 | writer.write_uint8(self.K) 70 | writer.write_uint32(self.tweak) 71 | 72 | def deserialize(self, reader: serialization.BinaryReader) -> None: 73 | """ 74 | Deserialize the object from a binary stream. 75 | 76 | Args: 77 | reader: instance. 78 | """ 79 | self.filter = reader.read_var_bytes(max=36000) 80 | self.K = reader.read_uint8() 81 | if self.K > 50: 82 | raise ValueError("Deserialization error - K exceeds limit of 50") 83 | self.tweak = reader.read_uint32() 84 | 85 | @classmethod 86 | def _serializable_init(cls): 87 | bf = crypto.BloomFilter(8, 2, 345) 88 | return cls(bf) 89 | -------------------------------------------------------------------------------- /examples/nep17-airdrop.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to send tokens to multiple accounts in one go. 3 | It will mint the "COZ Token" 4 | """ 5 | 6 | import asyncio 7 | from neo3.api.wrappers import ChainFacade, NeoToken, NEP17Contract 8 | from neo3.api.helpers.signing import sign_with_account 9 | from neo3.network.payloads.verification import Signer 10 | from examples import shared 11 | 12 | 13 | async def example_airdrop(node: shared.ExampleNode): 14 | # This example shows how to airdrop NEP-17 tokens 15 | wallet = shared.user_wallet 16 | account = wallet.account_default 17 | 18 | # This is your interface for talking to the blockchain 19 | facade = ChainFacade(rpc_host=node.rpc_host) 20 | facade.add_signer( 21 | sign_with_account(account), 22 | Signer(account.script_hash), # default scope is CALLED_BY_ENTRY 23 | ) 24 | 25 | # Use the generic NEP17 class to wrap the token 26 | token = NEP17Contract(shared.coz_token_hash) 27 | receipt = await facade.test_invoke(token.balance_of(account.address)) 28 | print(f"Current COZ token balance: {receipt.result}") 29 | 30 | # First we have to mint the tokens to our own wallet 31 | # We do this by sending NEO to the contract 32 | # We increase the retry delay to match our local chain block production time 33 | neo = NeoToken() 34 | print("Minting once...", end="") 35 | receipt = await facade.invoke( 36 | neo.transfer( 37 | source=account.address, destination=shared.coz_token_hash, amount=100 38 | ) 39 | ) 40 | print(receipt.result) 41 | print("Minting twice...", end="") 42 | receipt = await facade.invoke( 43 | neo.transfer( 44 | source=account.address, destination=shared.coz_token_hash, amount=100 45 | ) 46 | ) 47 | 48 | print(receipt.result) 49 | 50 | receipt = await facade.test_invoke(token.balance_of(account.address)) 51 | print(f"New COZ token balance: {receipt.result}") 52 | 53 | # Now let's airdrop the tokens 54 | destination_addresses = [ 55 | "NWuHQdxabXPdC6vVwJhxjYELDQPqc1d4TG", 56 | "NhVnpBxSRjkScZKHGzsEreYAMS1qRrNdaH", 57 | "NanYZRm6m6sa6Z6F3RBRYSXqdpg5rZqxdZ", 58 | "NUqLhf1p1vQyP2KJjMcEwmdEBPnbCGouVp", 59 | "NKuyBkoGdZZSLyPbJEetheRhMjeznFZszf", 60 | ] 61 | print("Airdropping 10 tokens and waiting for receipt") 62 | print( 63 | await facade.invoke( 64 | token.transfer_multi(account.address, destination_addresses, 10), 65 | ) 66 | ) 67 | 68 | 69 | if __name__ == "__main__": 70 | with shared.ExampleNode() as local_node: 71 | asyncio.run(example_airdrop(local_node)) 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: black build clean clean-test clean-pyc clean-build clean-docs docs docs-deploy help test coverage version-major version-minor version-patch 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python3 -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test clean-docs ## remove all build, test, coverage and Python artifacts 30 | 31 | 32 | clean-build: ## remove build artifacts 33 | rm -rf build/ 34 | rm -rf dist/ 35 | rm -rf .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-docs: ## clean the /docs/ 40 | rm -rf docs/site/ 41 | 42 | clean-pyc: ## remove Python file artifacts 43 | find . -name '*.pyc' -exec rm -f {} + 44 | find . -name '*.pyo' -exec rm -f {} + 45 | find . -name '*~' -exec rm -f {} + 46 | find . -name '__pycache__' -exec rm -fr {} + 47 | 48 | clean-test: ## remove test and coverage artifacts 49 | rm -f .coverage 50 | rm -rf htmlcov/ 51 | 52 | test: ## run tests quickly with the default Python 53 | python -m unittest discover -v -s tests/ 54 | 55 | coverage: ## check code coverage quickly with the default Python 56 | coverage run -m unittest discover -v -s tests/ 57 | coverage report 58 | coverage html 59 | $(BROWSER) htmlcov/index.html 60 | 61 | docs: ## generate Sphinx HTML documentation, including API docs 62 | rm -rf docs/site/ 63 | mkdocs build -f docs/mkdocs.yml 64 | $(BROWSER) docs/site/index.html 65 | 66 | docs-deploy: ## manually deploy the docs to github pages 67 | aws s3 sync ./docs/site s3://docs-coz/neo3/mamba --acl public-read 68 | 69 | type: ## perform static type checking using mypy 70 | mypy neo3/ 71 | 72 | black: ## apply black formatting 73 | black neo3/ examples/ tests/ 74 | 75 | build: ## create source distribution and wheel 76 | python -m build 77 | 78 | version-major: ## bump the major version prior to release, auto commits and tag 79 | bump-my-version bump major 80 | 81 | version-minor: ## bump the minor version prior to release, auto commits and tag 82 | bump-my-version bump minor 83 | 84 | version-patch: ## bump the patch version prior to release, auto commits and tag 85 | bump-my-version bump patch 86 | -------------------------------------------------------------------------------- /examples/nep17-transfer.py: -------------------------------------------------------------------------------- 1 | """ 2 | This files has 2 examples that show how to transfer NEP-17 tokens for a contract that 3 | has an existing wrapper (like NEO) and how to transfer for any arbitrary contract that 4 | implements the NEP-17 standard 5 | """ 6 | 7 | import asyncio 8 | from neo3.api.wrappers import ChainFacade, NeoToken, NEP17Contract 9 | from neo3.api.helpers.signing import sign_with_account 10 | from neo3.network.payloads.verification import Signer 11 | from neo3.core import types 12 | from examples import shared 13 | 14 | 15 | async def example_transfer_neo(node: shared.ExampleNode): 16 | # This example shows how to transfer NEO tokens, a contract that has a dedicated wrapper 17 | wallet = shared.user_wallet 18 | account = wallet.account_default 19 | 20 | # This is your interface for talking to the blockchain 21 | facade = ChainFacade(rpc_host=node.rpc_host) 22 | facade.add_signer( 23 | sign_with_account(account), 24 | Signer(account.script_hash), # default scope is CALLED_BY_ENTRY 25 | ) 26 | 27 | source = account.address 28 | destination = "NUVaphUShQPD82yoXcbvFkedjHX6rUF7QQ" 29 | # Dedicated Neo native contract wrapper 30 | neo = NeoToken() 31 | print("Calling transfer and waiting for receipt...") 32 | print(await facade.invoke(neo.transfer(source, destination, 10))) 33 | 34 | 35 | async def example_transfer_other(node: shared.ExampleNode): 36 | # This example shows how to transfer NEP-17 tokens for a contract that does not 37 | # have a dedicated wrapper like Neo and Gas have. 38 | # Most of the setup is the same as the first example 39 | wallet = shared.user_wallet 40 | account = wallet.account_default 41 | 42 | # This is your interface for talking to the blockchain 43 | facade = ChainFacade(rpc_host=node.rpc_host) 44 | facade.add_signer( 45 | sign_with_account(account), 46 | Signer(account.script_hash), # default scope is te/CALLED_BY_ENTRY 47 | ) 48 | 49 | source = account.address 50 | destination = "NUVaphUShQPD82yoXcbvFkedjHX6rUF7QQ" 51 | 52 | # Use the generic NEP17 class to wrap the token and create a similar interface as before 53 | # The contract hash is that of our sample Nep17 token which is deployed in our neoxpress setup 54 | contract_hash = types.UInt160.from_string( 55 | "0x41ee5befd936c90f15893261abbd681f20ed0429" 56 | ) 57 | token = NEP17Contract(contract_hash) 58 | # Now call it in the same fashion as before with the NEoToken 59 | print("Calling transfer and waiting for receipt...") 60 | print(await facade.invoke(token.transfer(source, destination, 10))) 61 | 62 | 63 | if __name__ == "__main__": 64 | with shared.ExampleNode() as local_node: 65 | asyncio.run(example_transfer_neo(local_node)) 66 | asyncio.run(example_transfer_other(local_node)) 67 | -------------------------------------------------------------------------------- /neo3/core/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from enum import Enum 3 | from collections.abc import Sequence 4 | from neo3.core import serialization, Size, types 5 | 6 | 7 | def get_var_size(value: object) -> int: 8 | """ 9 | Determine the variable size of an object. 10 | 11 | Note: 12 | This function is only used internally for correctly returning object sizes of network payloads to be in line 13 | with C#'s output. 14 | 15 | Args: 16 | value: any object type 17 | 18 | Raises: 19 | TypeError: if a specific Iterable type is not supported. 20 | ValueError: if a specific object type is not supported . 21 | """ 22 | # public static int GetVarSize(this string value) 23 | if isinstance(value, str): 24 | value_size = len(value.encode("utf-8")) 25 | return get_var_size(value_size) + value_size 26 | 27 | # internal static int GetVarSize(int value) 28 | elif isinstance(value, int): 29 | if value < 0xFD: 30 | return Size.uint8 31 | elif value <= 0xFFFF: 32 | return Size.uint8 + Size.uint16 33 | else: 34 | return Size.uint8 + Size.uint32 35 | 36 | # internal static int GetVarSize(this T[] value) 37 | elif isinstance(value, Sequence): 38 | value_length = len(value) 39 | value_size = 0 40 | 41 | if value_length > 0: 42 | if isinstance(value[0], serialization.ISerializable): 43 | value_size = sum(map(lambda t: len(t), value)) 44 | elif isinstance(value[0], Enum): 45 | # Note: currently all Enum's in neo core (C#) are of type Byte. Only porting that part of the code 46 | value_size = value_length * Size.uint8 47 | elif isinstance(value, (bytes, bytearray)): 48 | # experimental replacement for: value_size = value.Length * Marshal.SizeOf(); 49 | # because I don't think we have a reliable 'SizeOf' in python 50 | value_size = value_length * Size.uint8 51 | else: 52 | raise TypeError( 53 | f"Cannot accurately determine size of objects that do not inherit from 'ISerializable', " 54 | f"'Enum' or 'bytes'. Found type: {type(value[0])}" 55 | ) 56 | else: 57 | raise ValueError( 58 | f"[NOT SUPPORTED] Unexpected value type {type(value)} for get_var_size()" 59 | ) 60 | 61 | return get_var_size(value_length) + value_size 62 | 63 | 64 | def to_script_hash(data: bytes) -> types.UInt160: 65 | """ 66 | Create a script hash based on the input data. 67 | 68 | Args: 69 | data: data to hash 70 | """ 71 | intermediate_data = hashlib.sha256(data).digest() 72 | data_ = hashlib.new("ripemd160", intermediate_data).digest() 73 | return types.UInt160(data_) 74 | -------------------------------------------------------------------------------- /examples/nep-11-airdrop.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to send NFTs to multiple accounts in one go (airdrop). 3 | """ 4 | 5 | import asyncio 6 | from neo3.api.wrappers import ChainFacade, GasToken, NEP11NonDivisibleContract 7 | from neo3.api.helpers.signing import sign_with_account 8 | from neo3.network.payloads.verification import Signer 9 | from examples import shared 10 | 11 | 12 | async def example_airdrop(node: shared.ExampleNode): 13 | wallet = shared.user_wallet 14 | account = wallet.account_default 15 | 16 | # This is your interface for talking to the blockchain 17 | facade = ChainFacade(rpc_host=node.rpc_host) 18 | facade.add_signer( 19 | sign_with_account(account), 20 | Signer(account.script_hash), # default scope is CALLED_BY_ENTRY 21 | ) 22 | 23 | # Wrap the NFT contract 24 | ntf = NEP11NonDivisibleContract(shared.nep11_token_hash) 25 | receipt = await facade.test_invoke(ntf.token_ids_owned_by(account.address)) 26 | print(f"Current NFT balance: {len(receipt.result)}") 27 | 28 | # First we have to mint the NFTs to our own wallet 29 | # We do this by sending 10 GAS to the contract. We do this in 2 separate transactions because the NFT is 30 | # in part generated based on the transaction hash 31 | # We increase the retry delay to match our local chain block production time 32 | gas = GasToken() 33 | print("Minting NFT 1..", end="") 34 | receipt = await facade.invoke( 35 | gas.transfer( 36 | source=account.address, 37 | destination=shared.nep11_token_hash, 38 | amount=10 * (8**10), 39 | ) 40 | ) 41 | print(receipt.result) 42 | print("Minting NFT 2..", end="") 43 | receipt = await facade.invoke( 44 | gas.transfer( 45 | source=account.address, 46 | destination=shared.nep11_token_hash, 47 | amount=10 * (8**10), 48 | ) 49 | ) 50 | print(receipt.result) 51 | receipt = await facade.test_invoke(ntf.token_ids_owned_by(account.address)) 52 | token_ids = receipt.result 53 | print(f"New NFT token balance: {len(token_ids)}, ids: {token_ids}") 54 | 55 | # Now let's airdrop the NFTs 56 | destination_addresses = [ 57 | "NWuHQdxabXPdC6vVwJhxjYELDQPqc1d4TG", 58 | "NhVnpBxSRjkScZKHGzsEreYAMS1qRrNdaH", 59 | ] 60 | print("Airdropping 1 NFT to each address and waiting for receipt...", end="") 61 | receipt = await facade.invoke( 62 | ntf.transfer_multi(destination_addresses, token_ids), 63 | ) 64 | print(receipt.result) 65 | 66 | for d in destination_addresses: 67 | nft = await facade.test_invoke(ntf.token_ids_owned_by(d)) 68 | print(f"Address: {d} owns NFT: {nft.result}") 69 | 70 | 71 | if __name__ == "__main__": 72 | with shared.ExampleNode() as local_node: 73 | asyncio.run(example_airdrop(local_node)) 74 | -------------------------------------------------------------------------------- /examples/contract-deploy-update-destroy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to deploy a contract with an `add` function that increases the input with 1. 3 | The contract is then updated on chain with a new version where the `add` function is changed to increase the input by 2. 4 | Finally, the contract is destroyed. 5 | """ 6 | 7 | import asyncio 8 | from neo3.api.wrappers import GenericContract, ChainFacade 9 | from neo3.api.helpers.signing import sign_with_account 10 | from neo3.api.helpers import unwrap 11 | from neo3.contracts import nef, manifest 12 | from neo3.network.payloads.verification import Signer 13 | from examples import shared 14 | 15 | 16 | async def main(node: shared.ExampleNode): 17 | wallet = shared.user_wallet 18 | account = wallet.account_default 19 | 20 | # This is your interface for talking to the blockchain 21 | facade = ChainFacade(rpc_host=node.rpc_host) 22 | facade.add_signer( 23 | sign_with_account(account), 24 | Signer(account.script_hash), # default scope is CALLED_BY_ENTRY 25 | ) 26 | 27 | files_path = f"{shared.shared_dir}/deploy-update-destroy/" 28 | 29 | nef_v1 = nef.NEF.from_file(files_path + "contract_v1.nef") 30 | manifest_v1 = manifest.ContractManifest.from_file( 31 | files_path + "contract_v1.manifest.json" 32 | ) 33 | print("Deploying contract v1...", end="") 34 | receipt = await facade.invoke(GenericContract.deploy(nef_v1, manifest_v1)) 35 | contract_hash = receipt.result 36 | print(f"contract hash = {contract_hash}") 37 | 38 | contract = GenericContract(contract_hash) 39 | print("Calling `add` with input 1, result is: ", end="") 40 | # using test_invoke here because we don't really care about the result being persisted to the chain 41 | receipt = await facade.test_invoke(contract.call_function("add", [1])) 42 | print(unwrap.as_int(receipt.result)) 43 | 44 | print("Updating contract with version 2...", end="") 45 | nef_v2 = nef.NEF.from_file(files_path + "contract_v2.nef") 46 | manifest_v2 = manifest.ContractManifest.from_file( 47 | files_path + "contract_v2.manifest.json" 48 | ) 49 | # updating doesn't give any return value. So if it doens't fail then it means success 50 | await facade.invoke(contract.update(nef=nef_v2, manifest=manifest_v2)) 51 | print("done") 52 | 53 | print("Calling `add` with input 1, result is: ", end="") 54 | # Using test_invoke here because we don't really care about the result being persisted to the chain 55 | receipt = await facade.test_invoke(contract.call_function("add", [1])) 56 | print(unwrap.as_int(receipt.result)) 57 | 58 | print("Destroying contract...", end="") 59 | # destroy also doesn't give any return value. So if it doesn't fail then it means success 60 | await facade.invoke(contract.destroy()) 61 | print("done") 62 | 63 | 64 | if __name__ == "__main__": 65 | with shared.ExampleNode() as local_node: 66 | asyncio.run(main(local_node)) 67 | -------------------------------------------------------------------------------- /neo3/wallet/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | NEO address utilities. 3 | """ 4 | 5 | import base58 6 | from neo3.core import types, cryptography, utils as coreutils 7 | from neo3.wallet.types import NeoAddress 8 | from neo3.contracts import utils as contractutils 9 | 10 | 11 | def script_hash_to_address( 12 | script_hash: types.UInt160, address_version: int = 0x35 13 | ) -> NeoAddress: 14 | """ 15 | Convert the specified script hash to an address. 16 | 17 | Args: 18 | script_hash: script hash to convert. 19 | address_version: network protocol address version. Historically has been fixed to `0x35` for MainNet and TestNet. 20 | Use the `getversion()` RPC method to query for its value. 21 | """ 22 | data = address_version.to_bytes(1, "little") + script_hash.to_array() 23 | return base58.b58encode_check(data).decode("utf-8") 24 | 25 | 26 | def address_to_script_hash(address: NeoAddress) -> types.UInt160: 27 | """ 28 | Convert the specified address to a script hash. 29 | 30 | Args: 31 | address: address to convert. 32 | 33 | Raises: 34 | ValueError: if the length of data (address value in bytes) is not valid. 35 | ValueError: if the account version is not valid. 36 | """ 37 | validate_address(address) 38 | data = base58.b58decode_check(address) 39 | return types.UInt160(data[1:]) 40 | 41 | 42 | def public_key_to_script_hash(public_key: cryptography.ECPoint) -> types.UInt160: 43 | """ 44 | Convert the specified public key to a script hash. 45 | """ 46 | contract_script = contractutils.create_signature_redeemscript(public_key) 47 | return coreutils.to_script_hash(contract_script) 48 | 49 | 50 | def is_valid_address(address: NeoAddress) -> bool: 51 | """ 52 | Test if the provided address is a valid address. 53 | 54 | Args: 55 | address: an address. 56 | """ 57 | try: 58 | validate_address(address) 59 | except ValueError: 60 | return False 61 | return True 62 | 63 | 64 | def validate_address(address: NeoAddress, address_version: int = 0x35) -> None: 65 | """ 66 | Validate a given address. If address is not valid an exception will be raised. 67 | 68 | Args: 69 | address: an address. 70 | address_version: network protocol address version. Historically has been fixed to `0x35` for MainNet and TestNet. 71 | Use the `getversion()` RPC method to query for its value. 72 | 73 | Raises: 74 | ValueError: if the length of data(address value in bytes) is not valid. 75 | ValueError: if the account version is not valid. 76 | """ 77 | data: bytes = base58.b58decode_check(address) 78 | if len(data) != len(types.UInt160.zero()) + 1: 79 | raise ValueError( 80 | f"The address is wrong, because data (address value in bytes) length should be " 81 | f"{len(types.UInt160.zero()) + 1}" 82 | ) 83 | elif data[0] != address_version: 84 | raise ValueError(f"The account version is not {address_version}") 85 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: .github/resources/images/logo.png 2 | :width: 400 px 3 | :align: center 4 | 5 | neo-mamba 6 | ----------- 7 | 8 | .. image:: https://img.shields.io/github/actions/workflow/status/CityOfZion/neo-mamba/validate-pr-commit.yml?branch=master 9 | :target: https://shields.io/ 10 | 11 | .. image:: https://coveralls.io/repos/github/CityOfZion/neo-mamba/badge.svg?branch=master 12 | :target: https://coveralls.io/github/CityOfZion/neo-mamba?branch=master 13 | 14 | .. image:: http://www.mypy-lang.org/static/mypy_badge.svg 15 | :target: http://mypy-lang.org/ 16 | 17 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 18 | :target: https://github.com/psf/black 19 | 20 | .. image:: https://img.shields.io/python/required-version-toml?tomlFilePath=https://raw.githubusercontent.com/CityOfZion/neo-mamba/master/pyproject.toml 21 | :target: https://pypi.org/project/neo-mamba 22 | 23 | .. image:: .github/resources/images/platformbadge.svg 24 | :target: https://github.com/CityOfZion/neo-mamba 25 | 26 | This project is for you if you're looking to use Python to 27 | 28 | * Deploy smart contracts 29 | * Transfer NEP-11 and NEP-17 tokens 30 | * Vote for your favourite consensus node 31 | * Interact with on-chain smart contracts 32 | * Manage wallets 33 | * Build and sign specialized transactions 34 | * and more.. 35 | 36 | This SDK provides building blocks for Python developers to interact with the NEO blockchain without requiring to run a full node. 37 | In order to interact with the chain and obtain information it relies heavily on RPC nodes. You can find a list of public RPC nodes `here `_. 38 | 39 | Please report any issues on `Github `_ or submit ideas how to improve the SDK. 40 | 41 | Also check out our Python smart contract compiler `Boa `_ ! 42 | 43 | Installation and usage 44 | ---------------------- 45 | Installation instructions, how to interact with smart contracts as well as API reference documentation can be found at 46 | https://mamba.coz.io/ 47 | 48 | Developing or contributing 49 | -------------------------- 50 | Install the requirements, modify the code and PR :-) 51 | :: 52 | 53 | pip install -e .[dev] 54 | 55 | For larger changes consider opening an issue first to discuss the change. Below are a few guidelines for contributing 56 | 57 | * The project uses `Black `_ for code formatting. PRs will fail if formatted incorrectly. 58 | You might want to `integrate `_ ``black`` into your 59 | editor or run it manually with ``make black``. 60 | * All public functions/classes must have docstrings. 61 | * All your code must be typed. Test your typing with ``make type``. In rare cases it might be hard/impractical to add typing. 62 | Point it out if that is the case and add a short description why we could do without. 63 | * Add tests that cover the newly added (or changed if applicable) code. Use ``make test`` and ``make coverage``. 64 | -------------------------------------------------------------------------------- /neo3/settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import binascii 3 | from types import SimpleNamespace 4 | from neo3.core import cryptography 5 | 6 | 7 | class IndexableNamespace(SimpleNamespace): 8 | def __len__(self): 9 | return len(self.__dict__) 10 | 11 | def __getitem__(self, key): 12 | return self.__dict__[key] 13 | 14 | def __contains__(self, key): 15 | try: 16 | self.__dict__[key] 17 | return True 18 | except KeyError: 19 | return False 20 | 21 | def get(self, key, default=None): 22 | try: 23 | return self.__dict__[key] 24 | except KeyError: 25 | return default 26 | 27 | 28 | class Settings(IndexableNamespace): 29 | db = None 30 | _cached_standby_committee = None 31 | default_settings = { 32 | "network": { 33 | "magic": 5195086, 34 | "account_version": 53, 35 | "seedlist": [], 36 | "validators_count": 1, 37 | "standby_committee": [ 38 | "02158c4a4810fa2a6a12f7d33d835680429e1a68ae61161c5b3fbc98c7f1f17765" 39 | ], 40 | }, 41 | } 42 | 43 | @classmethod 44 | def from_json(cls, json: dict): 45 | o = cls(**json) 46 | o._convert(o.__dict__, o.__dict__) 47 | return o 48 | 49 | @classmethod 50 | def from_file(cls, path_to_json: str): 51 | with open(path_to_json, "r") as f: 52 | data = json.load(f) 53 | return cls.from_json(data) 54 | 55 | def register(self, json: dict): 56 | self.__dict__.update(json) 57 | self._convert(self.__dict__, self.__dict__) 58 | 59 | def _convert(self, what: dict, where: dict): 60 | # turn all _dictionary what into IndexableNamespaces 61 | to_update = [] 62 | for k, v in what.items(): 63 | if isinstance(v, dict): 64 | to_update.append((k, IndexableNamespace(**v))) 65 | 66 | for k, v in to_update: 67 | if isinstance(where, dict): 68 | where.update({k: v}) 69 | else: 70 | where.__dict__.update({k: v}) 71 | self._convert(where[k].__dict__, where[k].__dict__) 72 | 73 | @property 74 | def standby_committee(self) -> list[cryptography.ECPoint]: 75 | if self._cached_standby_committee is None: 76 | points = [] 77 | for p in self.network.standby_committee: 78 | points.append( 79 | cryptography.ECPoint.deserialize_from_bytes(binascii.unhexlify(p)) 80 | ) 81 | self._cached_standby_committee = points 82 | return self._cached_standby_committee 83 | 84 | @property 85 | def standby_validators(self) -> list[cryptography.ECPoint]: 86 | return self.standby_committee[: self.network.validators_count] 87 | 88 | def reset_settings_to_default(self): 89 | self.__dict__.clear() 90 | self.__dict__.update(self.from_json(self.default_settings).__dict__) 91 | 92 | 93 | settings = Settings.from_json(Settings.default_settings) 94 | -------------------------------------------------------------------------------- /tests/core/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from neo3.core import utils, serialization, types 3 | from neo3.core.serialization import BinaryReader, BinaryWriter 4 | from enum import IntEnum 5 | 6 | 7 | class DummyEnum(IntEnum): 8 | DEFAULT = 0 9 | 10 | 11 | class DummySerializable(serialization.ISerializable): 12 | def serialize(self, writer: BinaryWriter) -> None: 13 | pass 14 | 15 | def deserialize(self, reader: BinaryReader) -> None: 16 | pass 17 | 18 | def __len__(self): 19 | return 2 20 | 21 | 22 | class VarSizeTestCase(unittest.TestCase): 23 | def test_varsize_int(self): 24 | self.assertEqual(1, utils.get_var_size(0xFC)) 25 | self.assertEqual(3, utils.get_var_size(0xFD)) 26 | self.assertEqual(3, utils.get_var_size(0xFFFF)) 27 | self.assertEqual(5, utils.get_var_size(0xFFFF + 1)) 28 | 29 | def test_varsize_string(self): 30 | input = "abc" 31 | self.assertEqual(1 + len(input), utils.get_var_size(input)) 32 | 33 | input = "a" * 0xFC 34 | self.assertEqual(1 + len(input), utils.get_var_size(input)) 35 | 36 | # boundary check 37 | input = "a" * 0xFD 38 | self.assertEqual(3 + len(input), utils.get_var_size(input)) 39 | 40 | input = "a" * 0xFFFF 41 | self.assertEqual(3 + len(input), utils.get_var_size(input)) 42 | 43 | input = "a" * (0xFFFF + 1) 44 | self.assertEqual(5 + len(input), utils.get_var_size(input)) 45 | 46 | def test_iterables(self): 47 | iterable = [] 48 | self.assertEqual(1, utils.get_var_size(iterable)) 49 | 50 | iterable = b"\x01\x02" 51 | self.assertEqual(1 + len(iterable), utils.get_var_size(iterable)) 52 | 53 | iterable = [DummySerializable(), DummySerializable()] 54 | fixed_dummy_size = 2 55 | self.assertEqual(1 + (2 * fixed_dummy_size), utils.get_var_size(iterable)) 56 | 57 | # so far NEO only has byte enums, so the length is fixed to 1 58 | iterable = [DummyEnum.DEFAULT, DummyEnum.DEFAULT] 59 | self.assertEqual(1 + 2, utils.get_var_size(iterable)) 60 | 61 | # test unsupported type in iterable 62 | iterable = [object()] 63 | with self.assertRaises(TypeError) as context: 64 | utils.get_var_size(iterable) 65 | self.assertIn( 66 | "Cannot accurately determine size of objects that do not", 67 | str(context.exception), 68 | ) 69 | 70 | def test_not_supported_objects(self): 71 | with self.assertRaises(ValueError) as context: 72 | utils.get_var_size(object()) 73 | self.assertIn("NOT SUPPORTED", str(context.exception)) 74 | 75 | 76 | class ScriptHashTestCase(unittest.TestCase): 77 | def test_to_script_hash(self): 78 | # from https://github.com/neo-project/neo/blob/8e68c3fabf8b7cad3bd27e0c556cbeda17c2b123/tests/Neo.UnitTests/UT_Helper.cs#L35 79 | data = b"\x42" + b"\x20" * 63 80 | expected = types.UInt160.from_string( 81 | "0x2d3b96ae1bcc5a585e075e3b81920210dec16302" 82 | ) 83 | actual = utils.to_script_hash(data) 84 | self.assertEqual(expected, actual) 85 | -------------------------------------------------------------------------------- /tests/api/helpers/test_stdlib.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from neo3.api.helpers import stdlib 3 | from neo3.core import types 4 | 5 | 6 | class TestStdLibHelpers(unittest.TestCase): 7 | def test_with_map(self): 8 | """ 9 | C# reference code 10 | var m = new Map(new ReferenceCounter()); 11 | var i = new Integer(new BigInteger(1)); 12 | var i2 = new Integer(new BigInteger(2)); 13 | var b = new Neo.VM.Types.Boolean(true); 14 | m[i] = b; 15 | m[i2] = b; 16 | """ 17 | # moved outside of multiline comment because pycharm is broken: https://youtrack.jetbrains.com/issue/PY-43117 18 | # Console.WriteLine($"b'\\x{BitConverter.ToString(BinarySerializer.Serialize(m, 999)).Replace("-", @"\x")}'"); 19 | 20 | data = b"\x48\x02\x21\x01\x01\x20\x01\x21\x01\x02\x20\x01" 21 | 22 | expected = {1: True, 2: True} 23 | results: dict = stdlib.binary_deserialize(data) 24 | 25 | self.assertEqual(expected, results) 26 | 27 | def test_with_array(self): 28 | """ 29 | var a = new Neo.VM.Types.Array(); 30 | var i = new Integer(new BigInteger(1)); 31 | var i2 = new Integer(new BigInteger(2)); 32 | a.Add(i); 33 | a.Add(i2); 34 | """ 35 | # Console.WriteLine($"b'\\x{BitConverter.ToString(BinarySerializer.Serialize(a, 999)).Replace("-", @"\x")}'"); 36 | # moved outside of multiline comment because pycharm is broken: https://youtrack.jetbrains.com/issue/PY-43117 37 | data = b"\x40\x02\x21\x01\x01\x21\x01\x02" 38 | expected = [1, 2] 39 | results: dict = stdlib.binary_deserialize(data) 40 | self.assertEqual(expected, results) 41 | 42 | def test_with_null(self): 43 | data = b"\x00" 44 | expected = None 45 | results: dict = stdlib.binary_deserialize(data) 46 | self.assertEqual(expected, results) 47 | 48 | def test_deserialize_bytestring(self): 49 | data = b"\x28\x02\x01\x02" 50 | expected = b"\x01\x02" 51 | results: dict = stdlib.binary_deserialize(data) 52 | self.assertEqual(expected, results) 53 | 54 | def test_deserialize_buffer(self): 55 | data = b"\x30\x02\x01\x02" 56 | expected = b"\x01\x02" 57 | results: dict = stdlib.binary_deserialize(data) 58 | self.assertEqual(expected, results) 59 | 60 | def test_deserialize_struct(self): 61 | # struct with 2 integers (1,2) 62 | data = b"\x41\x02\x21\x01\x01\x21\x01\x02" 63 | expected = [1, 2] 64 | results: dict = stdlib.binary_deserialize(data) 65 | self.assertEqual(expected, results) 66 | 67 | def test_invalid(self): 68 | data = b"\xff" # invalid stack item type 69 | with self.assertRaises(ValueError) as context: 70 | stdlib.binary_deserialize(data) 71 | self.assertIn("not a valid StackItemType", str(context.exception)) 72 | 73 | with self.assertRaises(ValueError) as context: 74 | stdlib.binary_deserialize(b"") 75 | self.assertEqual("Nothing to deserialize", str(context.exception)) 76 | 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /neo3/core/cryptography/ecc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from neo3.core import serialization 3 | from neo3crypto import ( # type: ignore 4 | ECPoint as _ECPointCpp, 5 | ECCCurve, 6 | ECCException, 7 | sign as ecdsa_sign, 8 | verify as ecdsa_verify, 9 | ) 10 | from typing import Type, Any 11 | import os 12 | import binascii 13 | 14 | 15 | # mypy workaround 16 | type_ECPoint = type(_ECPointCpp) # type: Any 17 | type_Serializable = type(serialization.ISerializable) # type: Any 18 | 19 | 20 | class SerializableECPointMeta(type_ECPoint, type_Serializable): 21 | pass 22 | 23 | 24 | class ECPoint( 25 | _ECPointCpp, serialization.ISerializable, metaclass=SerializableECPointMeta 26 | ): 27 | def __init__(self, *args, **kwargs): 28 | super(ECPoint, self).__init__(*args, **kwargs) 29 | 30 | def __str__(self): 31 | return binascii.hexlify(self.encode_point(compressed=True)).decode("utf8") 32 | 33 | def __bool__(self): 34 | return True 35 | 36 | def __hash__(self): 37 | return hash(self.x + self.y) 38 | 39 | def __deepcopy__(self, memodict={}): 40 | return ECPoint.deserialize_from_bytes(self.to_array(), self.curve, False) 41 | 42 | def is_zero(self): 43 | return self.x == 0 and self.y == 0 44 | 45 | def serialize(self, writer: serialization.BinaryWriter, compress=True) -> None: 46 | if self.is_infinity: 47 | writer.write_bytes(b"\x00") 48 | else: 49 | writer.write_bytes(self.encode_point(compress)) 50 | 51 | def deserialize( 52 | self, reader: serialization.BinaryReader, curve=ECCCurve.SECP256R1 53 | ) -> None: 54 | try: 55 | f0 = reader.read_byte() 56 | except ValueError: 57 | # infinity 58 | self.from_bytes(b"\x00", curve, True) 59 | return 60 | 61 | f1 = int.from_bytes(f0, "little") 62 | if f1 == 0: 63 | # infinity 64 | self.from_bytes(b"\x00", curve, True) 65 | return 66 | elif f1 == 2 or f1 == 3: 67 | data = reader.read_bytes(32) 68 | self.from_bytes(f0 + data, curve, True) 69 | return 70 | else: 71 | raise ValueError(f"Unsupported point encoding: {str(f0)}") 72 | 73 | @classmethod 74 | def deserialize_from_bytes( 75 | cls: Type[serialization.ISerializable_T], 76 | data: bytes | bytearray, 77 | curve: ECCCurve = ECCCurve.SECP256R1, 78 | validate: bool = True, 79 | ) -> serialization.ISerializable_T: 80 | """ 81 | Parse data into an object instance. 82 | 83 | Args: 84 | data: ECPoint in hex escaped bytes format. 85 | curve: the curve type to decompress 86 | validate: validate if the point valid point on the specified curve 87 | 88 | Returns: 89 | a deserialized instance of the class. 90 | """ 91 | return cls(data, curve, validate) # type: ignore 92 | 93 | @classmethod 94 | def _serializable_init(cls): 95 | return cls(b"\x00", ECCCurve.SECP256R1, False) 96 | 97 | 98 | class KeyPair: 99 | def __init__(self, private_key: bytes, curve: ECCCurve = ECCCurve.SECP256R1): 100 | self.private_key = private_key 101 | self.public_key: ECPoint = ECPoint(private_key, curve) 102 | 103 | @classmethod 104 | def generate(cls): 105 | return cls(os.urandom(32)) 106 | -------------------------------------------------------------------------------- /neo3/network/payloads/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Payload containing node information. 3 | """ 4 | 5 | from __future__ import annotations 6 | import datetime 7 | from neo3.core import Size as s, serialization, utils 8 | from neo3.network import capabilities 9 | from neo3 import settings 10 | from collections.abc import Sequence 11 | 12 | 13 | class VersionPayload(serialization.ISerializable): 14 | """ 15 | A payload carrying node handshake data. 16 | """ 17 | 18 | MAX_CAPABILITIES = 32 19 | 20 | def __init__( 21 | self, 22 | nonce: int, 23 | user_agent: str, 24 | capabilities: Sequence[capabilities.NodeCapability], 25 | ): 26 | """ 27 | Create payload. 28 | 29 | Args: 30 | nonce: unique number which identifies the node instance. 31 | user_agent: node user agent description. e.g. "NEO3-PYTHON-V001". Max 1024 bytes. 32 | capabilities: a list of services a node offers. 33 | """ 34 | 35 | #: A network id. Differs for NEO's Mainnet (use 5195086) and Testnet (use 1951352142). 36 | #: 37 | #: Reference nodes will disconnect if the value doesn't match with their local settings. 38 | self.magic = settings.settings.network.magic 39 | #: Protocol version of the node 40 | self.version = 0 41 | #: The UTC time when the node connected 42 | self.timestamp = int(datetime.datetime.utcnow().timestamp()) 43 | #: A unique identifier for the node. 44 | self.nonce = nonce 45 | #: A node client description i.e. "NEO-MAMBA-V001" 46 | self.user_agent = user_agent 47 | #: A list of services a node offers. See :ref:`capabilities ` 48 | self.capabilities = capabilities 49 | 50 | def __len__(self): 51 | """Get the total size in bytes of the object.""" 52 | return ( 53 | s.uint32 54 | + s.uint32 55 | + s.uint32 56 | + s.uint32 57 | + utils.get_var_size(self.user_agent) 58 | + utils.get_var_size(self.capabilities) 59 | ) 60 | 61 | def serialize(self, writer: serialization.BinaryWriter) -> None: 62 | """ 63 | Serialize the object into a binary stream. 64 | 65 | Args: 66 | writer: instance. 67 | """ 68 | writer.write_uint32(self.magic) 69 | writer.write_uint32(self.version) 70 | writer.write_uint32(self.timestamp) 71 | writer.write_uint32(self.nonce) 72 | writer.write_var_string(self.user_agent) 73 | writer.write_serializable_list(self.capabilities) 74 | 75 | def deserialize(self, reader: serialization.BinaryReader) -> None: 76 | """ 77 | Deserialize the object from a binary stream. 78 | 79 | Args: 80 | reader: instance. 81 | """ 82 | self.magic = reader.read_uint32() 83 | self.version = reader.read_uint32() 84 | self.timestamp = reader.read_uint32() 85 | self.nonce = reader.read_uint32() 86 | self.user_agent = reader.read_var_string(max=1024) 87 | 88 | capabilities_cnt = reader.read_var_int(self.MAX_CAPABILITIES) 89 | capabilities_list = [] 90 | for _ in range(capabilities_cnt): 91 | capabilities_list.append( 92 | capabilities.NodeCapability.deserialize_from(reader) 93 | ) 94 | self.capabilities = capabilities_list 95 | 96 | @classmethod 97 | def _serializable_init(cls): 98 | return cls(0, "", []) 99 | -------------------------------------------------------------------------------- /neo3/api/helpers/stdlib.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds helper functions for data that has been serialized using the StdLib native contract 3 | """ 4 | 5 | from neo3 import vm 6 | from typing import NamedTuple, cast, Any 7 | from neo3.core import serialization, types 8 | 9 | 10 | class PlaceHolder(NamedTuple): 11 | type: vm.StackItemType 12 | count: int # type: ignore 13 | 14 | 15 | def binary_deserialize(data: bytes): 16 | """ 17 | Deserialize data that has been serialized using StdLib.serialize() 18 | 19 | This is the equivalent of the StdLib.deserialize() 20 | https://github.com/neo-project/neo/blob/29fab8d3f8f21046a95232b29053c08f9d81f0e3/src/Neo/SmartContract/Native/StdLib.cs#L39 21 | and can be used to deserialize data from smart contract storage that was serialized when stored. 22 | """ 23 | # https://github.com/neo-project/neo-vm/blob/859417ad8ff25c2e4a432b6b5b628149875b3eb9/src/Neo.VM/ExecutionEngineLimits.cs#L39 24 | max_size = 0xFFFF * 2 25 | if len(data) == 0: 26 | raise ValueError("Nothing to deserialize") 27 | 28 | deserialized: list[Any | PlaceHolder] = [] 29 | to_deserialize = 1 30 | with serialization.BinaryReader(data) as reader: 31 | while not to_deserialize == 0: 32 | to_deserialize -= 1 33 | item_type = vm.StackItemType(reader.read_byte()[0]) 34 | if item_type == vm.StackItemType.ANY: 35 | deserialized.append(None) 36 | elif item_type == vm.StackItemType.BOOLEAN: 37 | deserialized.append(reader.read_bool()) 38 | elif item_type == vm.StackItemType.INTEGER: 39 | # https://github.com/neo-project/neo-vm/blob/859417ad8ff25c2e4a432b6b5b628149875b3eb9/src/Neo.VM/Types/Integer.cs#L27 40 | deserialized.append(int(types.BigInteger(reader.read_var_bytes(32)))) 41 | elif item_type in [vm.StackItemType.BYTESTRING, vm.StackItemType.BUFFER]: 42 | deserialized.append(reader.read_var_bytes(len(data))) 43 | elif item_type in [vm.StackItemType.ARRAY, vm.StackItemType.STRUCT]: 44 | count = reader.read_var_int(max_size) 45 | deserialized.append(PlaceHolder(item_type, count)) 46 | to_deserialize += count 47 | elif item_type == vm.StackItemType.MAP: 48 | count = reader.read_var_int(max_size) 49 | deserialized.append(PlaceHolder(item_type, count)) 50 | to_deserialize += count * 2 51 | else: 52 | raise ValueError("unreachable") 53 | 54 | temp: list = [] 55 | while len(deserialized) > 0: 56 | item = deserialized.pop() 57 | if type(item) == PlaceHolder: 58 | item = cast(PlaceHolder, item) 59 | if item.type == vm.StackItemType.ARRAY: 60 | array = [] 61 | for _ in range(0, item.count): 62 | array.append(temp.pop()) 63 | temp.append(array) 64 | elif item.type == vm.StackItemType.STRUCT: 65 | struct = [] 66 | for _ in range(0, item.count): 67 | struct.append(temp.pop()) 68 | temp.append(struct) 69 | elif item.type == vm.StackItemType.MAP: 70 | m = dict() 71 | for _ in range(0, item.count): 72 | k = temp.pop() 73 | v = temp.pop() 74 | m[k] = v 75 | temp.append(m) 76 | else: 77 | temp.append(item) 78 | return temp.pop() 79 | -------------------------------------------------------------------------------- /neo3/network/convenience/nodeweight.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class NodeWeight: 5 | """ 6 | An internal class for tracking and calculating node weight data. 7 | Is used by ``NodeManager`` for selecting the next node candidate. 8 | """ 9 | 10 | SPEED_RECORD_COUNT = 3 11 | SPEED_INIT_VALUE = 100 * 1024 ^ 2 # Start with a big speed of 100 MB/s 12 | 13 | REQUEST_TIME_RECORD_COUNT = 3 14 | 15 | def __init__(self, node_id: int): 16 | """ 17 | Args: 18 | node_id: the `NeoNode.id` of the node this weight belongs to. 19 | """ 20 | 21 | #: The :attr:`~neo3.network.node.NeoNode.id` of the node this weight belongs to. 22 | self.id: int = node_id 23 | 24 | #: in number of bytes per second 25 | self.speed: list[float] = [self.SPEED_INIT_VALUE] * self.SPEED_RECORD_COUNT 26 | self.timeout_count = 0 27 | self.error_response_count = 0 28 | now = datetime.utcnow().timestamp() * 1000 # milliseconds 29 | self.request_time: list[float] = [now] * self.REQUEST_TIME_RECORD_COUNT 30 | 31 | def append_new_speed(self, speed: float) -> None: 32 | """ 33 | Add a speed value to the collection from which the average speed is calculated. 34 | 35 | The average is calculated based on 3 values. The last value is popped from the collection. 36 | 37 | Args: 38 | speed: in number of bytes per second. 39 | """ 40 | # remove oldest 41 | self.speed.pop(-1) 42 | # add new 43 | self.speed.insert(0, speed) 44 | 45 | def append_new_request_time(self) -> None: 46 | """ 47 | Add a request time value to the collection from which the average request time is calculated. 48 | 49 | The average is calculated based on 3 values. The last value is popped from the collection. 50 | """ 51 | 52 | self.request_time.pop(-1) 53 | 54 | now = datetime.utcnow().timestamp() * 1000 # milliseconds 55 | self.request_time.insert(0, now) 56 | 57 | def _avg_speed(self) -> float: 58 | return sum(self.speed) / self.SPEED_RECORD_COUNT 59 | 60 | def _avg_request_time(self) -> float: 61 | avg_request_time: float = 0 62 | now = datetime.utcnow().timestamp() * 1000 # milliseconds 63 | 64 | for t in self.request_time: 65 | avg_request_time += now - t 66 | 67 | avg_request_time = avg_request_time / self.REQUEST_TIME_RECORD_COUNT 68 | return avg_request_time 69 | 70 | def weight(self) -> float: 71 | """ 72 | A score indicating the quality of the node. Higher is better. 73 | 74 | Nodes with the highest speed and the longest time between querying for data have the highest weight 75 | and will be accessed first by the syncmanager unless their error/timeout count is higher. This distributes the 76 | load across nodes. 77 | """ 78 | weight = self._avg_speed() + self._avg_request_time() 79 | 80 | # punish errors and timeouts harder than slower speeds and more recent access 81 | if self.error_response_count: 82 | weight /= ( 83 | self.error_response_count + 1 84 | ) # make sure we at least always divide by 2 85 | 86 | if self.timeout_count: 87 | weight /= self.timeout_count + 1 88 | return weight 89 | 90 | def __lt__(self, other): 91 | return self.weight() < other.weight() 92 | 93 | def __repr__(self): 94 | return ( 95 | f"<{self.__class__.__name__} at {hex(id(self))}> weight:{self.weight():.2f}" 96 | ) 97 | -------------------------------------------------------------------------------- /tests/contracts/test_nef.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import binascii 3 | from neo3.contracts import callflags, nef 4 | from neo3.core import types 5 | from copy import deepcopy 6 | 7 | 8 | class NEFTestCase(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls) -> None: 11 | """ 12 | var nef = new NefFile 13 | { 14 | Compiler = "test-compiler 0.1", 15 | Source = "source_link", 16 | Script = new byte[] {(byte) OpCode.RET}, 17 | Tokens = new MethodToken[] 18 | { 19 | new MethodToken() 20 | { 21 | Hash = UInt160.Zero, 22 | Method = "test_method", 23 | ParametersCount = 0, 24 | HasReturnValue = true, 25 | CallFlags = CallFlags.None 26 | } 27 | } 28 | }; 29 | nef.CheckSum = NefFile.ComputeChecksum(nef); 30 | Console.WriteLine(nef.ToArray().ToHexString()); 31 | Console.WriteLine(nef.Size); 32 | """ 33 | cls.expected = binascii.unhexlify( 34 | b"4e454633746573742d636f6d70696c657220302e3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b736f757263655f6c696e6b000100000000000000000000000000000000000000000b746573745f6d6574686f64000001000000014072adaf87" 35 | ) 36 | cls.expected_length = 126 37 | compiler = "test-compiler 0.1" 38 | source = "source_link" 39 | ret = b"\x40" # vm.OpCode.RET 40 | tokens = [ 41 | nef.MethodToken( 42 | types.UInt160.zero(), "test_method", 0, True, callflags.CallFlags.NONE 43 | ) 44 | ] 45 | cls.nef = nef.NEF( 46 | compiler_name=compiler, script=ret, tokens=tokens, source=source 47 | ) 48 | 49 | def test_serialization(self): 50 | self.assertEqual(self.expected, self.nef.to_array()) 51 | 52 | def test_deserialization(self): 53 | nef_ = nef.NEF.deserialize_from_bytes(self.expected) 54 | self.assertEqual(self.nef.magic, nef_.magic) 55 | self.assertEqual(self.nef.source, nef_.source) 56 | self.assertEqual(self.nef.compiler, nef_.compiler) 57 | self.assertEqual(self.nef.script, nef_.script) 58 | self.assertEqual(self.nef.checksum, nef_.checksum) 59 | 60 | def test_deserialization_error(self): 61 | nef1 = deepcopy(self.nef) 62 | nef1.magic = 0xDEADBEEF 63 | with self.assertRaises(ValueError) as context: 64 | nef.NEF.deserialize_from_bytes(nef1.to_array()) 65 | self.assertEqual( 66 | "Deserialization error - Incorrect magic", str(context.exception) 67 | ) 68 | 69 | nef_ = deepcopy(self.nef) 70 | nef_.script = b"" 71 | with self.assertRaises(ValueError) as context: 72 | nef.NEF.deserialize_from_bytes(nef_.to_array()) 73 | self.assertEqual( 74 | "Deserialization error - Script can't be empty", str(context.exception) 75 | ) 76 | 77 | # test with wrong checksum 78 | nef_ = deepcopy(self.nef) 79 | nef_._checksum = 0xDEADBEEF 80 | with self.assertRaises(ValueError) as context: 81 | nef.NEF.deserialize_from_bytes(nef_.to_array()) 82 | self.assertEqual( 83 | "Deserialization error - Invalid checksum", str(context.exception) 84 | ) 85 | 86 | def test_len(self): 87 | self.assertEqual(self.expected_length, len(self.nef)) 88 | 89 | def test_eq(self): 90 | compiler = "test-compiler 0.1" 91 | ret = b"\x40" # vm.OpCode.RET 92 | nef1 = nef.NEF(compiler_name=compiler, script=ret) 93 | nef2 = nef.NEF(compiler_name=compiler, script=ret) 94 | self.assertFalse(nef1 == object()) 95 | self.assertTrue(nef1 == nef2) 96 | -------------------------------------------------------------------------------- /neo3/core/cryptography/merkletree.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from neo3.core import types 3 | from typing import Optional 4 | from collections.abc import Sequence 5 | 6 | 7 | class _MerkleTreeNode: 8 | def __init__(self, hash: Optional[types.UInt256] = None): 9 | self.hash = hash if hash else types.UInt256.zero() # type: types.UInt256 10 | self.parent = None # type: Optional[_MerkleTreeNode] 11 | self.left_child = None # type: Optional[_MerkleTreeNode] 12 | self.right_child = None # type: Optional[_MerkleTreeNode] 13 | 14 | def is_leaf(self) -> bool: 15 | """ 16 | Return `True` if the node is a leaf. 17 | """ 18 | return self.left_child is None and self.right_child is None 19 | 20 | def is_root(self) -> bool: 21 | """ 22 | Return `True` if the node is the root node. 23 | """ 24 | return self.parent is None 25 | 26 | 27 | class MerkleTree: 28 | def __init__(self, hashes: Sequence[types.UInt256]): 29 | """ 30 | 31 | Args: 32 | hashes: the list of hashes to build the tree from. 33 | 34 | Raises: 35 | ValueError: if the `hashes` list is empty. 36 | """ 37 | if len(hashes) == 0: 38 | self.has_root = False 39 | return 40 | else: 41 | self.has_root = True 42 | 43 | self.root = self._build(leaves=[_MerkleTreeNode(h) for h in hashes]) 44 | _depth = 1 45 | i = self.root 46 | while i.left_child is not None: 47 | _depth += 1 48 | i = i.left_child 49 | self.depth = _depth 50 | 51 | def to_hash_array(self) -> list[types.UInt256]: 52 | """ 53 | Create a list of hashes the Merkle tree is build up from. 54 | 55 | Note: does not include the Merkle root hash. 56 | """ 57 | hashes: list[types.UInt256] = [] 58 | if self.has_root: 59 | MerkleTree._depth_first_search(self.root, hashes) 60 | return hashes 61 | 62 | @staticmethod 63 | def _depth_first_search(node: _MerkleTreeNode, hashes: list[types.UInt256]) -> None: 64 | # if left is None then Right is also always None, but it helps the static type checker understand this 65 | # otherwise it thinks it might go to the else branch and the second call is then invalid 66 | if node.left_child is None or node.right_child is None: 67 | hashes.append(node.hash) 68 | else: 69 | MerkleTree._depth_first_search(node.left_child, hashes) 70 | MerkleTree._depth_first_search(node.right_child, hashes) 71 | 72 | @staticmethod 73 | def _build(leaves: Sequence[_MerkleTreeNode]) -> _MerkleTreeNode: 74 | if len(leaves) == 0: 75 | raise ValueError("Leaves must have length") 76 | if len(leaves) == 1: 77 | return leaves[0] 78 | 79 | num_parents = int((len(leaves) + 1) / 2) 80 | parents = [_MerkleTreeNode() for i in range(0, num_parents)] 81 | 82 | for i in range(0, num_parents): 83 | node = parents[i] 84 | node.left_child = leaves[i * 2] 85 | leaves[i * 2].parent = node 86 | if i * 2 + 1 == len(leaves): 87 | node.right_child = node.left_child 88 | else: 89 | node.right_child = leaves[i * 2 + 1] 90 | leaves[i * 2 + 1].parent = node 91 | 92 | data = node.left_child.hash.to_array() + node.right_child.hash.to_array() 93 | hashed_data = hashlib.sha256(hashlib.sha256(data).digest()).digest() 94 | node.hash = types.UInt256(data=hashed_data) 95 | 96 | return MerkleTree._build(parents) 97 | 98 | @staticmethod 99 | def compute_root(hashes: Sequence[types.UInt256]) -> types.UInt256: 100 | """ 101 | Compute the Merkle root hash from a list of hashes. 102 | 103 | Args: 104 | hashes: 105 | 106 | Raises: 107 | ValueError: if the `hashes` list is empty. 108 | """ 109 | if len(hashes) == 0: 110 | return types.UInt256.zero() 111 | if len(hashes) == 1: 112 | return hashes[0] 113 | tree = MerkleTree(hashes) 114 | return tree.root.hash 115 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-commit.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | push: 7 | branches: 8 | - master 9 | workflow_dispatch: 10 | 11 | jobs: 12 | setup: 13 | name: Setup ${{ matrix.os }} Python ${{ matrix.python-version }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-24.04, windows-latest] 18 | python-version: ["3.13", "3.14"] 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | path: 'mamba' 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5.2.0 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | check-latest: true 29 | - if: matrix.os == 'ubuntu-24.04' 30 | name: Setup install dependencies 31 | run: | 32 | cd mamba 33 | python -m venv venv 34 | source venv/bin/activate 35 | pip install -e .[dev,docs] 36 | - if: matrix.os == 'windows-latest' 37 | name: Setup install dependencies 38 | run: | 39 | cd mamba 40 | python -m venv venv 41 | venv\Scripts\activate 42 | pip install -e .[dev,docs] 43 | - name: prep for persist 44 | run: tar -czf mamba.tar.gz mamba 45 | - name: persist 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: setup-artifact-${{ matrix.os }}-py${{ matrix.python-version }} 49 | path: mamba.tar.gz 50 | retention-days: 1 51 | linting: 52 | runs-on: ubuntu-24.04 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: psf/black@stable 56 | with: 57 | options: '--check --target-version py313' 58 | version: '25.12.0' 59 | src: "neo3/ examples/ tests/" 60 | type-checking: 61 | needs: setup 62 | runs-on: ubuntu-24.04 63 | steps: 64 | - name: restore artifact 65 | uses: actions/download-artifact@v4 66 | with: 67 | name: setup-artifact-ubuntu-24.04-py3.13 68 | - name: Set up Python 3.13 69 | uses: actions/setup-python@v5.6.0 70 | with: 71 | python-version: "3.13" 72 | check-latest: true 73 | - name: extract & type check 74 | run: | 75 | tar -xf mamba.tar.gz 76 | cd mamba 77 | source venv/bin/activate 78 | make type 79 | unit-tests: 80 | name: Unit tests ${{ matrix.os }} Python ${{ matrix.python-version }} 81 | needs: setup 82 | runs-on: ${{ matrix.os }} 83 | strategy: 84 | matrix: 85 | os: [ ubuntu-24.04, windows-latest ] 86 | python-version: ["3.13", "3.14"] 87 | steps: 88 | - name: restore artifact 89 | uses: actions/download-artifact@v4 90 | with: 91 | name: setup-artifact-${{ matrix.os }}-py${{ matrix.python-version }} 92 | - name: Set up Python ${{ matrix.python-version }} 93 | uses: actions/setup-python@v5.2.0 94 | with: 95 | python-version: '${{ matrix.python-version }}' 96 | check-latest: true 97 | - if: success() && matrix.os == 'ubuntu-24.04' 98 | name: extract and test 99 | run: | 100 | tar -xf mamba.tar.gz 101 | cd mamba 102 | source venv/bin/activate 103 | make test 104 | - if: success() && matrix.os == 'windows-latest' 105 | name: extract and test 106 | run: | 107 | tar -xf mamba.tar.gz 108 | cd mamba 109 | venv\Scripts\activate 110 | make test 111 | coverage: 112 | needs: setup 113 | runs-on: ubuntu-24.04 114 | env: 115 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 116 | steps: 117 | - name: restore artifact 118 | uses: actions/download-artifact@v4 119 | with: 120 | name: setup-artifact-ubuntu-24.04-py3.13 121 | - name: Set up Python 3.13 122 | uses: actions/setup-python@v5.6.0 123 | with: 124 | python-version: "3.13" 125 | check-latest: true 126 | - name: check coverage 127 | run: | 128 | tar -xf mamba.tar.gz 129 | cd mamba 130 | source venv/bin/activate 131 | coverage run -m unittest discover -v -s tests/ 132 | pip install coveralls 133 | coveralls --service=github 134 | -------------------------------------------------------------------------------- /tests/api/test_wrappers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Optional, Any 3 | from aioresponses import aioresponses 4 | from neo3.core import types 5 | from neo3.api.wrappers import _check_address_and_convert, ChainFacade 6 | 7 | JSON = Any 8 | 9 | 10 | class WrapperUtilsTest(unittest.TestCase): 11 | def test_check_address_and_convert(self): 12 | hash_in = types.UInt160.from_string( 13 | "0x7e9237a93f64407141a5b86c760200c66c81e2ec" 14 | ) 15 | self.assertIsInstance(_check_address_and_convert(hash_in), types.UInt160) 16 | 17 | with self.assertRaises(ValueError) as context: 18 | _check_address_and_convert(object()) 19 | self.assertEqual( 20 | "Input is of type expected UInt160 or NeoAddress(str)", 21 | str(context.exception), 22 | ) 23 | 24 | invalid_address = "NgNJsBfhcoJSm6MVMpMeGLqEK5mSQXuJTt" 25 | with self.assertRaises(ValueError) as context: 26 | _check_address_and_convert(invalid_address) 27 | self.assertEqual("Invalid checksum", str(context.exception)) 28 | 29 | valid_address = "NgNJsBfhcoJSm6MVMpMeGLqEK5mSQXuJTq" 30 | self.assertIsInstance(_check_address_and_convert(valid_address), types.UInt160) 31 | 32 | 33 | class TestChainFacade(unittest.IsolatedAsyncioTestCase): 34 | async def asyncSetUp(self) -> None: 35 | # CAREFULL THIS PATCHES ALL aiohttp CALLS! 36 | self.helper = aioresponses() 37 | self.helper.start() 38 | 39 | async def asyncTearDown(self) -> None: 40 | self.helper.stop() 41 | 42 | def mock_response( 43 | self, payload: Optional[JSON] = None, exc: Optional[Exception] = None 44 | ): 45 | """ 46 | Either payload or exc should be provided 47 | """ 48 | if payload is not None and exc is not None: 49 | raise ValueError("Arguments are mutual exclusive") 50 | 51 | if payload is not None: 52 | json = {"jsonrpc": "2.0", "id": 1, "result": payload} 53 | self.helper.post("localhost", payload=json) 54 | else: 55 | self.helper.post("localhost", exception=exc) 56 | 57 | async def test_receipt_retry_delay_and_timeout(self): 58 | user_agent = "/Neo:3.0.3/" 59 | get_version_captured = { 60 | "tcpport": 10333, 61 | "wsport": 10334, 62 | "nonce": 1930156121, 63 | "useragent": user_agent, 64 | "rpc": {"maxiteratorresultitems": 100, "sessionenabled": True}, 65 | "protocol": { 66 | "addressversion": 53, 67 | "network": 860833102, 68 | "validatorscount": 7, 69 | "msperblock": 15000, 70 | "maxtraceableblocks": 2102400, 71 | "maxvaliduntilblockincrement": 5760, 72 | "maxtransactionsperblock": 512, 73 | "memorypoolmaxtransactions": 50000, 74 | "initialgasdistribution": 5200000000000000, 75 | "hardforks": [ 76 | {"name": "Aspidochelone", "blockheight": 1730000}, 77 | {"name": "Basilisk", "blockheight": 4120000}, 78 | {"name": "Cockatrice", "blockheight": 5450000}, 79 | {"name": "Domovoi", "blockheight": 5570000}, 80 | ], 81 | }, 82 | } 83 | self.mock_response(get_version_captured) 84 | facade = ChainFacade("localhost") 85 | delay, timeout = await facade._get_receipt_time_values() 86 | self.assertEqual(3.0, delay) 87 | self.assertEqual(33.0, timeout) 88 | 89 | self.mock_response(get_version_captured) 90 | facade = ChainFacade("localhost", receipt_timeout=1) 91 | delay, timeout = await facade._get_receipt_time_values() 92 | self.assertEqual(3.0, delay) 93 | self.assertEqual(1.0, timeout) 94 | 95 | self.mock_response(get_version_captured) 96 | facade = ChainFacade("localhost", receipt_retry_delay=5) 97 | delay, timeout = await facade._get_receipt_time_values() 98 | self.assertEqual(5.0, delay) 99 | self.assertEqual(35.0, timeout) 100 | 101 | self.mock_response(get_version_captured) 102 | facade = ChainFacade("localhost", receipt_retry_delay=5, receipt_timeout=1) 103 | delay, timeout = await facade._get_receipt_time_values() 104 | self.assertEqual(5.0, delay) 105 | self.assertEqual(1.0, timeout) 106 | -------------------------------------------------------------------------------- /tests/network/test_ipfilter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from neo3.network.ipfilter import IPFilter 3 | 4 | 5 | class IPFilteringTestCase(unittest.TestCase): 6 | def test_nobody_allowed(self): 7 | filter = IPFilter() 8 | filter.load_config({"blacklist": ["0.0.0.0/0"], "whitelist": []}) 9 | 10 | self.assertFalse(filter.is_allowed("127.0.0.1")) 11 | self.assertFalse(filter.is_allowed("10.10.10.10")) 12 | 13 | def test_nobody_allowed_except_one(self): 14 | filter = IPFilter() 15 | filter.load_config({"blacklist": ["0.0.0.0/0"], "whitelist": ["10.10.10.10"]}) 16 | 17 | self.assertFalse(filter.is_allowed("127.0.0.1")) 18 | self.assertFalse(filter.is_allowed("10.10.10.11")) 19 | self.assertTrue(filter.is_allowed("10.10.10.10")) 20 | 21 | def test_everybody_allowed(self): 22 | filter = IPFilter() 23 | filter.load_config({"blacklist": [], "whitelist": []}) 24 | 25 | self.assertTrue(filter.is_allowed("127.0.0.1")) 26 | self.assertTrue(filter.is_allowed("10.10.10.11")) 27 | self.assertTrue(filter.is_allowed("10.10.10.10")) 28 | 29 | filter.load_config({"blacklist": [], "whitelist": ["0.0.0.0/0"]}) 30 | 31 | self.assertTrue(filter.is_allowed("127.0.0.1")) 32 | self.assertTrue(filter.is_allowed("10.10.10.11")) 33 | self.assertTrue(filter.is_allowed("10.10.10.10")) 34 | 35 | filter.load_config({"blacklist": ["0.0.0.0/0"], "whitelist": ["0.0.0.0/0"]}) 36 | 37 | self.assertTrue(filter.is_allowed("127.0.0.1")) 38 | self.assertTrue(filter.is_allowed("10.10.10.11")) 39 | self.assertTrue(filter.is_allowed("10.10.10.10")) 40 | 41 | def test_everybody_allowed_except_one(self): 42 | filter = IPFilter() 43 | filter.load_config({"blacklist": ["127.0.0.1"], "whitelist": []}) 44 | 45 | self.assertFalse(filter.is_allowed("127.0.0.1")) 46 | self.assertTrue(filter.is_allowed("10.10.10.11")) 47 | self.assertTrue(filter.is_allowed("10.10.10.10")) 48 | 49 | def test_disallow_ip_range(self): 50 | filter = IPFilter() 51 | filter.load_config({"blacklist": ["127.0.0.0/24"], "whitelist": []}) 52 | 53 | self.assertFalse(filter.is_allowed("127.0.0.0")) 54 | self.assertFalse(filter.is_allowed("127.0.0.1")) 55 | self.assertFalse(filter.is_allowed("127.0.0.100")) 56 | self.assertFalse(filter.is_allowed("127.0.0.255")) 57 | self.assertTrue(filter.is_allowed("10.10.10.11")) 58 | self.assertTrue(filter.is_allowed("10.10.10.10")) 59 | 60 | def test_updating_blacklist(self): 61 | filter = IPFilter() 62 | filter.load_config({"blacklist": [], "whitelist": []}) 63 | 64 | self.assertTrue(filter.is_allowed("127.0.0.1")) 65 | 66 | filter.blacklist_add("127.0.0.0/24") 67 | self.assertFalse(filter.is_allowed("127.0.0.1")) 68 | # should have no effect, only exact matches 69 | filter.blacklist_remove("127.0.0.1") 70 | self.assertFalse(filter.is_allowed("127.0.0.1")) 71 | 72 | filter.blacklist_remove("127.0.0.0/24") 73 | self.assertTrue(filter.is_allowed("127.0.0.1")) 74 | 75 | def test_updating_whitelist(self): 76 | filter = IPFilter() 77 | filter.load_config({"blacklist": ["0.0.0.0/0"], "whitelist": []}) 78 | 79 | self.assertFalse(filter.is_allowed("127.0.0.1")) 80 | 81 | filter.whitelist_add("127.0.0.0/24") 82 | self.assertTrue(filter.is_allowed("127.0.0.1")) 83 | 84 | filter.whitelist_remove("127.0.0.1") 85 | # should have no effect, only exact matches 86 | self.assertTrue(filter.is_allowed("127.0.0.1")) 87 | 88 | filter.whitelist_remove("127.0.0.0/24") 89 | self.assertFalse(filter.is_allowed("127.0.0.1")) 90 | 91 | def test_invalid_config_loading(self): 92 | filter = IPFilter() 93 | # mandatory keys not present 94 | with self.assertRaises(ValueError) as ctx: 95 | filter.load_config({"blacklist": []}) 96 | self.assertEqual("whitelist key not found", str(ctx.exception)) 97 | 98 | # mandatory key blacklist not present 99 | with self.assertRaises(ValueError) as ctx: 100 | filter.load_config({"whitelist": []}) 101 | self.assertEqual("blacklist key not found", str(ctx.exception)) 102 | 103 | def test_config_reset(self): 104 | filter = IPFilter() 105 | filter.blacklist_add("127.0.0.1") 106 | self.assertEqual(1, len(filter._config["blacklist"])) 107 | filter.reset() 108 | self.assertEqual(filter.default_config, filter._config) 109 | -------------------------------------------------------------------------------- /examples/shared/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import asyncio 3 | from neo3.wallet.wallet import Wallet, account 4 | from neo3.core import types 5 | from neo3.api.wrappers import GenericContract, NeoToken, GasToken 6 | from neo3.api.helpers.signing import sign_with_account, sign_with_multisig_account 7 | from neo3.contracts import nef, manifest 8 | from neo3.network.payloads.verification import Signer 9 | from boaconstructor import NeoGoNode 10 | 11 | shared_dir = pathlib.Path("shared").resolve(strict=True) 12 | 13 | user_wallet = Wallet.from_file(f"{shared_dir}/user-wallet.json", passwords=["123"]) 14 | coz_wallet = Wallet.from_file(f"{shared_dir}/coz-wallet.json", passwords=["123"]) 15 | 16 | coz_token_hash = types.UInt160.from_string("0x41ee5befd936c90f15893261abbd681f20ed0429") 17 | # corresponds to the nep-11 token in the `/nep11-token/` dir and deployed with the `coz` account 18 | nep11_token_hash = types.UInt160.from_string( 19 | "0x35de2913c480c19a7667da1cc3b2fe3e4c9de761" 20 | ) 21 | 22 | 23 | class ExampleNode(NeoGoNode): 24 | @property 25 | def rpc_host(self) -> str: 26 | return self.facade.rpc_host 27 | 28 | def __enter__(self): 29 | self.start() 30 | loop = asyncio.get_event_loop() 31 | loop.run_until_complete(self._setup_for_test()) 32 | return self 33 | 34 | def __exit__(self, exc_type, exc_val, exc_tb): 35 | self.stop() 36 | 37 | async def _setup_for_test(self): 38 | sign_pair = ( 39 | sign_with_multisig_account(self.account_committee), 40 | Signer(self.account_committee.script_hash), 41 | ) 42 | neo = NeoToken() 43 | gas = GasToken() 44 | coz_account = coz_wallet.account_default 45 | user_account = user_wallet.account_default 46 | await self.facade.invoke( 47 | neo.transfer_friendly( 48 | self.account_committee.script_hash, coz_account.script_hash, 50000000, 0 49 | ), 50 | signers=[sign_pair], 51 | ) 52 | await self.facade.invoke( 53 | gas.transfer_friendly( 54 | self.account_committee.script_hash, coz_account.script_hash, 26000000, 8 55 | ), 56 | signers=[sign_pair], 57 | ) 58 | await self.facade.invoke( 59 | neo.transfer_friendly( 60 | self.account_committee.script_hash, 61 | user_account.script_hash, 62 | 50000000, 63 | 0, 64 | ), 65 | signers=[sign_pair], 66 | ) 67 | await self.facade.invoke( 68 | gas.transfer_friendly( 69 | self.account_committee.script_hash, 70 | user_account.script_hash, 71 | 26000000, 72 | 8, 73 | ), 74 | signers=[sign_pair], 75 | ) 76 | 77 | await self._deploy_contract( 78 | f"{shared_dir}/nep17-token/nep17token.nef", coz_account 79 | ) 80 | await self._deploy_contract( 81 | f"{shared_dir}/nep11-token/nep11-token.nef", coz_account 82 | ) 83 | 84 | sign_with_user = ( 85 | sign_with_account(user_account), 86 | Signer(user_account.script_hash), 87 | ) 88 | 89 | await self.facade.invoke( 90 | neo.transfer_friendly( 91 | user_account.script_hash, "NgJ6aLeAi3wJAQ3JbgcWsHGwUT76bvcWMM", 100, 0 92 | ), 93 | signers=[sign_with_user], 94 | ) 95 | 96 | sign_with_coz = ( 97 | sign_with_account(coz_account), 98 | Signer(coz_account.script_hash), 99 | ) 100 | await self.facade.invoke( 101 | neo.candidate_register(coz_account.public_key), signers=[sign_with_coz] 102 | ) 103 | 104 | async def _deploy_contract( 105 | self, nef_path: str, signing_account: account.Account 106 | ) -> types.UInt160: 107 | _nef = nef.NEF.from_file(nef_path) 108 | manifest_path = nef_path.removesuffix(".nef") + ".manifest.json" 109 | _manifest = manifest.ContractManifest.from_file(manifest_path) 110 | 111 | if signing_account.is_multisig: 112 | sign_pair = ( 113 | sign_with_multisig_account(signing_account), 114 | Signer(signing_account.script_hash), 115 | ) 116 | else: 117 | sign_pair = ( 118 | sign_with_account(signing_account), 119 | Signer(signing_account.script_hash), 120 | ) 121 | 122 | receipt = await self.facade.invoke( 123 | GenericContract.deploy(_nef, _manifest), signers=[sign_pair] 124 | ) 125 | return receipt.result 126 | -------------------------------------------------------------------------------- /tests/network/test_capabilities.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import binascii 3 | from neo3.network import capabilities 4 | from neo3.core import serialization 5 | 6 | 7 | class FullNodeCapabilitiesTestCase(unittest.TestCase): 8 | @classmethod 9 | def setUpClass(cls) -> None: 10 | """ 11 | FullNodeCapability capability = new FullNodeCapability(123); 12 | 13 | Console.WriteLine($"{capability.Size}"); 14 | Console.WriteLine($"{BitConverter.ToString(capability.ToArray()).Replace("-", "")}"); 15 | """ 16 | cls.capability = capabilities.FullNodeCapability(start_height=123) 17 | 18 | def test_len(self): 19 | # captured from C#, see setUpClass() for the capture code 20 | expected_len = 5 21 | self.assertEqual(expected_len, len(self.capability)) 22 | 23 | def test_serialization(self): 24 | # captured from C#, see setUpClass() for the capture code 25 | expected_data = binascii.unhexlify(b"107B000000") 26 | self.assertEqual(expected_data, self.capability.to_array()) 27 | 28 | def test_deserialization(self): 29 | # if the serialization() test for this class passes, we can use that as a reference to test deserialization against 30 | deserialized_capability = ( 31 | capabilities.FullNodeCapability.deserialize_from_bytes( 32 | self.capability.to_array() 33 | ) 34 | ) 35 | self.assertEqual( 36 | self.capability.start_height, deserialized_capability.start_height 37 | ) 38 | 39 | 40 | class ServerNodeCapabilitiesTestCase(unittest.TestCase): 41 | @classmethod 42 | def setUpClass(cls) -> None: 43 | """ 44 | ServerCapability capability = new ServerCapability(NodeCapabilityType.TcpServer, 10333); 45 | 46 | Console.WriteLine($"{capability.Size}"); 47 | Console.WriteLine($"{BitConverter.ToString(capability.ToArray()).Replace("-", "")}"); 48 | """ 49 | cls.capability = capabilities.ServerCapability( 50 | n_type=capabilities.NodeCapabilityType.TCPSERVER, port=10333 51 | ) 52 | 53 | def test_len(self): 54 | # captured from C#, see setUpClass() for the capture code 55 | expected_len = 3 56 | self.assertEqual(expected_len, len(self.capability)) 57 | 58 | def test_serialization(self): 59 | # captured from C#, see setUpClass() for the capture code 60 | expected_data = binascii.unhexlify(b"015D28") 61 | self.assertEqual(expected_data, self.capability.to_array()) 62 | 63 | def test_deserialization(self): 64 | # if the serialization() test for this class passes, we can use that as a reference to test deserialization against 65 | deserialized_capability = capabilities.ServerCapability.deserialize_from_bytes( 66 | self.capability.to_array() 67 | ) 68 | self.assertEqual( 69 | capabilities.NodeCapabilityType.TCPSERVER, deserialized_capability.type 70 | ) 71 | self.assertEqual(self.capability.type, deserialized_capability.type) 72 | self.assertEqual(self.capability.port, deserialized_capability.port) 73 | 74 | def test_creation_with_invalid_type(self): 75 | with self.assertRaises(TypeError) as context: 76 | capabilities.ServerCapability(n_type=999, port=123) 77 | self.assertIn("999 not one of: TCPSERVER WSSERVER", str(context.exception)) 78 | 79 | 80 | class BaseCapabilitiesTestCase(unittest.TestCase): 81 | def test_deserialize_from(self): 82 | server = capabilities.ServerCapability( 83 | n_type=capabilities.NodeCapabilityType.TCPSERVER, port=10333 84 | ) 85 | fullnode = capabilities.FullNodeCapability(start_height=123) 86 | 87 | with serialization.BinaryReader(server.to_array()) as br: 88 | capability = capabilities.NodeCapability.deserialize_from(br) 89 | self.assertIsInstance(capability, capabilities.ServerCapability) 90 | 91 | with serialization.BinaryReader(fullnode.to_array()) as br: 92 | capability = capabilities.NodeCapability.deserialize_from(br) 93 | self.assertIsInstance(capability, capabilities.FullNodeCapability) 94 | 95 | # test equality 96 | # ServerCapability capability = new ServerCapability(NodeCapabilityType.TcpServer, 10333); 97 | # ServerCapability capability2 = new ServerCapability(NodeCapabilityType.TcpServer, 10333); 98 | # Console.WriteLine($"{capability == capability2} {capability.Equals(capability2)}"); // false false 99 | fullnode2 = capabilities.FullNodeCapability(start_height=123) 100 | self.assertNotEqual(fullnode, fullnode2) 101 | self.assertFalse(fullnode == fullnode2) 102 | self.assertFalse(id(fullnode) == id(fullnode2)) 103 | -------------------------------------------------------------------------------- /examples/shared/nep17-token/nep17token.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "COZ Token", 3 | "groups": [], 4 | "abi": { 5 | "methods": [ 6 | { 7 | "name": "symbol", 8 | "offset": 0, 9 | "parameters": [], 10 | "returntype": "String", 11 | "safe": true 12 | }, 13 | { 14 | "name": "decimals", 15 | "offset": 2, 16 | "parameters": [], 17 | "returntype": "Integer", 18 | "safe": true 19 | }, 20 | { 21 | "name": "totalSupply", 22 | "offset": 4, 23 | "parameters": [], 24 | "returntype": "Integer", 25 | "safe": true 26 | }, 27 | { 28 | "name": "balanceOf", 29 | "offset": 27, 30 | "parameters": [ 31 | { 32 | "name": "account", 33 | "type": "Hash160" 34 | } 35 | ], 36 | "returntype": "Integer", 37 | "safe": true 38 | }, 39 | { 40 | "name": "transfer", 41 | "offset": 59, 42 | "parameters": [ 43 | { 44 | "name": "from_address", 45 | "type": "Hash160" 46 | }, 47 | { 48 | "name": "to_address", 49 | "type": "Hash160" 50 | }, 51 | { 52 | "name": "amount", 53 | "type": "Integer" 54 | }, 55 | { 56 | "name": "data", 57 | "type": "Any" 58 | } 59 | ], 60 | "returntype": "Boolean", 61 | "safe": false 62 | }, 63 | { 64 | "name": "verify", 65 | "offset": 413, 66 | "parameters": [], 67 | "returntype": "Boolean", 68 | "safe": false 69 | }, 70 | { 71 | "name": "_deploy", 72 | "offset": 518, 73 | "parameters": [ 74 | { 75 | "name": "data", 76 | "type": "Any" 77 | }, 78 | { 79 | "name": "update", 80 | "type": "Boolean" 81 | } 82 | ], 83 | "returntype": "Void", 84 | "safe": false 85 | }, 86 | { 87 | "name": "onNEP17Payment", 88 | "offset": 420, 89 | "parameters": [ 90 | { 91 | "name": "from_address", 92 | "type": "Any" 93 | }, 94 | { 95 | "name": "amount", 96 | "type": "Integer" 97 | }, 98 | { 99 | "name": "data", 100 | "type": "Any" 101 | } 102 | ], 103 | "returntype": "Void", 104 | "safe": false 105 | }, 106 | { 107 | "name": "_initialize", 108 | "offset": 570, 109 | "parameters": [], 110 | "returntype": "Void", 111 | "safe": false 112 | } 113 | ], 114 | "events": [ 115 | { 116 | "name": "Transfer", 117 | "parameters": [ 118 | { 119 | "name": "from_addr", 120 | "type": "Any" 121 | }, 122 | { 123 | "name": "to_addr", 124 | "type": "Any" 125 | }, 126 | { 127 | "name": "amount", 128 | "type": "Integer" 129 | } 130 | ] 131 | } 132 | ] 133 | }, 134 | "permissions": [ 135 | { 136 | "contract": "*", 137 | "methods": [ 138 | "onNEP17Payment" 139 | ] 140 | } 141 | ], 142 | "trusts": [], 143 | "features": {}, 144 | "supportedstandards": [ 145 | "NEP-17" 146 | ], 147 | "extra": { 148 | "Author": "Mirella Medeiros, Ricardo Prado and Lucas Uezu. COZ in partnership with Simpli", 149 | "Description": "NEP-17 Example Token", 150 | "Email": "contact@coz.io" 151 | } 152 | } -------------------------------------------------------------------------------- /neo3/network/payloads/extensible.py: -------------------------------------------------------------------------------- 1 | """ 2 | Customizable payload. 3 | """ 4 | 5 | from __future__ import annotations 6 | import hashlib 7 | from neo3.core import types, serialization, Size as s, utils 8 | from neo3.network.payloads import inventory, verification 9 | 10 | 11 | class ExtensiblePayload(inventory.IInventory): 12 | """ 13 | A payload that supports arbitrary `data`. 14 | """ 15 | 16 | # Comes from neo3.network.message.Message.PAYLOAD_MAX_SIZE but wanted to break the import cycle 17 | # despite not causing any circular imported errors yet 18 | PAYLOAD_MAX_SIZE = 0x2000000 19 | 20 | def __init__( 21 | self, 22 | category: str, 23 | valid_block_start: int, 24 | valid_block_end: int, 25 | sender: types.UInt160, 26 | data: bytes, 27 | witness: verification.Witness, 28 | ): 29 | super(ExtensiblePayload, self).__init__() 30 | #: An identifier to which category the data belongs 31 | self.category = category 32 | #: Starting height in which the payload is valid 33 | self.valid_block_start = valid_block_start 34 | #: Last height in which the payload is valid 35 | self.valid_block_end = valid_block_end 36 | #: The hash of the account who has sent the payload to the network 37 | self.sender = sender 38 | #: Arbitrary data as required by the payload category 39 | self.data = data 40 | #: The witness of the payload 41 | self.witness = witness 42 | 43 | self.witnesses = [self.witness] # for IVerifiable super 44 | 45 | def __len__(self): 46 | return ( 47 | utils.get_var_size(self.category) 48 | + s.uint32 49 | + s.uint32 50 | + s.uint160 51 | + utils.get_var_size(self.data) 52 | + 1 53 | + len(self.witness) 54 | ) 55 | 56 | def serialize(self, writer: serialization.BinaryWriter) -> None: 57 | """ 58 | Serialize the object into a binary stream. 59 | 60 | Args: 61 | writer: instance. 62 | """ 63 | self.serialize_unsigned(writer) 64 | writer.write_uint8(1) 65 | writer.write_serializable(self.witness) 66 | 67 | def serialize_unsigned(self, writer: serialization.BinaryWriter) -> None: 68 | """ 69 | Serialize the unsigned part of the object into a binary stream. 70 | 71 | Args: 72 | writer: instance. 73 | """ 74 | writer.write_var_string(self.category) 75 | writer.write_uint32(self.valid_block_start) 76 | writer.write_uint32(self.valid_block_end) 77 | writer.write_serializable(self.sender) 78 | writer.write_var_bytes(self.data) 79 | 80 | def deserialize(self, reader: serialization.BinaryReader) -> None: 81 | """ 82 | Deserialize the object from a binary stream. 83 | 84 | Args: 85 | reader: instance. 86 | 87 | Raises: 88 | ValueError: if the check byte is not 1. 89 | """ 90 | self.deserialize_unsigned(reader) 91 | if reader.read_uint8() != 1: 92 | raise ValueError("Deserialization error - check byte incorrect") 93 | self.witness = reader.read_serializable(verification.Witness) 94 | 95 | def deserialize_unsigned(self, reader: serialization.BinaryReader) -> None: 96 | """ 97 | Deserialize the unsigned data part of the object from a binary stream. 98 | 99 | Args: 100 | reader: instance. 101 | 102 | Raises: 103 | ValueError: if the valid_block_start exceeds the valid_block_end field. 104 | """ 105 | self.category = reader.read_var_string(32) 106 | self.valid_block_start = reader.read_uint32() 107 | self.valid_block_end = reader.read_uint32() 108 | if self.valid_block_start >= self.valid_block_end: 109 | raise ValueError( 110 | "Deserialization error - valid_block_start is bigger than valid_block_end" 111 | ) 112 | self.sender = reader.read_serializable(types.UInt160) 113 | self.data = reader.read_var_bytes(self.PAYLOAD_MAX_SIZE) 114 | 115 | def hash(self) -> types.UInt256: 116 | """ 117 | Get a unique identifier based on the unsigned data portion of the object. 118 | """ 119 | with serialization.BinaryWriter() as bw: 120 | self.serialize_unsigned(bw) 121 | data_to_hash = bytearray(bw._stream.getvalue()) 122 | data = hashlib.sha256(data_to_hash).digest() 123 | return types.UInt256(data=data) 124 | 125 | @property 126 | def inventory_type(self) -> inventory.InventoryType: 127 | """ 128 | Inventory type identifier. 129 | """ 130 | return inventory.InventoryType.EXTENSIBLE 131 | 132 | def get_script_hashes_for_verifying(self, snapshot) -> list[types.UInt160]: 133 | """ 134 | Helper method to get the data used in verifying the object. 135 | """ 136 | return [self.sender] 137 | 138 | @classmethod 139 | def _serializable_init(cls): 140 | return cls("", 0, 0, types.UInt160.zero(), b"", verification.Witness(b"", b"")) 141 | -------------------------------------------------------------------------------- /neo3/api/helpers/txbuilder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Builder for creating a Transaction. Calculate fees, add signers and sign. 3 | """ 4 | 5 | from neo3.api import noderpc 6 | from neo3.api.helpers import signing 7 | from neo3.network.payloads import transaction, verification 8 | from neo3.wallet import account 9 | from typing import Optional 10 | 11 | 12 | class TxBuilder: 13 | """ 14 | Transaction builder. 15 | """ 16 | 17 | def __init__(self, client: noderpc.NeoRpcClient, script: Optional[bytes] = None): 18 | self.client = client 19 | self.tx = transaction.Transaction( 20 | version=0, 21 | nonce=123, 22 | system_fee=0, 23 | network_fee=0, 24 | valid_until_block=0, 25 | attributes=[], 26 | signers=[], 27 | script=b"" if script is None else script, 28 | ) 29 | self.signing_funcs: list[signing.SigningFunction] = [] 30 | self.network = -1 31 | 32 | async def init(self) -> None: 33 | """ 34 | Initialize the builder. 35 | """ 36 | res = await self.client.get_version() 37 | self.network = res.protocol.network 38 | 39 | async def calculate_system_fee(self) -> None: 40 | """ 41 | Calculates and set the system fee. Requires at least one signer. 42 | """ 43 | if len(self.tx.signers) == 0: 44 | raise ValueError( 45 | "Need at least one signer (a.k.a the sender who pays for the transaction) or the " 46 | "fee calculation will be incorrect" 47 | ) 48 | res = await self.client.invoke_script(self.tx.script, self.tx.signers) 49 | self.tx.system_fee = res.gas_consumed 50 | 51 | async def set_valid_until_block(self, blocks: int = 1500) -> None: 52 | """ 53 | Set maximum time the transaction is valid in the mempool. Defaults to about 24h for a network with 15s blocktime. 54 | 55 | Args: 56 | blocks: until how many blocks from the current chain height is the transaction valid. Defaults to ~24 hours. 57 | """ 58 | self.tx.valid_until_block = await self.client.get_block_count() + blocks 59 | 60 | async def calculate_network_fee(self) -> None: 61 | """ 62 | Calculate and set the network fee. Requires at least one signer. 63 | """ 64 | if len(self.tx.witnesses) == 0: 65 | if len(self.tx.signers) == 0: 66 | raise ValueError("Cannot calculate network fee without signers") 67 | # adding a witness(es) so we can calculate the network fee 68 | for _ in range(len(self.tx.signers)): 69 | self.tx.witnesses.append(TxBuilder._dummy_signing_witness()) 70 | self.tx.network_fee = await self.client.calculate_network_fee(self.tx) 71 | # removing it here as it will be replaced by a proper one once we're signing 72 | self.tx.witnesses = [] 73 | else: 74 | if len(self.tx.signers) == 0: 75 | raise ValueError("Cannot calculate network fee without signers") 76 | self.tx.network_fee = await self.client.calculate_network_fee(self.tx) 77 | 78 | @staticmethod 79 | def _dummy_signing_witness() -> verification.Witness: 80 | """single signature account witness""" 81 | acc = account.Account.create_new() 82 | if acc.contract is None: 83 | raise Exception( 84 | "Unreachable" 85 | ) # we know this can't happen, but mypy doesn't 86 | return verification.Witness( 87 | invocation_script=b"", verification_script=acc.contract.script 88 | ) 89 | 90 | async def build_and_sign(self) -> transaction.Transaction: 91 | """ 92 | Sign the transaction with all signers and return the finalized result. 93 | """ 94 | len_signers = len(self.tx.signers) 95 | if len_signers == 0: 96 | raise ValueError("Cannot sign transaction without signers") 97 | 98 | if self.network == -1: 99 | raise ValueError( 100 | "Network value not valid (-1). Call init() to automatically sync it from the network or set the `network` attribute" 101 | ) 102 | 103 | for f, s in zip(self.signing_funcs, self.tx.signers): 104 | await f(self.tx, signing.SigningDetails(self.network)) 105 | return self.tx 106 | 107 | def build_unsigned(self) -> transaction.Transaction: 108 | """ 109 | Return the unsigned transaction. For example for use in an offline signing scenario. 110 | """ 111 | return self.tx 112 | 113 | def add_signer( 114 | self, func: signing.SigningFunction, signer: verification.Signer 115 | ) -> None: 116 | """ 117 | Add a Signer with scopes to the transaction and its signing function. 118 | 119 | Args: 120 | func: one of neo3.api.helpers.signing. 121 | signer: a Signer determining the validity of the signature. 122 | 123 | Returns: 124 | 125 | """ 126 | for s in self.tx.signers: 127 | if signer.account == s.account: 128 | raise ValueError( 129 | f"Signer with same account ({signer.account} already exists." 130 | ) 131 | self.tx.signers.append(signer) 132 | self.signing_funcs.append(func) 133 | -------------------------------------------------------------------------------- /neo3/network/ipfilter.py: -------------------------------------------------------------------------------- 1 | """A module for filtering IPs via black and whitelists on P2P nodes (`NeoNode`). 2 | 3 | A global instance ``ipfilter`` can be imported directly from the module and is taken into account by default in the 4 | `NeoNode` class when connections are established. 5 | """ 6 | 7 | from ipaddress import IPv4Network 8 | from contextlib import suppress 9 | from copy import deepcopy 10 | 11 | 12 | class IPFilter: 13 | """ 14 | Filtering rules. 15 | 16 | * The whitelist has precedence over the blacklist settings. 17 | * Host masks can be applied. 18 | * When using host masks do not set host bits (leave them to 0) or an exception will occur. 19 | 20 | The following are `configuration` examples for common scenario's. 21 | 22 | 1. Accept only specific trusted IPs. 23 | 24 | { 25 | 'blacklist': [ 26 | '0.0.0.0/0' 27 | ], 28 | 'whitelist': [ 29 | '10.10.10.10', 30 | '15.15.15.15' 31 | ] 32 | } 33 | 34 | 2. Accept only a range of trusted IPs. 35 | 36 | # Accepts any IP in the range of 10.10.10.0 - 10.10.10.255 37 | 38 | { 39 | 'blacklist': [ 40 | '0.0.0.0/0' 41 | ], 42 | 'whitelist': [ 43 | '10.10.10.0/24', 44 | ] 45 | } 46 | 47 | 3. Accept all except specific IPs. 48 | 49 | # Can be used for banning bad actors 50 | 51 | { 52 | 'blacklist': [ 53 | '12.12.12.12', 54 | '13.13.13.13' 55 | ], 56 | 'whitelist': [ 57 | ] 58 | } 59 | """ 60 | 61 | default_config: dict = {"blacklist": [], "whitelist": []} 62 | 63 | def __init__(self): 64 | self._config = deepcopy(self.default_config) 65 | 66 | def is_allowed(self, address: str) -> bool: 67 | """ 68 | Test if a given address passes the configured restrictions. 69 | 70 | Args: 71 | address: an IPv4 address as defined in the :py:class:`standard library `. 72 | """ 73 | ipv4_address = IPv4Network(address) 74 | 75 | is_allowed = True 76 | 77 | for ip in self._config["blacklist"]: 78 | disallowed = IPv4Network(ip) 79 | if disallowed.overlaps(ipv4_address): 80 | is_allowed = False 81 | break 82 | else: 83 | return is_allowed 84 | 85 | # can override blacklist 86 | for ip in self._config["whitelist"]: 87 | allowed = IPv4Network(ip) 88 | if allowed.overlaps(ipv4_address): 89 | is_allowed = True 90 | 91 | return is_allowed 92 | 93 | def blacklist_add(self, address: str) -> None: 94 | """ 95 | Add an address that will not pass restriction checks. 96 | 97 | Args: 98 | address: an IPv4 address as defined in the :py:class:`standard library `. 99 | """ 100 | self._config["blacklist"].append(address) 101 | 102 | def blacklist_remove(self, address: str) -> None: 103 | """ 104 | Remove an address from the blacklist. 105 | 106 | Args: 107 | address: an IPv4 address as defined in the :py:class:`standard library `. 108 | """ 109 | with suppress(ValueError): 110 | self._config["blacklist"].remove(address) 111 | 112 | def whitelist_add(self, address: str) -> None: 113 | """ 114 | Add an address that will pass restriction checks. 115 | 116 | Args: 117 | address: an IPv4 address as defined in the :py:class:`standard library `. 118 | """ 119 | self._config["whitelist"].append(address) 120 | 121 | def whitelist_remove(self, address: str) -> None: 122 | """ 123 | Remove an address from the whitelist. 124 | 125 | Args: 126 | address: an IPv4 address as defined in the :py:class:`standard library `. 127 | """ 128 | with suppress(ValueError): 129 | self._config["whitelist"].remove(address) 130 | 131 | def load_config(self, config: dict[str, list[str]]) -> None: 132 | """ 133 | Load filtering rules from a configuration object. 134 | 135 | Args: 136 | config: a _dictionary holding 2 keys, `blacklist` & `whitelist`, each having a 137 | :py:class:`list ` type value holding :py:class:`str ` type ``address`` es. 138 | See :ref:`IPFilter examples`. For ``address`` format refer to the 139 | :py:class:`standard library `. 140 | Raises: 141 | ValueError: if the required config keys are not found. 142 | """ 143 | if "whitelist" not in config: 144 | raise ValueError("whitelist key not found") 145 | if "blacklist" not in config: 146 | raise ValueError("blacklist key not found") 147 | self._config = config 148 | 149 | def reset(self) -> None: 150 | """ 151 | Clear the filter rules. 152 | """ 153 | self._config = deepcopy(self.default_config) 154 | 155 | 156 | ipfilter = IPFilter() 157 | -------------------------------------------------------------------------------- /tests/core/test_cryptography.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import binascii 3 | import hashlib 4 | from neo3crypto import mmh3_hash_bytes, mmh3_hash 5 | from neo3.core import types 6 | from neo3.core import cryptography as crypto 7 | 8 | 9 | class MerkleTreeTestCase(unittest.TestCase): 10 | def test_compute_root_single_hash(self): 11 | data = binascii.unhexlify(b"aa" * 32) 12 | hash1 = types.UInt256(data=data) 13 | root = crypto.MerkleTree.compute_root([hash1]) 14 | 15 | self.assertEqual(data, root.to_array()) 16 | 17 | def test_compute_root_multiple_hashes(self): 18 | expected_hash = hashlib.sha256( 19 | hashlib.sha256(binascii.unhexlify(b"aa" * 32 + b"bb" * 32)).digest() 20 | ).digest() 21 | 22 | hash1 = types.UInt256(data=binascii.unhexlify(b"aa" * 32)) 23 | hash2 = types.UInt256(data=binascii.unhexlify(b"bb" * 32)) 24 | hashes = [hash1, hash2] 25 | root = crypto.MerkleTree.compute_root(hashes) 26 | 27 | self.assertEqual(expected_hash, root.to_array()) 28 | 29 | def test_computer_root_no_input(self): 30 | self.assertEqual(types.UInt256.zero(), crypto.MerkleTree.compute_root([])) 31 | 32 | def test_build_no_leaves(self): 33 | with self.assertRaises(ValueError) as context: 34 | crypto.MerkleTree._build([]) 35 | self.assertIn("Leaves must have length", str(context.exception)) 36 | 37 | def test_to_hash_array(self): 38 | hash1 = types.UInt256(data=binascii.unhexlify(b"aa" * 32)) 39 | hash2 = types.UInt256(data=binascii.unhexlify(b"bb" * 32)) 40 | hash3 = types.UInt256(data=binascii.unhexlify(b"cc" * 32)) 41 | hash4 = types.UInt256(data=binascii.unhexlify(b"dd" * 32)) 42 | hash5 = types.UInt256(data=binascii.unhexlify(b"ee" * 32)) 43 | hashes = [hash1, hash2, hash3, hash4, hash5] 44 | 45 | m = crypto.MerkleTree(hashes) 46 | hash_array = m.to_hash_array() 47 | 48 | # sort the array 49 | hash_array = sorted(hash_array) 50 | 51 | for i, h in enumerate(hashes): 52 | self.assertEqual(h, hash_array[i]) 53 | 54 | def test_merkle_node_methods(self): 55 | hash1 = types.UInt256(data=binascii.unhexlify(b"aa" * 32)) 56 | hash2 = types.UInt256(data=binascii.unhexlify(b"bb" * 32)) 57 | hashes = [hash1, hash2] 58 | m = crypto.MerkleTree(hashes) 59 | 60 | self.assertEqual(True, m.root.is_root()) 61 | self.assertEqual(False, m.root.is_leaf()) 62 | self.assertEqual(False, m.root.left_child.is_root()) 63 | self.assertEqual(True, m.root.left_child.is_leaf()) 64 | 65 | 66 | class BloomFilterTestCase(unittest.TestCase): 67 | def shortDescription(self): 68 | # disable docstring printing in test runner 69 | return None 70 | 71 | def test_seed_building(self): 72 | filter = crypto.BloomFilter(m=7, k=10, ntweak=123456) 73 | # these values have been captured by creating a BloomFilter in C# and reading the seed values through debugging 74 | expected_seeds = [ 75 | 123456, 76 | 4222003669, 77 | 4148916586, 78 | 4075829503, 79 | 4002742420, 80 | 3929655337, 81 | 3856568254, 82 | 3783481171, 83 | 3710394088, 84 | 3637307005, 85 | ] 86 | self.assertEqual(expected_seeds, filter.seeds) 87 | 88 | def test_add_and_check(self): 89 | # modelled after https://github.com/neo-project/neo/blob/982e69090f27c1415872536ce39aea22f0873467/neo.UnitTests/Cryptography/UT_BloomFilter.cs#L12 90 | elements = b"\x00\x01\x02\x03\x04" 91 | filter = crypto.BloomFilter(m=7, k=10, ntweak=123456) 92 | filter.add(elements) 93 | self.assertTrue(filter.check(elements)) 94 | self.assertFalse(filter.check(elements + b"\x05\x06\x07\x08\x09")) 95 | 96 | def test_init_with_elements(self): 97 | elements = b"\x00\x01\x02\x03\x04" 98 | m = 7 99 | k = 10 100 | filter = crypto.BloomFilter(m=m, k=k, ntweak=123456, elements=elements) 101 | self.assertEqual(m, len(filter.bits)) 102 | self.assertEqual(k, len(filter.seeds)) 103 | 104 | def test_get_bits(self): 105 | """ 106 | byte[] elements = { 0, 1, 2, 3, 4}; 107 | BloomFilter bf = new BloomFilter(7, 10, 123456, elements); 108 | byte[] buffer = new byte[(bf.M/8)+1]; 109 | Console.WriteLine($"\\x{BitConverter.ToString(buffer).Replace("-", "\\x")}"); 110 | """ 111 | elements = b"\x00\x01\x02\x03\x04" 112 | filter = crypto.BloomFilter(m=7, k=10, ntweak=123456, elements=elements) 113 | self.assertEqual(b"\x00", filter.get_bits()) 114 | 115 | 116 | class Murmur128test(unittest.TestCase): 117 | def shortDescription(self): 118 | # disable docstring printing in test runner 119 | return None 120 | 121 | def test_neo_cases(self): 122 | # https://github.com/Liaojinghui/neo/blob/30f33d075502acd792f804ffcf84cce689255306/tests/neo.UnitTests/Cryptography/UT_Murmur128.cs 123 | self.assertEqual( 124 | bytes.fromhex("0bc59d0ad25fde2982ed65af61227a0e"), 125 | mmh3_hash_bytes("hello", 123), 126 | ) 127 | self.assertEqual( 128 | bytes.fromhex("3d3810fed480472bd214a14023bb407f"), 129 | mmh3_hash_bytes("world", 123), 130 | ) 131 | self.assertEqual( 132 | bytes.fromhex("e0a0632d4f51302c55e3b3e48d28795d"), 133 | mmh3_hash_bytes("hello world", 123), 134 | ) 135 | 136 | 137 | class MurMur32test(unittest.TestCase): 138 | def test_one(self): 139 | x: int = mmh3_hash(b"\x01\x02\x03\x04\x05\x06", 123, signed=False) 140 | self.assertEqual( 141 | bytes.fromhex("3cdc1e41"), x.to_bytes(4, "little", signed=False) 142 | ) 143 | -------------------------------------------------------------------------------- /neo3/contracts/contract.py: -------------------------------------------------------------------------------- 1 | """ 2 | Smart contract and account contract classes. Contains a list of all native contracts. 3 | """ 4 | 5 | from __future__ import annotations 6 | from collections.abc import Sequence 7 | from dataclasses import dataclass 8 | from neo3.contracts import abi, utils, nef, manifest 9 | from neo3.core import cryptography, utils as coreutils, types, serialization, Size as s 10 | 11 | 12 | @dataclass 13 | class _ContractHashes: 14 | CRYPTO_LIB = types.UInt160.from_string("0x726cb6e0cd8628a1350a611384688911ab75f51b") 15 | GAS_TOKEN = types.UInt160.from_string("0xd2a4cff31913016155e38e474a2c06d08be276cf") 16 | LEDGER = types.UInt160.from_string("0xda65b600f7124ce6c79950c1772a36403104f2be") 17 | MANAGEMENT = types.UInt160.from_string("0xfffdc93764dbaddd97c48f252a53ea4643faa3fd") 18 | NEO_TOKEN = types.UInt160.from_string("0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5") 19 | ORACLE = types.UInt160.from_string("0xfe924b7cfe89ddd271abaf7210a80a7e11178758") 20 | POLICY = types.UInt160.from_string("0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b") 21 | ROLE_MANAGEMENT = types.UInt160.from_string( 22 | "0x49cf4e5378ffcd4dec034fd98a174c5491e395e2" 23 | ) 24 | STD_LIB = types.UInt160.from_string("0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0") 25 | 26 | 27 | #: List of Neo's native contract hashes. 28 | CONTRACT_HASHES = _ContractHashes() 29 | 30 | 31 | class Contract: 32 | """ 33 | Generic contract. 34 | """ 35 | 36 | def __init__( 37 | self, script: bytes, parameter_list: Sequence[abi.ContractParameterType] 38 | ): 39 | #: The contract instructions (OpCodes) 40 | self.script = script 41 | self.parameter_list = parameter_list 42 | self._script_hash = coreutils.to_script_hash(self.script) 43 | self._address = None 44 | 45 | @property 46 | def script_hash(self) -> types.UInt160: 47 | """ 48 | The contract script hash. 49 | """ 50 | return self._script_hash 51 | 52 | @classmethod 53 | def create_multisig_contract( 54 | cls, m: int, public_keys: Sequence[cryptography.ECPoint] 55 | ) -> Contract: 56 | """ 57 | Create a multi-signature contract requiring `m` signatures from the list `public_keys`. 58 | 59 | Args: 60 | m: minimum number of signature required for signing. Can't be lower than 2. 61 | public_keys: public keys to use during verification. 62 | """ 63 | return cls( 64 | script=utils.create_multisig_redeemscript(m, public_keys), 65 | parameter_list=[abi.ContractParameterType.SIGNATURE] * m, 66 | ) 67 | 68 | @classmethod 69 | def create_signature_contract(cls, public_key: cryptography.ECPoint) -> Contract: 70 | """ 71 | Create a signature contract. 72 | 73 | Args: 74 | public_key: the public key to use during verification. 75 | """ 76 | return cls( 77 | utils.create_signature_redeemscript(public_key), 78 | [abi.ContractParameterType.SIGNATURE], 79 | ) 80 | 81 | 82 | class ContractState(serialization.ISerializable): 83 | """ 84 | Smart contract chain state container. 85 | """ 86 | 87 | def __init__( 88 | self, 89 | id_: int, 90 | nef: nef.NEF, 91 | manifest_: manifest.ContractManifest, 92 | update_counter: int, 93 | hash_: types.UInt160, 94 | ): 95 | self.id = id_ 96 | self.nef = nef 97 | self.manifest = manifest_ 98 | self.update_counter = update_counter 99 | self.hash = hash_ 100 | 101 | def __len__(self): 102 | return ( 103 | s.uint32 # id 104 | + len(self.nef.to_array()) 105 | + len(self.manifest) 106 | + s.uint16 # update counter 107 | + len(self.hash) 108 | ) 109 | 110 | def __eq__(self, other): 111 | if other is None: 112 | return False 113 | if type(self) != type(other): 114 | return False 115 | if self.hash != other.hash: 116 | return False 117 | return True 118 | 119 | def __deepcopy__(self, memodict={}): 120 | return ContractState.deserialize_from_bytes(self.to_array()) 121 | 122 | @property 123 | def script(self) -> bytes: 124 | """ 125 | NEF script 126 | """ 127 | return self.nef.script 128 | 129 | @script.setter 130 | def script(self, value: bytes) -> None: 131 | self.nef.script = value 132 | 133 | def serialize(self, writer: serialization.BinaryWriter) -> None: 134 | writer.write_int32(self.id) 135 | writer.write_serializable(self.nef) 136 | writer.write_serializable(self.manifest) 137 | writer.write_uint16(self.update_counter) 138 | writer.write_serializable(self.hash) 139 | 140 | def deserialize(self, reader: serialization.BinaryReader) -> None: 141 | self.id = reader.read_int32() 142 | self.nef = reader.read_serializable(nef.NEF) 143 | self.manifest = reader.read_serializable(manifest.ContractManifest) 144 | self.update_counter = reader.read_uint16() 145 | self.hash = reader.read_serializable(types.UInt160) 146 | 147 | def can_call(self, target_contract: ContractState, target_method: str) -> bool: 148 | """ 149 | Utility function to check if the contract has permission to call `target_method` on `target_contract`. 150 | 151 | Args: 152 | target_contract: 153 | target_method: 154 | 155 | Returns: 156 | `True` if allowed. `False` if not possible. 157 | """ 158 | results = list( 159 | map( 160 | lambda p: p.is_allowed( 161 | target_contract.hash, target_contract.manifest, target_method 162 | ), 163 | self.manifest.permissions, 164 | ) 165 | ) 166 | return any(results) 167 | 168 | @classmethod 169 | def _serializable_init(cls): 170 | return cls( 171 | 0, 172 | nef.NEF._serializable_init(), 173 | manifest.ContractManifest(), 174 | 0, 175 | types.UInt160.zero(), 176 | ) 177 | -------------------------------------------------------------------------------- /tests/core/test_biginteger.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from neo3.core.types import BigInteger 3 | 4 | 5 | class BigIntegerTestCase(TestCase): 6 | def test_big_integer_add(self): 7 | b1 = BigInteger(10) 8 | b2 = BigInteger(20) 9 | 10 | b3 = b1 + b2 11 | 12 | self.assertIsInstance(b3, BigInteger) 13 | self.assertEqual(30, b3) 14 | 15 | def test_big_integer_sub(self): 16 | b1 = BigInteger(5505505505505505050505) 17 | b2 = BigInteger(5505505505505505000000) 18 | 19 | b3 = b1 - b2 20 | 21 | self.assertIsInstance(b3, BigInteger) 22 | self.assertEqual(50505, b3) 23 | 24 | def test_big_integer_mul(self): 25 | b1 = BigInteger(55055055055055) 26 | b2 = BigInteger(55055055055) 27 | 28 | b3 = b1 * b2 29 | 30 | self.assertIsInstance(b3, BigInteger) 31 | self.assertEqual(3031059087112109081053025, b3) 32 | 33 | def test_big_integer_div(self): 34 | b1 = BigInteger(55055055055055) 35 | b2 = BigInteger(55055055) 36 | 37 | b3 = b1 / b2 38 | self.assertIsInstance(b3, BigInteger) 39 | self.assertEqual(1000000, b3) 40 | 41 | def test_big_integer_div2(self): 42 | b1 = BigInteger(41483775933600000000) 43 | b2 = BigInteger(414937759336) 44 | 45 | b3 = b1 / b2 46 | b4 = b1 // b2 47 | self.assertIsInstance(b3, BigInteger) 48 | self.assertEqual(99975899, b3) 49 | self.assertEqual(b4, b3) 50 | 51 | def test_big_integer_div_rounding(self): 52 | b1 = BigInteger(1) 53 | b2 = BigInteger(2) 54 | self.assertEqual(0, b1 / b2) # 0.5 -> 0 55 | 56 | b1 = BigInteger(2) 57 | b2 = BigInteger(3) 58 | self.assertEqual(0, b1 / b2) # 0.66 -> 0 59 | 60 | b1 = BigInteger(5) 61 | b2 = BigInteger(4) 62 | self.assertEqual(1, b1 / b2) # 1.25 -> 1 63 | 64 | b1 = BigInteger(5) 65 | b2 = BigInteger(3) 66 | self.assertEqual(1, b1 / b2) # 1.66 -> 1 67 | 68 | b1 = BigInteger(-1) 69 | b2 = BigInteger(3) 70 | self.assertEqual(0, b1 / b2) # -0.33 -> 0 71 | 72 | b1 = BigInteger(-5) 73 | b2 = BigInteger(3) 74 | self.assertEqual(-1, b1 / b2) # -1.66 -> -1 75 | 76 | b1 = BigInteger(1) 77 | b2 = BigInteger(-2) 78 | self.assertEqual(0, b1 / b2) 79 | 80 | def test_big_integer_div_old_block1473972(self): 81 | b1 = BigInteger(-11001000000) 82 | b2 = BigInteger(86400) 83 | result = b1 / b2 84 | self.assertEqual(-127326, result) 85 | 86 | def test_big_integer_to_bytearray(self): 87 | b1 = BigInteger(8972340892734890723) 88 | ba = b1.to_array() 89 | 90 | integer = BigInteger(ba) 91 | self.assertEqual(8972340892734890723, integer) 92 | 93 | b2 = BigInteger(-100) 94 | b2ba = b2.to_array() 95 | integer2 = BigInteger(b2ba) 96 | self.assertEqual(-100, integer2) 97 | 98 | b3 = BigInteger(128) 99 | b3ba = b3.to_array() 100 | self.assertEqual(b"\x80\x00", b3ba) 101 | 102 | b4 = BigInteger(0) 103 | b4ba = b4.to_array() 104 | self.assertEqual(b"\x00", b4ba) 105 | 106 | b5 = BigInteger(-146) 107 | b5ba = b5.to_array() 108 | self.assertEqual(b"\x6e\xff", b5ba) 109 | 110 | b6 = BigInteger(-48335248028225339427907476932896373492484053930) 111 | b6ba = b6.to_array() 112 | self.assertEqual(20, len(b6ba)) 113 | 114 | b7 = BigInteger(-399990000) 115 | b7ba = b7.to_array() 116 | self.assertEqual(b"\x10\xa3\x28\xe8", b7ba) 117 | 118 | b8 = BigInteger(-65023) 119 | b8ba = b8.to_array() 120 | self.assertEqual(b"\x01\x02\xff", b8ba) 121 | 122 | def test_big_integer_frombytes(self): 123 | b1 = BigInteger(8972340892734890723) 124 | ba = b1.to_array() 125 | 126 | b2 = BigInteger(ba) 127 | self.assertEqual(b1, b2) 128 | self.assertTrue(b1 == b2) 129 | 130 | def test_big_integer_sign(self): 131 | b1 = BigInteger(3) 132 | b2 = BigInteger(0) 133 | b3 = BigInteger(-4) 134 | self.assertEqual(1, b1.sign) 135 | self.assertEqual(0, b2.sign) 136 | self.assertEqual(-1, b3.sign) 137 | 138 | def test_big_integer_modulo(self): 139 | b1 = BigInteger(860593) 140 | b2 = BigInteger(-201) 141 | self.assertEqual(112, b1 % b2) 142 | 143 | b1 = BigInteger(20195283520469175757) 144 | b2 = BigInteger(1048576) 145 | self.assertEqual(888269, b1 % b2) 146 | 147 | b1 = BigInteger( 148 | -18224909727634776050312394179610579601844989529623334093909233530432892596607 149 | ) 150 | b2 = BigInteger(14954691977398614017) 151 | self.assertEqual(-3100049211437790421, b1 % b2) 152 | 153 | b3 = BigInteger(b"+K\x05\xbe\xaai\xfa\xd4") 154 | self.assertEqual(b3, b1 % b2) 155 | 156 | def test_dunder_methods(self): 157 | b1 = BigInteger(1) 158 | b2 = BigInteger(2) 159 | b3 = BigInteger(3) 160 | 161 | self.assertEqual(1, abs(b1)) 162 | self.assertEqual(0, b1 % 1) 163 | self.assertEqual(-1, -b1) 164 | self.assertEqual("1", str(b1)) 165 | self.assertEqual(1, b3 // b2) 166 | 167 | right_shift = b3 >> b1 168 | self.assertEqual(1, right_shift) 169 | self.assertIsInstance(right_shift, BigInteger) 170 | 171 | left_shift = b1 << b3 172 | self.assertEqual(8, left_shift) 173 | self.assertIsInstance(left_shift, BigInteger) 174 | 175 | def test_negative_shifting(self): 176 | # C#'s BigInteger changes a left shift with a negative shift index, 177 | # to a right shift with a positive index. 178 | 179 | b1 = BigInteger(8) 180 | b2 = BigInteger(-3) 181 | # shift against BigInteger 182 | self.assertEqual(1, b1 << b2) 183 | # shift against integer 184 | self.assertEqual(1, b1 << -3) 185 | 186 | # the same as above but for right shift 187 | self.assertEqual(64, b1 >> b2) 188 | self.assertEqual(64, b1 >> -3) 189 | 190 | def test_specials(self): 191 | self.assertEqual(0, BigInteger.zero()) 192 | self.assertEqual(1, BigInteger.one()) 193 | b = BigInteger.zero() 194 | -------------------------------------------------------------------------------- /neo3/network/capabilities.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import abc 3 | from enum import IntEnum 4 | from neo3.core import serialization, Size as s 5 | 6 | 7 | class NodeCapabilityType(IntEnum): 8 | #: Server has TCP listening capabilities 9 | TCPSERVER = 0x01 10 | #: Server has WebSocket listening capabilities 11 | WSSERVER = 0x02 12 | #: Disable P2P compression 13 | DISABLE_P2P_COMPRESSION = 0x03 14 | #: Server has full chain data 15 | FULLNODE = 0x10 16 | 17 | 18 | class NodeCapability(serialization.ISerializable): 19 | """ 20 | Capability base class. 21 | """ 22 | 23 | def __init__(self, n_type: NodeCapabilityType): 24 | self.type = n_type 25 | 26 | def __len__(self): 27 | """Get the total size in bytes of the object.""" 28 | return s.uint8 29 | 30 | def __eq__(self, other): 31 | pass 32 | 33 | def serialize(self, writer: serialization.BinaryWriter) -> None: 34 | """ 35 | Serialize the object into a binary stream. 36 | 37 | Args: 38 | writer: instance. 39 | """ 40 | writer.write_uint8(self.type) 41 | self.serialize_without_type(writer) 42 | 43 | def deserialize(self, reader: serialization.BinaryReader) -> None: 44 | """ 45 | Deserialize the object from a binary stream. 46 | 47 | Args: 48 | reader: instance. 49 | """ 50 | self.type = NodeCapabilityType(reader.read_uint8()) 51 | self.deserialize_without_type(reader) 52 | 53 | @staticmethod 54 | def deserialize_from(reader: serialization.BinaryReader) -> NodeCapability: 55 | capability_type = NodeCapabilityType(reader.read_uint8()) 56 | if capability_type in [ 57 | NodeCapabilityType.TCPSERVER, 58 | NodeCapabilityType.WSSERVER, 59 | ]: 60 | capability = ServerCapability(capability_type) # type: NodeCapability 61 | elif capability_type == NodeCapabilityType.FULLNODE: 62 | capability = FullNodeCapability() 63 | else: 64 | raise ValueError( 65 | "Unreachable" 66 | ) # instantiating NodeCapabilityType will raise an error on unknown type 67 | 68 | capability.deserialize_without_type(reader) 69 | return capability # a type of NodeCapability or inherited 70 | 71 | @abc.abstractmethod 72 | def deserialize_without_type(self, reader: serialization.BinaryReader) -> None: 73 | """Deserialize from a buffer without reading the `type` member.""" 74 | 75 | @abc.abstractmethod 76 | def serialize_without_type(self, writer: serialization.BinaryWriter) -> None: 77 | """Serialize into a buffer without including the `type` member.""" 78 | 79 | @classmethod 80 | def _serializable_init(cls): 81 | return cls(NodeCapabilityType.FULLNODE) 82 | 83 | 84 | class ServerCapability(NodeCapability): 85 | """ 86 | A capability expressing node support for TCP or Websocket services. 87 | """ 88 | 89 | def __init__(self, n_type: NodeCapabilityType, port: int = 0): 90 | super(ServerCapability, self).__init__(n_type) 91 | if n_type not in [NodeCapabilityType.TCPSERVER, NodeCapabilityType.WSSERVER]: 92 | raise TypeError( 93 | f"{n_type} not one of: {NodeCapabilityType.TCPSERVER.name} {NodeCapabilityType.WSSERVER.name}" 94 | ) # noqa 95 | self.port = port 96 | 97 | def __len__(self): 98 | return super(ServerCapability, self).__len__() + s.uint16 99 | 100 | def serialize_without_type(self, writer: serialization.BinaryWriter) -> None: 101 | """ 102 | Serialize the object into a binary stream without serializing the base class `type` property. 103 | 104 | Args: 105 | writer: instance. 106 | """ 107 | writer.write_uint16(self.port) 108 | 109 | def deserialize_without_type(self, reader: serialization.BinaryReader) -> None: 110 | """ 111 | Deserialize the object from a binary stream without deserializing the base class `type` property. 112 | 113 | Args: 114 | reader: instance. 115 | """ 116 | self.port = reader.read_uint16() 117 | 118 | @classmethod 119 | def _serializable_init(cls): 120 | return cls(NodeCapabilityType.TCPSERVER, 0) 121 | 122 | 123 | class FullNodeCapability(NodeCapability): 124 | """ 125 | A capability expressing the node has full blockchain data and accepts relaying. 126 | """ 127 | 128 | def __init__(self, start_height: int = 0): 129 | super(FullNodeCapability, self).__init__(NodeCapabilityType.FULLNODE) 130 | self.start_height = start_height 131 | 132 | def __len__(self): 133 | return super(FullNodeCapability, self).__len__() + s.uint32 134 | 135 | def serialize_without_type(self, writer: serialization.BinaryWriter) -> None: 136 | """ 137 | Serialize the object into a binary stream without serializing the base class `type` property. 138 | 139 | Args: 140 | writer: instance. 141 | """ 142 | writer.write_uint32(self.start_height) 143 | 144 | def deserialize_without_type(self, reader: serialization.BinaryReader) -> None: 145 | """ 146 | Deserialize the object from a binary stream without deserializing the base class `type` property. 147 | 148 | Args: 149 | reader: instance. 150 | """ 151 | self.start_height = reader.read_uint32() 152 | 153 | 154 | class DisableCompressionCapability(NodeCapability): 155 | """ 156 | A capability to disable P2P compression. 157 | """ 158 | 159 | def serialize_without_type(self, writer: serialization.BinaryWriter) -> None: 160 | """ 161 | Serialize the object into a binary stream without serializing the base class `type` property. 162 | 163 | Args: 164 | writer: instance. 165 | """ 166 | writer.write_uint8(0) 167 | 168 | def deserialize_without_type(self, reader: serialization.BinaryReader) -> None: 169 | """ 170 | Deserialize the object from a binary stream without deserializing the base class `type` property. 171 | 172 | Args: 173 | reader: instance. 174 | """ 175 | v = reader.read_uint8() 176 | if v != 0: 177 | raise ValueError("Disable compression type should not have any data") 178 | -------------------------------------------------------------------------------- /neo3/network/message.py: -------------------------------------------------------------------------------- 1 | """ 2 | P2P network message classes. 3 | """ 4 | 5 | from __future__ import annotations 6 | import lz4.block # type: ignore 7 | from enum import IntEnum, IntFlag 8 | from neo3.network.payloads import ( 9 | inventory, 10 | block, 11 | version, 12 | address, 13 | empty, 14 | extensible, 15 | ping, 16 | transaction, 17 | ) 18 | from neo3.core import Size as s, serialization 19 | from neo3 import network_logger as logger 20 | from typing import Optional 21 | 22 | 23 | class MessageType(IntEnum): 24 | """ 25 | P2P network message types. 26 | """ 27 | 28 | VERSION = 0x00 29 | VERACK = 0x01 30 | 31 | GETADDR = 0x10 32 | ADDR = 0x11 33 | PING = 0x18 34 | PONG = 0x19 35 | 36 | GETHEADERS = 0x20 37 | HEADERS = 0x21 38 | GETBLOCKS = 0x24 39 | MEMPOOL = 0x25 40 | INV = 0x27 41 | GETDATA = 0x28 42 | GETBLOCKBYINDEX = 0x29 43 | NOTFOUND = 0x2A 44 | TRANSACTION = 0x2B 45 | BLOCK = 0x2C 46 | CONSENSUS = 0x2D 47 | EXTENSIBLE = 0x2E 48 | REJECT = 0x2F 49 | 50 | FILTERLOAD = 0x30 51 | FILTERADD = 0x31 52 | FILTERCLEAR = 0x32 53 | MERKLEBLOCK = 0x38 54 | 55 | ALERT = 0x40 56 | 57 | DEFAULT = 0xFF # not supported in the real protocol 58 | 59 | 60 | class MessageConfig(IntFlag): 61 | """ 62 | P2P network message config flags. 63 | """ 64 | 65 | #: Indicates that the payload data is not compressed. 66 | NONE = 0 67 | #: Indicates that the payload data is compressed using LZ4. 68 | COMPRESSED = 1 << 0 69 | 70 | 71 | class Message(serialization.ISerializable): 72 | """ 73 | P2P network message container. 74 | """ 75 | 76 | PAYLOAD_MAX_SIZE = 0x2000000 77 | COMPRESSION_MIN_SIZE = 128 78 | COMPRESSION_THRESHOLD = 64 79 | 80 | def __init__( 81 | self, 82 | msg_type: MessageType, 83 | payload: Optional[serialization.ISerializable_T] = None, 84 | ): 85 | """ 86 | 87 | Args: 88 | msg_type: message object configuration. 89 | payload: an identifier specifying the purpose of the message. 90 | """ 91 | self.config = ( 92 | MessageConfig.NONE 93 | ) #: MessageConfig: message object configuration. 94 | #: MessageType: an identifier specifying the purpose of the message. 95 | self.type: MessageType = msg_type 96 | self.payload: serialization.ISerializable_T = empty.EmptyPayload() # type: ignore 97 | # mypy doesn't get EmptyPayload is an ISerializable 98 | 99 | if payload: 100 | self.payload = payload 101 | 102 | def __len__(self): 103 | """Get the total size in bytes of the object.""" 104 | return s.uint8 + s.uint8 + len(self.payload) 105 | 106 | def serialize(self, writer: serialization.BinaryWriter) -> None: 107 | """ 108 | Serialize the object into a binary stream. 109 | 110 | Args: 111 | writer: instance. 112 | """ 113 | payload = self.payload.to_array() 114 | 115 | if ( 116 | len(self.payload) > self.COMPRESSION_MIN_SIZE 117 | and MessageConfig.COMPRESSED not in self.config 118 | ): 119 | compressed_data = lz4.block.compress(self.payload.to_array(), store_size=False) # type: ignore 120 | compressed_data = len(payload).to_bytes(4, "little") + compressed_data 121 | if len(compressed_data) < len(self.payload) - self.COMPRESSION_THRESHOLD: 122 | payload = compressed_data 123 | self.config |= MessageConfig.COMPRESSED 124 | 125 | writer.write_uint8(self.config) 126 | writer.write_uint8(self.type.value) 127 | writer.write_var_bytes(payload) 128 | 129 | def deserialize(self, reader: serialization.BinaryReader) -> None: 130 | """ 131 | Deserialize the object from a binary stream. 132 | 133 | Args: 134 | reader: instance. 135 | """ 136 | self.config = MessageConfig(reader.read_uint8()) 137 | self.type = MessageType(reader.read_uint8()) 138 | 139 | payload_data = reader.read_var_bytes(self.PAYLOAD_MAX_SIZE) 140 | if len(payload_data) > 0: 141 | if MessageConfig.COMPRESSED in self.config: 142 | # From the lz4 documentation: 143 | # "The uncompressed_size argument specifies an upper bound on the size of the uncompressed data size 144 | # rather than an absolute value" 145 | try: 146 | size = int.from_bytes(payload_data[:4], "little") 147 | payload_data = lz4.block.decompress( 148 | payload_data[4:], uncompressed_size=size 149 | ) 150 | except lz4.block.LZ4BlockError: 151 | raise ValueError("Invalid payload data - decompress failed") 152 | 153 | self.payload = self._payload_from_data(self.type, payload_data) 154 | 155 | if self.payload is None: 156 | self.payload = empty.EmptyPayload() 157 | 158 | @staticmethod 159 | def _payload_from_data(msg_type, data): 160 | with serialization.BinaryReader(data) as br: 161 | if msg_type in [MessageType.INV, MessageType.GETDATA]: 162 | return br.read_serializable(inventory.InventoryPayload) 163 | elif msg_type == MessageType.GETBLOCKBYINDEX: 164 | return br.read_serializable(block.GetBlockByIndexPayload) 165 | elif msg_type == MessageType.VERSION: 166 | return br.read_serializable(version.VersionPayload) 167 | elif msg_type == MessageType.VERACK: 168 | return br.read_serializable(empty.EmptyPayload) 169 | elif msg_type == MessageType.BLOCK: 170 | return br.read_serializable(block.Block) 171 | elif msg_type == MessageType.HEADERS: 172 | return br.read_serializable(block.HeadersPayload) 173 | elif msg_type in [MessageType.PING, MessageType.PONG]: 174 | return br.read_serializable(ping.PingPayload) 175 | elif msg_type == MessageType.ADDR: 176 | return br.read_serializable(address.AddrPayload) 177 | elif msg_type == MessageType.TRANSACTION: 178 | return br.read_serializable(transaction.Transaction) 179 | elif msg_type == MessageType.EXTENSIBLE: 180 | return br.read_serializable(extensible.ExtensiblePayload) 181 | else: 182 | logger.debug(f"Unsupported payload {msg_type.name}") 183 | 184 | @classmethod 185 | def _serializable_init(cls): 186 | return cls(MessageType.DEFAULT) 187 | -------------------------------------------------------------------------------- /tests/wallet/test_account.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from neo3.wallet import account, scrypt_parameters as scrypt 4 | 5 | account_list = [ 6 | { 7 | "address": "NRaKbRA5JAEJtfUgJJZzmeDnKvP3pJwKp1", 8 | "encrypted_key": "6PYKuriAL7pFeVTr3tKksbD1SpKUP7K82vjGuskZ5zpo9EWDhLRW6GcnyL", 9 | "password": "city of zion", 10 | "private_key": "58124574dfcca1a7a958775f6ea94e3d6c392ec3ba125b5bc591dd5e14f05e52", 11 | "script_hash": "18f13748e08d53c9a164227e1a3e8d8d9e78193e", 12 | "wif_key": "KzAuju4yBqBhmUzYpfEEppPW8jfxALTsdsUR8hLPv9R3PBD97CUv", 13 | }, 14 | { 15 | "address": "NgPptMp2tcjnXuYbUrTozvwvLExGKk5jXc", 16 | "encrypted_key": "6PYMEujkLZiJrQ5AK9W4z1BtYZT2U27ZVKrjbEFt8zZh5CJANZdEx21Fyx", 17 | "password": "123", 18 | "private_key": "2032b737522d22e2b6faf30555faa91d95c5aa5113c18f218f45815b6934c558", 19 | "script_hash": "cfa9032d65b3d0fc1df3956a4ef01666f23ba7e0", 20 | "wif_key": "KxJJLmU1Nv7igx3RFM4siSvio7wasF3ZzMzi7SrJ1s78QDQeEtjs", 21 | "scrypt": {"n": 2, "r": 8, "p": 8}, 22 | }, 23 | { 24 | "address": "NZMHRJMPbyJJwtXpvS2mYAWcWp4qmZZFx8", 25 | "encrypted_key": "6PYL44vbRemjfwCJ8qprKKJJiuzcopnJhghPoMLRVJLpymDwm2BNj9v7fq", 26 | "password": "neo", 27 | "private_key": "4c5182d9041f416bee1a6adac6a03f3e0319a83e75e78e6ff739304095791f19", 28 | "script_hash": "0df27baba6baeeb6834bea0d6c2a78183b416393", 29 | "wif_key": "Kyn4fA6czAhktoAM9YXKv3m7jtt47AuQxCXqSusnBmj3GsZUZQ6M", 30 | "scrypt": {"n": 2, "r": 8, "p": 8}, 31 | }, 32 | ] 33 | 34 | 35 | class AccountCreationTestCase(unittest.TestCase): 36 | def test_new_account(self): 37 | for testcase in account_list[1:]: 38 | scrypt_params = testcase.get("scrypt", None) 39 | if scrypt_params is not None: 40 | scrypt_params = scrypt.ScryptParameters.from_json(scrypt_params) 41 | acc = account.Account(scrypt_parameters=scrypt_params) 42 | self.assertIsNotNone(acc) 43 | self.assertIsNotNone(acc.address) 44 | self.assertIsNotNone(acc.private_key) 45 | self.assertIsNotNone(acc.public_key) 46 | 47 | def test_new_account_from_private_key(self): 48 | for testcase in account_list: 49 | scrypt_params = testcase.get("scrypt", None) 50 | if scrypt_params is not None: 51 | scrypt_params = scrypt.ScryptParameters.from_json(scrypt_params) 52 | acc = account.Account.from_private_key( 53 | bytes.fromhex(testcase["private_key"]), scrypt_params 54 | ) 55 | self.assertEqual(testcase["address"], acc.address) 56 | self.assertEqual( 57 | testcase["encrypted_key"].encode("utf-8"), 58 | account.Account.private_key_to_nep2( 59 | bytes.fromhex(testcase["private_key"]), 60 | testcase["password"], 61 | scrypt_params, 62 | ), 63 | ) 64 | self.assertEqual(testcase["script_hash"], str(acc.script_hash)) 65 | self.assertIsNotNone(acc.public_key) 66 | 67 | def test_new_account_from_encrypted_key(self): 68 | for testcase in account_list[1:]: 69 | scrypt_params = testcase.get("scrypt", None) 70 | if scrypt_params is not None: 71 | scrypt_params = scrypt.ScryptParameters.from_json(scrypt_params) 72 | 73 | acc = account.Account.from_encrypted_key( 74 | testcase["encrypted_key"], testcase["password"], scrypt_params 75 | ) 76 | self.assertEqual(testcase["address"], acc.address) 77 | self.assertEqual( 78 | testcase["encrypted_key"].encode("utf-8"), 79 | account.Account.private_key_to_nep2( 80 | bytes.fromhex(testcase["private_key"]), 81 | testcase["password"], 82 | scrypt_params, 83 | ), 84 | ) 85 | self.assertEqual(testcase["script_hash"], str(acc.script_hash)) 86 | self.assertIsNotNone(acc.public_key) 87 | 88 | def test_new_watch_only_account(self): 89 | from neo3.core.types import UInt160 90 | 91 | for testcase in account_list[1:]: 92 | acc = account.Account.watch_only( 93 | UInt160.from_string(testcase["script_hash"]) 94 | ) 95 | self.assertEqual(testcase["address"], acc.address) 96 | self.assertIsNone(acc.private_key) 97 | self.assertEqual(testcase["script_hash"], str(acc.script_hash)) 98 | self.assertIsNone(acc.public_key) 99 | self.assertTrue(acc.is_watchonly) 100 | 101 | def test_new_watch_only_account_from_address(self): 102 | for testcase in account_list[1:]: 103 | acc = account.Account.watch_only_from_address(testcase["address"]) 104 | self.assertEqual(testcase["address"], acc.address) 105 | self.assertIsNone(acc.private_key) 106 | self.assertEqual(testcase["script_hash"], str(acc.script_hash)) 107 | self.assertIsNone(acc.public_key) 108 | 109 | def test_new_account_from_wif(self): 110 | for testcase in account_list[:1]: 111 | scrypt_params = testcase.get("scrypt", None) 112 | if scrypt_params is not None: 113 | scrypt_params = scrypt.ScryptParameters.from_json(scrypt_params) 114 | 115 | acc = account.Account.from_wif(testcase["wif_key"], scrypt_params) 116 | self.assertEqual(testcase["address"], acc.address) 117 | self.assertEqual( 118 | testcase["encrypted_key"].encode("utf-8"), 119 | account.Account.private_key_to_nep2( 120 | bytes.fromhex(testcase["private_key"]), 121 | testcase["password"], 122 | scrypt_params, 123 | ), 124 | ) 125 | self.assertEqual(testcase["script_hash"], str(acc.script_hash)) 126 | self.assertIsNotNone(acc.public_key) 127 | 128 | def test_new_account_wrong_password(self): 129 | for testcase in account_list: 130 | with self.assertRaises(ValueError) as context: 131 | account.Account.from_encrypted_key( 132 | testcase["encrypted_key"], "wrong password" 133 | ) 134 | self.assertIn("Wrong passphrase", str(context.exception)) 135 | 136 | def test_to_wif(self): 137 | wif = "L5kx9QRKG9dwzSJF72pgps1d2scJZjnECWoKuUGVsz2D1WRBEaJ7" 138 | acc = account.Account.from_wif(wif) 139 | self.assertEqual(wif, account.Account.private_key_to_wif(acc.private_key)) 140 | -------------------------------------------------------------------------------- /neo3/api/helpers/unwrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper functions to easily fetch native values from the `ResultStack` returned as response by various RPC methods 3 | such as `invoke_function()`, `invoke_script()`, `get_application_log_transaction()` and `get_application_log_block()`. 4 | 5 | Includes sanity checking. 6 | """ 7 | 8 | from __future__ import annotations 9 | from neo3.api import noderpc 10 | from neo3 import vm 11 | from neo3.core import types, cryptography 12 | 13 | 14 | def check_state_ok(res: noderpc.ExecutionResult): 15 | """ 16 | Check if the execution of the transaction finished in a success state. 17 | 18 | Raises: 19 | ValueError: if the VM state is not HALT. 20 | """ 21 | if vm.VMState.from_string(res.state) != vm.VMState.HALT: 22 | raise ValueError( 23 | f"Transaction execution failed with state {res.state} and err: {res.exception}" 24 | ) 25 | 26 | 27 | def as_bool(res: noderpc.ExecutionResult, idx: int = 0) -> bool: 28 | """ 29 | Convert the stack item at `idx` to a `bool`. 30 | 31 | Args: 32 | res: execution result. 33 | idx: the index in the result stack to fetch the stack item from. 34 | 35 | Raises: 36 | ValueError: if the index is out of range, or the value cannot be converted to a bool. 37 | """ 38 | return item(res, idx).as_bool() 39 | 40 | 41 | def as_str(res: noderpc.ExecutionResult, idx: int = 0) -> str: 42 | """ 43 | Convert the stack item at `idx` to a `str`. 44 | 45 | Args: 46 | res: execution result. 47 | idx: the index in the result stack to fetch the stack item from. 48 | 49 | Raises: 50 | ValueError: if the index is out of range, or the value cannot be converted to a `str`. 51 | """ 52 | return item(res, idx).as_str() 53 | 54 | 55 | def as_int(res: noderpc.ExecutionResult, idx: int = 0) -> int: 56 | """ 57 | Convert the stack item at `idx` to an `int`. 58 | 59 | Args: 60 | res: execution result. 61 | idx: the index in the result stack to fetch the stack item from. 62 | 63 | Raises: 64 | ValueError: if the index is out of range, or the value cannot be converted to an int. 65 | """ 66 | return item(res, idx).as_int() 67 | 68 | 69 | def as_uint160(res: noderpc.ExecutionResult, idx: int = 0) -> types.UInt160: 70 | """ 71 | Convert the stack item at `idx` to an `UInt160`. 72 | 73 | Args: 74 | res: execution result. 75 | idx: the index in the result stack to fetch the stack item from. 76 | 77 | Raises: 78 | ValueError: if the index is out of range, or the value cannot be converted to an UInt160. 79 | """ 80 | return item(res, idx).as_uint160() 81 | 82 | 83 | def as_uint256(res: noderpc.ExecutionResult, idx: int = 0) -> types.UInt256: 84 | """ 85 | Convert the stack item at `idx` to an `UInt256`. 86 | 87 | Args: 88 | res: execution result. 89 | idx: the index in the result stack to fetch the stack item from. 90 | 91 | Raises: 92 | ValueError: if the index is out of range, or the value cannot be converted to an UInt256. 93 | """ 94 | return item(res, idx).as_uint256() 95 | 96 | 97 | def as_address(res: noderpc.ExecutionResult, idx: int = 0) -> str: 98 | """ 99 | Convert the stack item at `idx` to a NEO3 address. 100 | 101 | Args: 102 | res: execution result. 103 | idx: the index in the result stack to fetch the stack item from. 104 | 105 | Raises: 106 | ValueError: if the index is out of range, or the value cannot be converted to a NEO3 address. 107 | """ 108 | return item(res, idx).as_address() 109 | 110 | 111 | def as_public_key(res: noderpc.ExecutionResult, idx: int = 0) -> cryptography.ECPoint: 112 | """ 113 | Convert the stack item at `idx` to a public key. 114 | 115 | Args: 116 | res: execution result. 117 | idx: the index in the result stack to fetch the stack item from. 118 | 119 | Raises: 120 | ValueError: if the index is out of range, or the value cannot be converted to an ECPoint. 121 | ECCException: if the resulting key is not valid on the SECP256R1 curve. 122 | """ 123 | return item(res, idx).as_public_key() 124 | 125 | 126 | def as_list(res: noderpc.ExecutionResult, idx: int = 0) -> list[noderpc.StackItem]: 127 | """ 128 | Convert the stack item at `idx` to a `list`. 129 | 130 | Args: 131 | res: execution result. 132 | idx: the index in the result stack to fetch the stack item from. 133 | 134 | Raises: 135 | ValueError: if the index is out of range, or the value cannot be converted to a list. 136 | """ 137 | return item(res, idx).as_list() 138 | 139 | 140 | def as_dict(res: noderpc.ExecutionResult, idx: int = 0) -> dict: 141 | """ 142 | Convert the stack item at `idx` to a dictionary. 143 | 144 | Args: 145 | res: execution result. 146 | idx: idx: the index in the result stack to fetch the stack item from. 147 | 148 | Raises: 149 | ValueError: if the index is out of range, or the value cannot be converted to a dict. 150 | 151 | 152 | Warning: 153 | Accepted key types on the Virtual Machine side are `int`, `str` and `bytes`. 154 | However, when data is returned there is no way to differentiate between 155 | the key being of type `str` or `bytes` as the RPC node will encode both as a `ByteString`. 156 | The dictionary returned will return such types as `bytes` and it is the user responsibility to decode 157 | them to a `str` if needed. 158 | """ 159 | return item(res, idx).as_dict() 160 | 161 | 162 | def as_none(res: noderpc.ExecutionResult, idx: int = 0) -> None: 163 | """ 164 | Convert the stack item at `idx` to `None`. 165 | 166 | Args: 167 | res: execution result. 168 | idx: the index in the result stack to fetch the stack item from. 169 | 170 | Raises: 171 | ValueError: if the index is out of range, or the value is not `None`. 172 | """ 173 | return item(res, idx).as_none() 174 | 175 | 176 | def as_bytes(res: noderpc.ExecutionResult, idx: int = 0) -> bytes: 177 | """ 178 | Convert the stack item at `idx` to `bytes`. 179 | 180 | Args: 181 | res: execution result. 182 | idx: the index in the result stack to fetch the stack item from. 183 | 184 | Raises: 185 | ValueError: if the index is out of range, or the value cannot be converted to bytes. 186 | 187 | """ 188 | return item(res, idx).as_bytes() 189 | 190 | 191 | def item(res: noderpc.ExecutionResult, idx: int = 0) -> noderpc.StackItem: 192 | """ 193 | Fetch the stack item at `idx` from the result stack. Performs basic validation and bounds checking. 194 | 195 | Args: 196 | res: execution result. 197 | idx: the index in the result stack to fetch the stack item from. 198 | """ 199 | check_state_ok(res) 200 | if idx > len(res.stack) - 1: 201 | raise ValueError("Too few result items") 202 | return res.stack[idx] 203 | -------------------------------------------------------------------------------- /tests/network/test_message.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import binascii 3 | import lz4 4 | from unittest.mock import patch, call 5 | from neo3.network import message 6 | from neo3.network.payloads import ( 7 | inventory, 8 | empty, 9 | block, 10 | transaction, 11 | ping, 12 | address, 13 | version, 14 | ) 15 | from neo3.core.types.uint import UInt256 16 | from neo3.core import serialization 17 | 18 | 19 | class NetworkMessageTestCase(unittest.TestCase): 20 | def test_create_no_payload(self): 21 | m = message.Message(message.MessageType.PING, payload=None) 22 | self.assertEqual(message.MessageType.PING, m.type) 23 | self.assertEqual(message.MessageConfig.NONE, m.config) 24 | 25 | def test_create_inv_message(self): 26 | hashes = [UInt256.zero()] 27 | inv_payload = inventory.InventoryPayload(inventory.InventoryType.BLOCK, hashes) 28 | m = message.Message(message.MessageType.INV, inv_payload) 29 | data = m.to_array() 30 | 31 | self.assertEqual(message.MessageType.INV, m.type) 32 | self.assertEqual(message.MessageConfig.NONE, m.config) 33 | self.assertIsInstance(m.payload, inventory.InventoryPayload) 34 | 35 | """ 36 | Taken from constructing the same object in C# 37 | 38 | UInt256[] hashes = { UInt256.Zero }; 39 | var inv_payload = InvPayload.Create(InventoryType.Block, hashes); 40 | ISerializable message = Message.Create(MessageCommand.Inv, inv_payload); 41 | 42 | using (MemoryStream ms = new MemoryStream()) 43 | using (BinaryWriter writer = new BinaryWriter(ms)) 44 | { 45 | message.Serialize(writer); 46 | writer.Flush(); 47 | byte[] data = ms.ToArray(); 48 | Console.WriteLine($"b\'{BitConverter.ToString(data).Replace("-","")}\'"); 49 | } 50 | 51 | """ 52 | expected_data = binascii.unhexlify( 53 | b"0027222C010000000000000000000000000000000000000000000000000000000000000000" 54 | ) 55 | self.assertEqual(expected_data, data) 56 | 57 | def test_create_compressed_inv_message(self): 58 | hashes = [UInt256.zero(), UInt256.zero(), UInt256.zero(), UInt256.zero()] 59 | inv_payload = inventory.InventoryPayload(inventory.InventoryType.BLOCK, hashes) 60 | m = message.Message(message.MessageType.INV, inv_payload) 61 | data = m.to_array() # triggers payload compression 62 | 63 | self.assertEqual(message.MessageType.INV, m.type) 64 | self.assertEqual(message.MessageConfig.COMPRESSED, m.config) 65 | self.assertIsInstance(m.payload, inventory.InventoryPayload) 66 | 67 | """ 68 | Data created in the same fashion as how it's done in test_create_inv_message() 69 | The deviation is `hashes` now contains 4 x UInt256.zero() 70 | """ 71 | 72 | expected_data = binascii.unhexlify(b"012711820000003F2C0400010067500000000000") 73 | self.assertEqual(expected_data, data) 74 | 75 | def test_inv_message_deserialization(self): 76 | # see test_create_compressed_inv_message() how it was obtained 77 | raw_data = binascii.unhexlify(b"012711820000003F2C0400010067500000000000") 78 | m = message.Message.deserialize_from_bytes(raw_data) 79 | self.assertIsInstance(m.payload, inventory.InventoryPayload) 80 | self.assertEqual(132, len(m)) 81 | 82 | def test_deserialization_with_not_enough_data(self): 83 | with self.assertRaises(ValueError) as context: 84 | m = message.Message.deserialize_from_bytes(bytearray(2)) 85 | self.assertEqual( 86 | str(context.exception), "Could not read byte from empty stream" 87 | ) 88 | 89 | def test_deserialization_without_payload(self): 90 | # some message types like PING/PONG have no payload 91 | m = message.Message(message.MessageType.PING) 92 | data = m.to_array() 93 | m2 = message.Message.deserialize_from_bytes(data) 94 | self.assertEqual(message.MessageType.PING, m2.type) 95 | self.assertEqual(0, len(m2.payload)) 96 | 97 | def test_deserialization_from_stream(self): 98 | # see test_create_compressed_inv_message() how it was obtained 99 | raw_data = binascii.unhexlify(b"012711820000003F2C0400010067500000000000") 100 | with serialization.BinaryReader(raw_data) as br: 101 | m = message.Message(message.MessageType.DEFAULT) 102 | m.deserialize(br) 103 | self.assertEqual(m.type, message.MessageType.INV) 104 | self.assertEqual(m.payload.type, inventory.InventoryType.BLOCK) 105 | 106 | def test_deserialization_with_unsupported_payload_type(self): 107 | hashes = [UInt256.zero()] 108 | inv_payload = inventory.InventoryPayload(inventory.InventoryType.BLOCK, hashes) 109 | m = message.Message(message.MessageType.ALERT, inv_payload) 110 | 111 | m2 = message.Message.deserialize_from_bytes(m.to_array()) 112 | self.assertIsInstance(m2.payload, empty.EmptyPayload) 113 | 114 | def test_deserialization_erroneous_compressed_data(self): 115 | # see test_create_compressed_inv_message() how it was obtained 116 | raw_data = binascii.unhexlify(b"01270D3F020400010067500000000000") 117 | 118 | with patch("lz4.block.decompress") as lz4_mock: 119 | with self.assertRaises(ValueError) as context: 120 | lz4_mock.side_effect = lz4.block.LZ4BlockError() 121 | m = message.Message.deserialize_from_bytes(raw_data) 122 | self.assertEqual( 123 | "Invalid payload data - decompress failed", str(context.exception) 124 | ) 125 | 126 | def test_payload_from_data(self): 127 | with patch("neo3.core.serialization.BinaryReader") as br: 128 | reader = br.return_value.__enter__.return_value 129 | message.Message._payload_from_data(message.MessageType.INV, b"") 130 | message.Message._payload_from_data(message.MessageType.GETBLOCKBYINDEX, b"") 131 | message.Message._payload_from_data(message.MessageType.VERSION, b"") 132 | message.Message._payload_from_data(message.MessageType.VERACK, b"") 133 | message.Message._payload_from_data(message.MessageType.BLOCK, b"") 134 | message.Message._payload_from_data(message.MessageType.HEADERS, b"") 135 | message.Message._payload_from_data(message.MessageType.PING, b"") 136 | message.Message._payload_from_data(message.MessageType.PONG, b"") 137 | message.Message._payload_from_data(message.MessageType.ADDR, b"") 138 | message.Message._payload_from_data(message.MessageType.TRANSACTION, b"") 139 | 140 | calls = [ 141 | call(inventory.InventoryPayload), 142 | call(block.GetBlockByIndexPayload), 143 | call(version.VersionPayload), 144 | call(empty.EmptyPayload), 145 | call(block.Block), 146 | call(block.HeadersPayload), 147 | call(ping.PingPayload), 148 | call(ping.PingPayload), 149 | call(address.AddrPayload), 150 | call(transaction.Transaction), 151 | ] 152 | reader.read_serializable.assert_has_calls(calls, any_order=False) 153 | --------------------------------------------------------------------------------