├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------