Python 3 (Python 3.6+
) library to assist smooth development on VeChain for developers and hobbyists.
... and will always be updated with the newest features on VeChain.
Read our documentation on ReadTheDocs.
pip3 install thor-devkit -U
Caveat: Bip32 depends on the ripemd160 hash library, which should be present on your system.
Supported extras:
test
: install developer requirements (pip install thor-devkit[test]
).docs
: installsphinx
-related packages (pip install thor-devkit[test,docs]
).
Many modules and classes have validate
and is_valid
methods. They perform exactly the same validation, but the former raises exceptions for malformed inputs (returns True
for valid), while the latter returns False
for invalid and True
for valid inputs.
>>> from thor_devkit import cry
>>> from thor_devkit.cry import secp256k1
>>> private_key = secp256k1.generate_private_key()
>>> public_key = secp256k1.derive_public_key(private_key)
>>> _address_bytes = cry.public_key_to_address(public_key)
>>> address = '0x' + _address_bytes.hex()
>>> address # doctest:+SKIP
'0x86d8cd908e43bc0076bc99e19e1a3c6221436ad0'
>>> cry.is_address(address) # Is it a valid address?
True
>>> cry.to_checksum_address(address) # doctest:+SKIP
'0x86d8CD908e43BC0076Bc99e19E1a3c6221436aD0'
>>> from thor_devkit.cry import secp256k1, keccak256
>>> private_key = bytes.fromhex(
... '7582be841ca040aa940fff6c05773129e135623e41acce3e0b8ba520dc1ae26a'
... ) # bytes
>>> msg_hash, _ = keccak256([b'hello world']) # bytes
Sign the message hash:
>>> signature = secp256k1.sign(msg_hash, private_key) # bytes
Recover public key from given message hash and signature:
>>> public_key = secp256k1.recover(msg_hash, signature) # bytes
>>> from thor_devkit.cry import mnemonic
>>> words = mnemonic.generate()
>>> words # doctest:+SKIP
['fashion', 'reduce', 'resource', 'ordinary', 'seek', 'kite', 'space', 'marriage', 'cube', 'detail', 'bundle', 'latin']
>>> assert mnemonic.is_valid(words)
Quickly get a Bip32 master seed for HD wallets. See below "HD Wallet".
>>> seed = mnemonic.derive_seed(words)
Quickly get a private key:
>>> private_key = mnemonic.derive_private_key(words, 0)
Hierarchical Deterministic Wallets.
>>> from thor_devkit.cry import hdnode, HDNode
Construct an HD node from words (recommended):
>>> words = 'ignore empty bird silly journey junior ripple have guard waste between tenant'.split()
>>> hd_node = HDNode.from_mnemonic(
... words,
... init_path=hdnode.VET_EXTERNAL_PATH,
... ) # VET wallet, you can input other string values to generate BTC/ETH/... wallets.
Or, construct HD node from seed (advanced):
>>> seed = '28bc19620b4fbb1f8892b9607f6e406fcd8226a0d6dc167ff677d122a1a64ef936101a644e6b447fd495677f68215d8522c893100d9010668614a68b3c7bb49f'
>>> hd_node = HDNode.from_seed(
... bytes.fromhex(seed),
... init_path=hdnode.VET_EXTERNAL_PATH,
... ) # VET wallet, you can input other string values to generate BTC/ETH/... wallets.
Access the HD node's properties:
>>> priv = hd_node.private_key
>>> pub = hd_node.public_key
>>> addr = hd_node.address
>>> cc = hd_node.chain_code
Or, construct HD node from a given public key (advanced)
Notice: This HD node cannot derive child HD node with "private key".
>>> hd_node = HDNode.from_public_key(pub, cc)
Or, construct HD node from a given private key (advanced):
>>> hd_node = HDNode.from_private_key(priv, cc)
Let it derive further child HD nodes:
>>> for i in range(3):
... print('addr:', '0x' + hd_node.derive(i).address.hex())
... print('priv:', hd_node.derive(i).private_key.hex())
addr: 0x339fb3c438606519e2c75bbf531fb43a0f449a70
priv: 27196338e7d0b5e7bf1be1c0327c53a244a18ef0b102976980e341500f492425
addr: 0x5677099d06bc72f9da1113afa5e022feec424c8e
priv: cf44074ec3bf912d2a46b7c84fa6eb745652c9c74e674c3760dc7af07fc98b62
addr: 0x86231b5cdcbfe751b9ddcd4bd981fc0a48afe921
priv: 2ca054a50b53299ea3949f5362ee1d1cfe6252fbe30bea3651774790983e9348
>>> from thor_devkit.cry import keystore
>>> ks = {
... "version": 3,
... "id": "f437ebb1-5b0d-4780-ae9e-8640178ffd77",
... "address": "dc6fa3ec1f3fde763f4d59230ed303f854968d26",
... "crypto":
... {
... "kdf": "scrypt",
... "kdfparams": {
... "dklen": 32,
... "salt": "b57682e5468934be81217ad5b14ca74dab2b42c2476864592c9f3b370c09460a",
... "n": 262144,
... "r": 8,
... "p": 1
... },
... "cipher": "aes-128-ctr",
... "ciphertext": "88cb876f9c0355a89cad88ee7a17a2179700bc4306eaf78fa67320efbb4c7e31",
... "cipherparams": {
... "iv": "de5c0c09c882b3f679876b22b6c5af21"
... },
... "mac": "8426e8a1e151b28f694849cb31f64cbc9ae3e278d02716cf5b61d7ddd3f6e728"
... }
... }
>>> password = b'123456'
Decrypt:
>>> private_key = keystore.decrypt(ks, password)
Encrypt:
>>> ks_backup = keystore.encrypt(private_key, password)
>>> from thor_devkit.cry import blake2b256, keccak256
>>> result, length = blake2b256([b'hello world'])
>>> result2, length = blake2b256([b'hello', b' world'])
>>> assert result == result2
>>> result.hex()
'256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610'
>>> result, length = keccak256([b'hello world'])
>>> result2, length = keccak256([b'hello', b' world'])
>>> assert result == result2
>>> result.hex()
'47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad'
>>> from thor_devkit import Bloom
Create a bloom filter that can store 100 items:
>>> _k = Bloom.estimate_k(100)
>>> _k
14
>>> b = Bloom(_k)
Add an item to the bloom filter:
>>> b.add(b'hello world')
True
Verify:
>>> assert b'hello world' in b
>>> assert b'bye bye blue bird' not in b
See the VeChain net REST API details (e.g. post transaction): testnet, mainnet
>>> from thor_devkit import cry
>>> from thor_devkit.transaction import Transaction
>>> body = {
... "chainTag": int('0x4a', 16), # 0x4a/0x27/0xa4
... "blockRef": '0x00000000aabbccdd',
... "expiration": 32,
... "clauses": [
... {
... "to": '0x7567d83b7b8d80addcb281a71d54fc7b3364ffed',
... "value": 10000,
... "data": '0x000000606060'
... },
... {
... "to": '0x7567d83b7b8d80addcb281a71d54fc7b3364ffed',
... "value": 20000,
... "data": '0x000000606060'
... }
... ],
... "gasPriceCoef": 128,
... "gas": 21000,
... "dependsOn": None,
... "nonce": 12345678
... }
Construct an unsigned transaction:
>>> tx = Transaction(body)
Access its properties:
>>> assert tx.get_signing_hash() == cry.blake2b256([tx.encode()])[0]
>>> assert tx.signature is None
>>> assert tx.origin is None
>>> assert tx.intrinsic_gas == 37432 # estimate the gas this tx gonna cost.
Sign the transaction with a private key:
>>> priv_key = bytes.fromhex(
... '7582be841ca040aa940fff6c05773129e135623e41acce3e0b8ba520dc1ae26a'
... )
>>> message_hash = tx.get_signing_hash()
>>> signature = cry.secp256k1.sign(message_hash, priv_key)
>>> tx.signature = signature
>>> tx.origin
'0xd989829d88b0ed1b06edf5c50174ecfa64f14a64'
>>> tx.id
'0xf2c89da3d85952e99961d409abb0b2afb7fa266acc5ed23fb5d23a5d3db395d7'
Tx encoded into bytes, ready to be sent out:
>>> "0x" + tx.encode().hex()
'0xf8974a84aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e208600000060606081808252088083bc614ec0b8419d500064647f37254e22b3ffac04bb5ccff5d91b6d6103a53baeedac17708b8817c6137e1efe3472f3b6fd8af258c2c3945b742c58ba49de2796c8bb54a0bb0601'
See VIP-191 for reference.
>>> from thor_devkit.cry import secp256k1
>>> from thor_devkit.transaction import Transaction
>>> delegated_body = {
... "chainTag": 1,
... "blockRef": '0x00000000aabbccdd',
... "expiration": 32,
... "clauses": [
... {
... "to": '0x7567d83b7b8d80addcb281a71d54fc7b3364ffed',
... "value": 10000,
... "data": '0x000000606060'
... },
... {
... "to": '0x7567d83b7b8d80addcb281a71d54fc7b3364ffed',
... "value": 20000,
... "data": '0x000000606060'
... }
... ],
... "gasPriceCoef": 128,
... "gas": 21000,
... "dependsOn": None,
... "nonce": 12345678,
... "reserved": {
... "features": 1
... }
... }
>>> delegated_tx = Transaction(delegated_body)
Indicate it is a delegated Transaction using VIP-191.
>>> assert delegated_tx.is_delegated
Sender:
>>> addr_1 = '0xf9ea4ba688d55cc7f0eae0dd62f8271b744637bf'
>>> priv_1 = bytes.fromhex('58e444d4fe08b0f4d9d86ec42f26cf15072af3ddc29a78e33b0ceaaa292bcf6b')
Gas Payer:
>>> addr_2 = '0x34b7538c2a7c213dd34c3ecc0098097d03a94dcb'
>>> priv_2 = bytes.fromhex('0bfd6a863f347f4ef2cf2d09c3db7b343d84bb3e6fc8c201afee62de6381dc65')
>>> h = delegated_tx.get_signing_hash() # Sender hash to be signed.
>>> dh = delegated_tx.get_signing_hash(addr_1) # Gas Payer hash to be signed.
Sender signs the hash.
Gas payer signs the hash.
Concatenate two parts to forge a legal signature:
>>> sig = secp256k1.sign(h, priv_1) + secp256k1.sign(dh, priv_2)
>>> delegated_tx.signature = sig
>>> assert delegated_tx.origin == addr_1
>>> assert delegated_tx.delegator == addr_2
https://github.com/vechain/VIPs/blob/master/vips/VIP-192.md
>>> from thor_devkit.cry import secp256k1
>>> from thor_devkit.certificate import Certificate
My private key and address:
>>> address = '0xd989829d88b0ed1b06edf5c50174ecfa64f14a64'
>>> private_key = bytes.fromhex(
... '7582be841ca040aa940fff6c05773129e135623e41acce3e0b8ba520dc1ae26a'
... )
My certificate data:
>>> cert_dict = {
... 'purpose': 'identification',
... 'payload': {
... 'type': 'text',
... 'content': 'fyi',
... },
... 'domain': 'localhost',
... 'timestamp': 1545035330,
... 'signer': address,
... }
Construct a certificate without signature:
>>> cert = Certificate(**cert_dict)
Sign the certificate with my private key:
>>> sig_bytes = secp256k1.sign(
... cry.blake2b256([
... cert.encode().encode() # encode to string, then string to bytes.
... ])[0],
... private_key
... )
>>> signature = '0x' + sig_bytes.hex()
Construct a certificate with signature:
>>> cert_dict['signature'] = signature
>>> cert2 = Certificate(**cert_dict)
Verify, if verify failed it will throw Exceptions.
>>> cert2.verify()
True
Or get boolean validness:
>>> assert cert2.is_valid()
Encode function name and parameters according to ABI.
>>> from pprint import pprint
>>> from thor_devkit.abi import Function
>>> abi_dict = {
... "inputs": [
... {
... "name": "a1",
... "type": "uint256"
... },
... {
... "name": "a2",
... "type": "string"
... }
... ],
... "name": "f1",
... "outputs": [
... {
... "name": "r1",
... "type": "address"
... },
... {
... "name": "r2",
... "type": "bytes"
... }
... ],
... "stateMutability": "nonpayable",
... "type": "function"
... }
Create a function instance of the ABI:
>>> f = Function(abi_dict)
Get function selector:
>>> f.selector.hex()
'27fcbb2f'
Encode the function input parameters:
>>> f.encode([1, 'foo'], to_hex=True)
'0x27fcbb2f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000'
Decode function return result according to ABI:
>>> data = '000000000000000000000000abc000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000'
>>> result = f.decode(bytes.fromhex(data))
>>> result.to_dict() # Use dictionary form
{'r1': '0xabc0000000000000000000000000000000000001', 'r2': b'foo'}
>>> assert result[0] == '0xabc0000000000000000000000000000000000001' # Access by index
>>> assert result.r2 == b'foo' # Or by name
Create function from solidity code:
>>> contract = '''
... contract A {
... function f(uint x) public returns(bool) {}
... }
... '''
>>> func = Function.from_solidity(text=contract)
>>> pprint(func._definition)
{'inputs': [{'internalType': 'uint256', 'name': 'x', 'type': 'uint256'}],
'name': 'f',
'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}],
'stateMutability': 'nonpayable',
'type': 'function'}
Decode logs according to data and topics.
>>> from thor_devkit.abi import Event
>>> data = {
... "anonymous": True,
... "inputs": [
... {
... "indexed": True,
... "name": "a1",
... "type": "uint256"
... },
... {
... "indexed": False,
... "name": "a2",
... "type": "string"
... }
... ],
... "name": "E2",
... "type": "event"
... }
>>> event = Event(data)
Decode data in hex format:
>>> result = event.decode(
... data=bytes.fromhex('00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000'),
... topics=[
... bytes.fromhex('0000000000000000000000000000000000000000000000000000000000000001')
... ]
... )
>>> result.to_dict()
{'a1': 1, 'a2': 'foo'}
>>> result[0]
1
>>> result.a2
'foo'
Create event from solidity code:
>>> contract = '''
... contract A {
... event E(uint indexed a1, string a2) anonymous;
... }
... '''
>>> func = Event.from_solidity(text=contract)
>>> pprint(func._definition)
{'anonymous': True,
'inputs': [{'indexed': True,
'internalType': 'uint256',
'name': 'a1',
'type': 'uint256'},
{'indexed': False,
'internalType': 'string',
'name': 'a2',
'type': 'string'}],
'name': 'E',
'type': 'event'}
.
├── LICENSE
├── README.md
├── requirements.txt
└── thor_devkit
├── __init__.py
├── abi.py
├── bloom.py
├── certificate.py
├── cry
│ ├── __init__.py
│ ├── address.py
│ ├── blake2b.py
│ ├── hdnode.py
│ ├── keccak.py
│ ├── keystore.py
│ ├── mnemonic.py
│ ├── secp256k1.py
│ └── utils.py
├── exceptions.py
├── rlp.py
├── transaction.py
└── validation.py
You can setup local version with
# Create new environment (you can use other name or reuse existing one)
python -m venv .env
. .env/bin/activate
# Editable install
pip install -e .[test]
# Install git hooks
pre-commit install
Or with help of Makefile
:
# install dependencies
make install
# test code
make test
All project tests are based on pytest
. You can use tox
(configuration resides in pyproject.toml
) to test against multiple python
versions (it will also happen in CI, when you submit a PR).
You can run pre-commit
hooks without commiting with
pre-commit run --all-files
We enforce strict coding style: black
is a part of pre-commit
setup, also it
includes flake8
for additional validation.
Name | Bytes | Description |
---|---|---|
private key | 32 | random number |
public key | 65 | uncompressed, starts with "04" |
address | 20 | derived from public key |
keccak256 | 32 | hash |
blake2b256 | 32 | hash |
message hash | 32 | hash of a message |
signature | 65 | signing result, last bit as recovery parameter |
seed | 64 | used to derive bip32 master key |
In version 2.0.0
a few backwards incompatible changes were introduced.
- Transaction methods
get_delegator
,get_intrinsic_gas
,get_signature
,set_signature
,get_origin
are deprecated in favour of properties.Transaction.get_body
is replaced withTransaction.body
property andTransaction.copy_body()
method.Transaction.is_delegated
is now a property instead of regular method. - Certificate
__init__
method performs basic validation, so some invalid signatures will be rejected during instantiation and not inverify
method. Module-level functionsencode
andverify
are deprecated in favour ofCertificate
methods. Bloom
filter has__contains__
now (so you can useelement in bloom_filter
).- ABI module has changed significantly. Function and Event can now be instantiated from solidity code with
from_solidity
method. New methods were introduced for encoding and decoding.decode
results are now customnamedtuple
's instead of strange dictionary format, see docs for reference.Event.get_signature
andFunction.get_selector
are deprecated in favour ofEvent.signature
andFunction.selector
properties. - RLP module functions
pack
andunpack
are now deprecated, useBaseWrapper
orScalarKind
serialize
anddeserialize
methods instead. - Functions with odd names
derive_publicKey
andgenerate_privateKey
are deprecated in favour ofderive_public_key
andgenerate_private_key
. mnemonic.validate
is deprecated, usemnemonic.is_valid
instead.keystore.well_formed
is deprecated, usekeystore.validate
andkeystore.is_valid
instead.HDNode
uses properties instead of methods for simple attributes:private_key
,public_key
,chain_code
,address
,fingerprint
.