Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
77216001e5 | |||
20a2f7a672 | |||
41ffccfca3 | |||
8b60454fd0 | |||
7852f00cb9 | |||
ff5550db95 | |||
fbded540e2 | |||
561e03ab3c | |||
99190e910b | |||
9033c8df3d | |||
dae2168d8f | |||
5afd7e596f | |||
665f070028 | |||
ba7f44e484 |
|
@ -1,3 +1,10 @@
|
||||||
|
# Conventions
|
||||||
|
|
||||||
|
When contributing code to this project, you MUST follow the requirements
|
||||||
|
specified here.
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
When contributing code to this project, you MUST follow these principles:
|
When contributing code to this project, you MUST follow these principles:
|
||||||
|
|
||||||
- Code should be easy to read and understand.
|
- Code should be easy to read and understand.
|
||||||
|
@ -13,3 +20,13 @@ When contributing code to this project, you MUST follow these principles:
|
||||||
- Documentation should document semantics, not syntax.
|
- Documentation should document semantics, not syntax.
|
||||||
- Prefer importing modules, not individual items from modules.
|
- Prefer importing modules, not individual items from modules.
|
||||||
- Do not use f-strings in logging statements.
|
- Do not use f-strings in logging statements.
|
||||||
|
- Loop variables and walrus-expression-variables should be deleted when
|
||||||
|
unneeded to keep scope clean, and to avoid accidental use.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
When contributing test to this project, you MUST follow these principles:
|
||||||
|
|
||||||
|
- Do not use any testing libraries other than `pytest`.
|
||||||
|
- Mocking is the root of all evil. Writing your own stubs is much more
|
||||||
|
preferable.
|
||||||
|
|
11
README.md
11
README.md
|
@ -17,7 +17,16 @@ Group ApS.
|
||||||
|
|
||||||
This project requires [Python](https://www.python.org/) 3.8 or newer.
|
This project requires [Python](https://www.python.org/) 3.8 or newer.
|
||||||
|
|
||||||
This project does not have any library requirements 😎
|
All required libraries can be installed easily using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Full list of requirements:
|
||||||
|
- [coincurve](https://pypi.org/project/coincurve/)
|
||||||
|
- [pbcabi](https://gitfub.space/Jmaa/pbcabi)
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,32 @@
|
||||||
""" # Partisia Blockchain Client
|
"""# Partisia Blockchain Client
|
||||||
|
|
||||||
Unofficial library for reading contract states from Partisia Blockchain.
|
Unofficial library for reading contract states from Partisia Blockchain.
|
||||||
|
|
||||||
This library is not officially associated with Partisia Blockchain nor Partisia
|
This library is not officially associated with Partisia Blockchain nor Partisia
|
||||||
Group ApS.
|
Group ApS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import base64
|
|
||||||
import email.utils
|
import email.utils
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import ecdsa
|
|
||||||
import hashlib
|
|
||||||
import pbcabi
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Mapping
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import pbcabi
|
||||||
import requests
|
import requests
|
||||||
from frozendict import frozendict
|
from frozendict import frozendict
|
||||||
|
|
||||||
from .pbc_types import (Address, Signature, HashSha256, Transaction,
|
from ._version import __version__ # noqa: F401
|
||||||
SignedTransaction, SignedTransactionInnerPart)
|
from .crypto import SenderAuthentication, sign_transaction
|
||||||
|
from .pbc_types import (
|
||||||
|
Address,
|
||||||
|
SignedTransaction,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -40,6 +43,8 @@ URL_NONCE = 'https://{hostname}/{shard}blockchain/account/{address}'
|
||||||
|
|
||||||
MPC_DECIMALS = 10000
|
MPC_DECIMALS = 10000
|
||||||
|
|
||||||
|
TRANSACTION_VALIDITY_DURATION = 60
|
||||||
|
|
||||||
|
|
||||||
def shard_id_for_address(address: str | Address) -> str:
|
def shard_id_for_address(address: str | Address) -> str:
|
||||||
address = str(address)
|
address = str(address)
|
||||||
|
@ -47,7 +52,7 @@ def shard_id_for_address(address: str | Address) -> str:
|
||||||
if address is None:
|
if address is None:
|
||||||
msg = 'Address must not be None'
|
msg = 'Address must not be None'
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
if address.endswith('a'):
|
if address.endswith(('a', 'd', 'c')):
|
||||||
return 'shards/Shard0/'
|
return 'shards/Shard0/'
|
||||||
if address.endswith('2'):
|
if address.endswith('2'):
|
||||||
return 'shards/Shard1/'
|
return 'shards/Shard1/'
|
||||||
|
@ -60,49 +65,6 @@ class Balances:
|
||||||
mpc: Decimal
|
mpc: Decimal
|
||||||
byoc: Mapping[str, Decimal]
|
byoc: Mapping[str, Decimal]
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
|
||||||
class SenderAuthentication:
|
|
||||||
secret_key: str
|
|
||||||
|
|
||||||
def sender_address(self) -> Address:
|
|
||||||
derp = self._signing_key().verifying_key.to_string()
|
|
||||||
hashed = HashSha256.of_bytes(derp)
|
|
||||||
print(derp.hex())
|
|
||||||
print(derp)
|
|
||||||
print(hashed)
|
|
||||||
return Address(b'\0' + hashed._bytes[-20:])
|
|
||||||
|
|
||||||
def sign_hash(self, _hash: HashSha256) -> Signature:
|
|
||||||
return Signature(self._signing_key().sign(_hash._bytes))
|
|
||||||
|
|
||||||
def _signing_key(self):
|
|
||||||
return ecdsa.SigningKey.from_string(bytearray.fromhex(self.secret_key), curve=ecdsa.SECP256k1)
|
|
||||||
|
|
||||||
TRANSACTION_VALIDITY_DURATION = 60
|
|
||||||
|
|
||||||
def sign_transaction(
|
|
||||||
sender_authentication: SenderAuthentication,
|
|
||||||
nonce: int,
|
|
||||||
gas_cost: int,
|
|
||||||
chain_id: str,
|
|
||||||
contract_address: Address | str,
|
|
||||||
transaction_rpc: bytes,
|
|
||||||
) -> SignedTransaction:
|
|
||||||
sender = sender_authentication.sender_address
|
|
||||||
valid_to_time: int = int(time.time() + TRANSACTION_VALIDITY_DURATION) * 1000
|
|
||||||
|
|
||||||
inner: SignedTransactionInnerPart = SignedTransactionInnerPart(nonce,
|
|
||||||
valid_to_time,
|
|
||||||
gas_cost,
|
|
||||||
Transaction(Address.from_string(contract_address),
|
|
||||||
transaction_rpc))
|
|
||||||
|
|
||||||
transaction_hash_bytes = inner.rpc_serialize() + chain_id.encode('utf8')
|
|
||||||
|
|
||||||
transaction_hash: HashSha256 = HashSha256.of_bytes(transaction_hash_bytes)
|
|
||||||
signature : Signature = sender_authentication.sign_hash(transaction_hash)
|
|
||||||
return SignedTransaction(inner, signature, transaction_hash)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class PbcClient:
|
class PbcClient:
|
||||||
|
@ -117,29 +79,45 @@ class PbcClient:
|
||||||
if self.sender_authentication is not None:
|
if self.sender_authentication is not None:
|
||||||
assert isinstance(self.sender_authentication, SenderAuthentication)
|
assert isinstance(self.sender_authentication, SenderAuthentication)
|
||||||
|
|
||||||
|
|
||||||
def send_transaction(self, contract_address: str, rpc: bytes, gas_cost: int):
|
def send_transaction(self, contract_address: str, rpc: bytes, gas_cost: int):
|
||||||
if self.sender_authentication is None:
|
if self.sender_authentication is None:
|
||||||
msg = "PbcClient.sender_authentication required for send_transaction"
|
msg = 'PbcClient.sender_authentication required for send_transaction'
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
|
|
||||||
signed_transaction = sign_transaction(self.sender_authentication,
|
valid_to_time: int = int(time.time() + TRANSACTION_VALIDITY_DURATION) * 1000
|
||||||
self.get_sender_authentication_nonce(),
|
signed_transaction = sign_transaction(
|
||||||
gas_cost,
|
self.sender_authentication,
|
||||||
self.get_chain_id(),
|
self.get_sender_authentication_nonce(),
|
||||||
contract_address, rpc)
|
valid_to_time,
|
||||||
|
gas_cost,
|
||||||
|
self.get_chain_id(),
|
||||||
|
contract_address,
|
||||||
|
rpc,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(signed_transaction.inner.transaction.rpc_serialize()) == 25
|
||||||
|
assert len(signed_transaction.signature.rpc_serialize()) == 65
|
||||||
|
assert len(signed_transaction.inner.rpc_serialize()) == 49
|
||||||
|
assert len(signed_transaction.rpc_serialize()) == 114
|
||||||
|
|
||||||
return self.send_signed_transaction(signed_transaction)
|
return self.send_signed_transaction(signed_transaction)
|
||||||
|
|
||||||
def send_signed_transaction(self, signed_transaction: SignedTransaction):
|
def send_signed_transaction(self, signed_transaction: SignedTransaction):
|
||||||
url = URL_TRANSACTION.format(
|
url = URL_TRANSACTION.format(
|
||||||
hostname=self.hostname,
|
hostname=self.hostname,
|
||||||
shard=shard_id_for_address(signed_transaction.inner.transaction.contract_address),
|
shard=shard_id_for_address(
|
||||||
|
signed_transaction.inner.transaction.contract_address,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction_payload: str = base64.b64encode(signed_transaction.rpc_serialize()).decode('utf8')
|
transaction_payload: str = base64.b64encode(
|
||||||
self._get_json(url, data = {'transactionPayload':transaction_payload }, method = 'PUT')
|
signed_transaction.rpc_serialize(),
|
||||||
|
).decode('utf8')
|
||||||
|
self._get_json(
|
||||||
|
url,
|
||||||
|
data={'transactionPayload': transaction_payload},
|
||||||
|
method='PUT',
|
||||||
|
)
|
||||||
|
|
||||||
def _get_json(
|
def _get_json(
|
||||||
self,
|
self,
|
||||||
|
@ -163,6 +141,8 @@ class PbcClient:
|
||||||
'date',
|
'date',
|
||||||
)
|
)
|
||||||
date = email.utils.parsedate_to_datetime(date_text)
|
date = email.utils.parsedate_to_datetime(date_text)
|
||||||
|
if response.text == '':
|
||||||
|
return (None, date)
|
||||||
json_data = response.json()
|
json_data = response.json()
|
||||||
if json_data is None:
|
if json_data is None:
|
||||||
msg = 'No result data for ' + url
|
msg = 'No result data for ' + url
|
||||||
|
@ -226,7 +206,6 @@ class PbcClient:
|
||||||
)
|
)
|
||||||
return self._get_json(url, method='GET')[0]['nonce']
|
return self._get_json(url, method='GET')[0]['nonce']
|
||||||
|
|
||||||
|
|
||||||
def get_contract_state(self, address: str) -> tuple[dict, datetime.datetime]:
|
def get_contract_state(self, address: str) -> tuple[dict, datetime.datetime]:
|
||||||
# TODO: Rename to get_contract_state_json
|
# TODO: Rename to get_contract_state_json
|
||||||
url = URL_CONTRACT_STATE.format(
|
url = URL_CONTRACT_STATE.format(
|
||||||
|
@ -244,11 +223,15 @@ class PbcClient:
|
||||||
file_abi = self.get_contract_abi(address)
|
file_abi = self.get_contract_abi(address)
|
||||||
state, server_time = self.get_contract_state(address)
|
state, server_time = self.get_contract_state(address)
|
||||||
state_bytes = base64.b64decode(state['state']['data'])
|
state_bytes = base64.b64decode(state['state']['data'])
|
||||||
state_deserialized = file_abi.contract.read_state(state_bytes)
|
state_deserialized = file_abi.contract.read_state(state_bytes)
|
||||||
|
|
||||||
return state_deserialized, server_time
|
return state_deserialized, server_time
|
||||||
|
|
||||||
def get_typed_contract_avl_tree(self, address: str, avl_tree_id: pbcabi.model.AvlTreeId) -> tuple[dict, datetime.datetime]:
|
def get_typed_contract_avl_tree(
|
||||||
|
self,
|
||||||
|
address: str,
|
||||||
|
avl_tree_id: pbcabi.model.AvlTreeId,
|
||||||
|
) -> tuple[dict, datetime.datetime]:
|
||||||
file_abi = self.get_contract_abi(address)
|
file_abi = self.get_contract_abi(address)
|
||||||
state, server_time = self.get_contract_state(address)
|
state, server_time = self.get_contract_state(address)
|
||||||
for avl_tree in state['avlTrees']:
|
for avl_tree in state['avlTrees']:
|
||||||
|
@ -260,8 +243,14 @@ class PbcClient:
|
||||||
key_bytes = base64.b64decode(key_and_value['key']['data']['data'])
|
key_bytes = base64.b64decode(key_and_value['key']['data']['data'])
|
||||||
value_bytes = base64.b64decode(key_and_value['value']['data'])
|
value_bytes = base64.b64decode(key_and_value['value']['data'])
|
||||||
|
|
||||||
key = file_abi.contract.read_state(key_bytes, avl_tree_id.type_spec.type_key)
|
key = file_abi.contract.read_state(
|
||||||
value = file_abi.contract.read_state(value_bytes, avl_tree_id.type_spec.type_value)
|
key_bytes,
|
||||||
|
avl_tree_id.type_spec.type_key,
|
||||||
|
)
|
||||||
|
value = file_abi.contract.read_state(
|
||||||
|
value_bytes,
|
||||||
|
avl_tree_id.type_spec.type_value,
|
||||||
|
)
|
||||||
|
|
||||||
data[key] = value
|
data[key] = value
|
||||||
|
|
||||||
|
@ -269,7 +258,6 @@ class PbcClient:
|
||||||
|
|
||||||
return data, server_time
|
return data, server_time
|
||||||
|
|
||||||
|
|
||||||
def get_contract_abi(self, address: str) -> pbcabi.model.FileAbi:
|
def get_contract_abi(self, address: str) -> pbcabi.model.FileAbi:
|
||||||
url = URL_CONTRACT_STATE.format(
|
url = URL_CONTRACT_STATE.format(
|
||||||
hostname=self.hostname,
|
hostname=self.hostname,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '0.1.3'
|
__version__ = '0.1.5'
|
||||||
|
|
84
pbc_client/crypto.py
Normal file
84
pbc_client/crypto.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
import coincurve
|
||||||
|
|
||||||
|
from .pbc_types import (
|
||||||
|
Address,
|
||||||
|
HashSha256,
|
||||||
|
Signature,
|
||||||
|
SignedTransaction,
|
||||||
|
SignedTransactionInnerPart,
|
||||||
|
Transaction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_recovery_id(
|
||||||
|
der_sig: bytes,
|
||||||
|
message_hash: HashSha256,
|
||||||
|
expected_public_key: coincurve.PublicKey,
|
||||||
|
) -> int:
|
||||||
|
r, s = coincurve.der.parse_signature(der_sig)
|
||||||
|
|
||||||
|
for recovery_id in range(4):
|
||||||
|
recovered_public_key = coincurve.PublicKey.from_signature_and_message(
|
||||||
|
signature=r + s + bytes([recovery_id]),
|
||||||
|
message=message_hash._bytes,
|
||||||
|
hasher=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if recovered_public_key.format() == expected_public_key.format():
|
||||||
|
return recovery_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class SenderAuthentication:
|
||||||
|
_private_key_hex: str
|
||||||
|
|
||||||
|
def sender_address(self) -> Address:
|
||||||
|
verifying_key_repr = self._public_key().format(False)
|
||||||
|
assert len(verifying_key_repr) == 65
|
||||||
|
hashed = HashSha256.of_bytes(verifying_key_repr)
|
||||||
|
return Address(b'\0' + hashed._bytes[-20:])
|
||||||
|
|
||||||
|
def sign_hash(self, hash_to_sign: HashSha256) -> Signature:
|
||||||
|
private_key = self._private_key()
|
||||||
|
signature_wrong_location = private_key.sign_recoverable(
|
||||||
|
hash_to_sign._bytes,
|
||||||
|
hasher=lambda x: x,
|
||||||
|
)
|
||||||
|
|
||||||
|
signature = signature_wrong_location[64:] + signature_wrong_location[:64]
|
||||||
|
|
||||||
|
return Signature(signature)
|
||||||
|
|
||||||
|
def _public_key(self) -> coincurve.PublicKey:
|
||||||
|
return self._private_key().public_key
|
||||||
|
|
||||||
|
def _private_key(self) -> coincurve.PrivateKey:
|
||||||
|
return coincurve.PrivateKey.from_hex(
|
||||||
|
self._private_key_hex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_transaction(
|
||||||
|
sender_authentication: SenderAuthentication,
|
||||||
|
nonce: int,
|
||||||
|
valid_to_time: int,
|
||||||
|
gas_cost: int,
|
||||||
|
chain_id: str,
|
||||||
|
contract_address: Address | str,
|
||||||
|
transaction_rpc: bytes,
|
||||||
|
) -> SignedTransaction:
|
||||||
|
sender = sender_authentication.sender_address
|
||||||
|
|
||||||
|
inner: SignedTransactionInnerPart = SignedTransactionInnerPart(
|
||||||
|
nonce,
|
||||||
|
valid_to_time,
|
||||||
|
gas_cost,
|
||||||
|
Transaction(Address.from_string(contract_address), transaction_rpc),
|
||||||
|
)
|
||||||
|
|
||||||
|
signature: Signature = sender_authentication.sign_hash(inner.hash(chain_id))
|
||||||
|
return SignedTransaction(inner, signature)
|
|
@ -1,22 +1,11 @@
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
|
||||||
import base64
|
|
||||||
import email.utils
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import pbcabi
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import time
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def size_prefixed(_bytes: bytes) -> bytes:
|
def size_prefixed(_bytes: bytes) -> bytes:
|
||||||
return len(_bytes).to_bytes(4, 'big') + _bytes
|
return len(_bytes).to_bytes(4, 'big') + _bytes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Address:
|
class Address:
|
||||||
_bytes: bytes
|
_bytes: bytes
|
||||||
|
@ -31,7 +20,7 @@ class Address:
|
||||||
return self._bytes.hex()
|
return self._bytes.hex()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(s: str) -> 'Address':
|
def from_string(s: 'Address | str') -> 'Address':
|
||||||
if isinstance(s, Address):
|
if isinstance(s, Address):
|
||||||
return s
|
return s
|
||||||
return Address(bytes.fromhex(s))
|
return Address(bytes.fromhex(s))
|
||||||
|
@ -42,7 +31,8 @@ class Signature:
|
||||||
_bytes: bytes
|
_bytes: bytes
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
assert len(self._bytes) == 64, len(self._bytes)
|
assert len(self._bytes) == 65, len(self._bytes)
|
||||||
|
assert self._bytes[0] < 4, 'First byte is the recovery id. Must be less than 4'
|
||||||
|
|
||||||
def rpc_serialize(self) -> bytes:
|
def rpc_serialize(self) -> bytes:
|
||||||
return self._bytes
|
return self._bytes
|
||||||
|
@ -50,6 +40,7 @@ class Signature:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self._bytes.hex()
|
return self._bytes.hex()
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class HashSha256:
|
class HashSha256:
|
||||||
_bytes: bytes
|
_bytes: bytes
|
||||||
|
@ -67,6 +58,7 @@ class HashSha256:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self._bytes.hex()
|
return self._bytes.hex()
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Transaction:
|
class Transaction:
|
||||||
contract_address: Address
|
contract_address: Address
|
||||||
|
@ -76,7 +68,10 @@ class Transaction:
|
||||||
assert isinstance(self.contract_address, Address), self.contract_address
|
assert isinstance(self.contract_address, Address), self.contract_address
|
||||||
|
|
||||||
def rpc_serialize(self) -> bytes:
|
def rpc_serialize(self) -> bytes:
|
||||||
return self.contract_address.rpc_serialize() + size_prefixed(self.transaction_rpc)
|
return self.contract_address.rpc_serialize() + size_prefixed(
|
||||||
|
self.transaction_rpc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class SignedTransactionInnerPart:
|
class SignedTransactionInnerPart:
|
||||||
|
@ -86,13 +81,26 @@ class SignedTransactionInnerPart:
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
|
|
||||||
def rpc_serialize(self) -> bytes:
|
def rpc_serialize(self) -> bytes:
|
||||||
return self.nonce.to_bytes(4, 'big') + self.valid_to_time.to_bytes(8, 'big') + self.gas_cost.to_bytes(8, 'big') + self.transaction.rpc_serialize()
|
return (
|
||||||
|
self.nonce.to_bytes(8, 'big')
|
||||||
|
+ self.valid_to_time.to_bytes(8, 'big')
|
||||||
|
+ self.gas_cost.to_bytes(8, 'big')
|
||||||
|
+ self.transaction.rpc_serialize()
|
||||||
|
)
|
||||||
|
|
||||||
|
def hash(self, chain_id: str) -> HashSha256:
|
||||||
|
return HashSha256.of_bytes(
|
||||||
|
self.rpc_serialize() + size_prefixed(chain_id.encode('utf8')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class SignedTransaction:
|
class SignedTransaction:
|
||||||
inner: SignedTransactionInnerPart
|
inner: SignedTransactionInnerPart
|
||||||
signature: Signature
|
signature: Signature
|
||||||
hash: HashSha256
|
|
||||||
|
def hash(self, chain_id: str) -> HashSha256:
|
||||||
|
return self.inner.hash(chain_id)
|
||||||
|
|
||||||
def rpc_serialize(self) -> bytes:
|
def rpc_serialize(self) -> bytes:
|
||||||
return self.inner.rpc_serialize() + self.signature.rpc_serialize() + self.hash.rpc_serialize()
|
return self.signature.rpc_serialize() + self.inner.rpc_serialize()
|
||||||
|
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
coincurve
|
||||||
|
pbcabi @ git+https://gitfub.space/Jmaa/pbcabi
|
7
setup.py
7
setup.py
|
@ -11,7 +11,7 @@ from setuptools import setup
|
||||||
PACKAGE_NAME = 'pbc_client'
|
PACKAGE_NAME = 'pbc_client'
|
||||||
|
|
||||||
PACKAGE_DESCRIPTION = """
|
PACKAGE_DESCRIPTION = """
|
||||||
# Partisia Blockchain Client
|
# Partisia Blockchain Client
|
||||||
|
|
||||||
Unofficial library for reading contract states from Partisia Blockchain.
|
Unofficial library for reading contract states from Partisia Blockchain.
|
||||||
|
|
||||||
|
@ -35,7 +35,10 @@ with open(PACKAGE_NAME + '/_version.py') as f:
|
||||||
version = parse_version_file(f.read())
|
version = parse_version_file(f.read())
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS_MAIN = []
|
REQUIREMENTS_MAIN = [
|
||||||
|
'coincurve',
|
||||||
|
'pbcabi @ git+https://gitfub.space/Jmaa/pbcabi',
|
||||||
|
]
|
||||||
REQUIREMENTS_TEST = []
|
REQUIREMENTS_TEST = []
|
||||||
|
|
||||||
|
|
||||||
|
|
30
test/test_crypto.py
Normal file
30
test/test_crypto.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from pbc_client.crypto import SenderAuthentication, sign_transaction
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign():
|
||||||
|
sender_authentication = SenderAuthentication('01')
|
||||||
|
chain_id = 'SpecificChainIDLocatable'
|
||||||
|
contract_address = '000000000000000000000000000000000000000001'
|
||||||
|
signed = sign_transaction(
|
||||||
|
sender_authentication=sender_authentication,
|
||||||
|
nonce=2,
|
||||||
|
valid_to_time=3,
|
||||||
|
gas_cost=2,
|
||||||
|
chain_id=chain_id,
|
||||||
|
contract_address=contract_address,
|
||||||
|
transaction_rpc=bytes([0 for i in range(99)]),
|
||||||
|
)
|
||||||
|
print(signed)
|
||||||
|
assert (
|
||||||
|
str(signed.inner.rpc_serialize().hex())
|
||||||
|
== '00000000000000020000000000000003000000000000000200000000000000000000000000000000000000000100000063000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
str(signed.hash(chain_id))
|
||||||
|
== '1be2895a85862e8dd3cc75b290cc28a22e960ae02dd5c07a73b93716f9adbee8'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
str(signed.signature)
|
||||||
|
== '01f0f7d8f44919466eedbebc76bcb369bbb4c6f4f076e25c0ffe8ec9285890e53b4e39098540d088878a019c345ad73963543ce813fb9ccf4b84b0c25770452bd1'
|
||||||
|
)
|
2
test/test_init.py
Normal file
2
test/test_init.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
def test_init():
|
||||||
|
import pbc_client # noqa: F401
|
Loading…
Reference in New Issue
Block a user