Sending transactions is complicated stuff
This commit is contained in:
parent
a4fc294fd9
commit
5021b6c540
|
@ -11,14 +11,20 @@ import base64
|
||||||
import email.utils
|
import email.utils
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import ecdsa
|
||||||
|
import hashlib
|
||||||
import pbcabi
|
import pbcabi
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
import time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from frozendict import frozendict
|
from frozendict import frozendict
|
||||||
|
|
||||||
|
from .pbc_types import (Address, Signature, HashSha256, Transaction,
|
||||||
|
SignedTransaction, SignedTransactionInnerPart)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,8 +33,10 @@ HOSTNAME_TESTNET = 'node1.testnet.partisiablockchain.com'
|
||||||
|
|
||||||
URL_ACCOUNT_PLUGIN = 'https://{hostname}/{shard}blockchain/accountPlugin/local'
|
URL_ACCOUNT_PLUGIN = 'https://{hostname}/{shard}blockchain/accountPlugin/local'
|
||||||
URL_ACCOUNT_PLUGIN_GLOBAL = 'https://{hostname}/{shard}blockchain/accountPlugin/global'
|
URL_ACCOUNT_PLUGIN_GLOBAL = 'https://{hostname}/{shard}blockchain/accountPlugin/global'
|
||||||
|
URL_TRANSACTION = 'https://{hostname}/{shard}blockchain/transaction'
|
||||||
URL_CONTRACT_STATE = 'https://{hostname}/{shard}blockchain/contracts/{address}?requireContractState=false'
|
URL_CONTRACT_STATE = 'https://{hostname}/{shard}blockchain/contracts/{address}?requireContractState=false'
|
||||||
|
URL_CHAIN_ID = 'https://{hostname}/blockchain/chainId'
|
||||||
|
URL_NONCE = 'https://{hostname}/{shard}blockchain/account/{address}'
|
||||||
|
|
||||||
MPC_DECIMALS = 10000
|
MPC_DECIMALS = 10000
|
||||||
|
|
||||||
|
@ -51,13 +59,75 @@ 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:
|
||||||
|
return Address(self._signing_key().verifying_key.to_string())
|
||||||
|
|
||||||
|
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: 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(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:
|
||||||
session: requests.Session
|
session: requests.Session
|
||||||
|
sender_authentication: SenderAuthentication | None = None
|
||||||
hostname: str = HOSTNAME_MAINNET
|
hostname: str = HOSTNAME_MAINNET
|
||||||
|
|
||||||
def get_json(
|
def send_transaction(self, contract_address: str, rpc: bytes, gas_cost: int):
|
||||||
|
if self.sender_authentication is None:
|
||||||
|
msg = "PbcClient.sender_authentication required for send_transaction"
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
signed_transaction = sign_transaction(self.sender_authentication,
|
||||||
|
self.get_sender_authentication_nonce(),
|
||||||
|
gas_cost,
|
||||||
|
self.get_chain_id(),
|
||||||
|
contract_address, rpc)
|
||||||
|
|
||||||
|
return self.send_signed_transaction(signed_transaction)
|
||||||
|
|
||||||
|
def send_signed_transaction(self, signed_transaction: SignedTransaction):
|
||||||
|
url = URL_TRANSACTION.format(
|
||||||
|
hostname=self.hostname,
|
||||||
|
shard=shard_id_for_address(signed_transaction.inner.transaction.contract_address),
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction_payload: str = base64.b64encode(signed_transaction.rpc_serialize()).decode('utf8')
|
||||||
|
self._get_json(url, data = {'transactionPayload':transaction_payload }, method = 'PUT')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_json(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
data: Mapping[str, Any] = frozendict(),
|
data: Mapping[str, Any] = frozendict(),
|
||||||
|
@ -93,7 +163,7 @@ class PbcClient:
|
||||||
shard='',
|
shard='',
|
||||||
)
|
)
|
||||||
|
|
||||||
json_data, date = self.get_json(url, data=data)
|
json_data, date = self._get_json(url, data=data)
|
||||||
return json_data['coins']['coins']
|
return json_data['coins']['coins']
|
||||||
|
|
||||||
def get_account_balances(self, address: str) -> Balances:
|
def get_account_balances(self, address: str) -> Balances:
|
||||||
|
@ -110,7 +180,7 @@ class PbcClient:
|
||||||
{'type': 'avl', 'keyType': 'BLOCKCHAIN_ADDRESS', 'key': address},
|
{'type': 'avl', 'keyType': 'BLOCKCHAIN_ADDRESS', 'key': address},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
account_data, date = self.get_json(url, data=data)
|
account_data, date = self._get_json(url, data=data)
|
||||||
|
|
||||||
byoc: dict[str, Decimal] = {}
|
byoc: dict[str, Decimal] = {}
|
||||||
mpc = Decimal(account_data['mpcTokens']) / MPC_DECIMALS
|
mpc = Decimal(account_data['mpcTokens']) / MPC_DECIMALS
|
||||||
|
@ -125,6 +195,24 @@ class PbcClient:
|
||||||
|
|
||||||
return Balances(date, mpc, byoc)
|
return Balances(date, mpc, byoc)
|
||||||
|
|
||||||
|
def get_chain_id(self) -> str:
|
||||||
|
url = URL_NONCE.format(
|
||||||
|
hostname=self.hostname,
|
||||||
|
)
|
||||||
|
return self._get_json(url, method='GET')[0]['chainId']
|
||||||
|
|
||||||
|
def get_sender_authentication_nonce(self) -> int:
|
||||||
|
return self.get_nonce_for_account(self.sender_authentication.sender_address)
|
||||||
|
|
||||||
|
def get_nonce_for_account(self, address: str) -> int:
|
||||||
|
url = URL_NONCE.format(
|
||||||
|
hostname=self.hostname,
|
||||||
|
shard=shard_id_for_address(address),
|
||||||
|
address=address,
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
@ -133,7 +221,7 @@ class PbcClient:
|
||||||
address=address,
|
address=address,
|
||||||
)
|
)
|
||||||
data: dict = {'path': []}
|
data: dict = {'path': []}
|
||||||
return self.get_json(url, data=data)
|
return self._get_json(url, data=data)
|
||||||
|
|
||||||
def get_typed_contract_state(self, address: str) -> tuple[dict, datetime.datetime]:
|
def get_typed_contract_state(self, address: str) -> tuple[dict, datetime.datetime]:
|
||||||
"""
|
"""
|
||||||
|
@ -174,6 +262,6 @@ class PbcClient:
|
||||||
shard=shard_id_for_address(address),
|
shard=shard_id_for_address(address),
|
||||||
address=address,
|
address=address,
|
||||||
)
|
)
|
||||||
meta, _ = self.get_json(url, method='GET')
|
meta, _ = self._get_json(url, method='GET')
|
||||||
abi_bytes = base64.b64decode(meta['abi'])
|
abi_bytes = base64.b64decode(meta['abi'])
|
||||||
return pbcabi.model.FileAbi.read_from(abi_bytes)
|
return pbcabi.model.FileAbi.read_from(abi_bytes)
|
||||||
|
|
79
pbc_client/pbc_types.py
Normal file
79
pbc_client/pbc_types.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import dataclasses
|
||||||
|
import datetime
|
||||||
|
import base64
|
||||||
|
import email.utils
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
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:
|
||||||
|
return len(_bytes).to_bytes(4, 'big') + _bytes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Address:
|
||||||
|
_bytes: bytes
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert len(self._bytes) == 21
|
||||||
|
|
||||||
|
def rpc_serialize(self) -> bytes:
|
||||||
|
return self._bytes
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Signature:
|
||||||
|
_bytes: bytes
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert len(self._bytes) == 32
|
||||||
|
|
||||||
|
def rpc_serialize(self) -> bytes:
|
||||||
|
return self._bytes
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class HashSha256:
|
||||||
|
_bytes: bytes
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert len(self._bytes) == 32
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def of_bytes(b: bytes) -> 'HashSha256':
|
||||||
|
return HashSha256(hashlib.sha256(b).digest())
|
||||||
|
|
||||||
|
def rpc_serialize(self) -> bytes:
|
||||||
|
return self._bytes
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Transaction:
|
||||||
|
contract_address: Address
|
||||||
|
transaction_rpc: bytes
|
||||||
|
|
||||||
|
def rpc_serialize(self) -> bytes:
|
||||||
|
return self.contract_address.rpc_serialize() + size_prefixed(self.transaction_rpc)
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class SignedTransactionInnerPart:
|
||||||
|
nonce: int
|
||||||
|
valid_to_time: int
|
||||||
|
gas_cost: int
|
||||||
|
transaction: Transaction
|
||||||
|
|
||||||
|
def rpc_serialize(self) -> bytes:
|
||||||
|
return self.nonce.to_bytes(4, 'big') + self.valid_to_time.to_bytes(4, 'big') + self.gas_cost.to_bytes(4, 'big') + self.transaction.rpc_serialize()
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class SignedTransaction:
|
||||||
|
inner: SignedTransactionInnerPart
|
||||||
|
signature: Signature
|
||||||
|
hash: HashSha256
|
||||||
|
|
||||||
|
def rpc_serialize(self) -> bytes:
|
||||||
|
return self.inner.rpc_serialize() + self.signature.rpc_serialize() + self.hash.rpc_serialize()
|
Loading…
Reference in New Issue
Block a user