Sending transactions is complicated stuff
This commit is contained in:
parent
a4fc294fd9
commit
5021b6c540
|
@ -11,14 +11,20 @@ import base64
|
|||
import email.utils
|
||||
import json
|
||||
import logging
|
||||
import ecdsa
|
||||
import hashlib
|
||||
import pbcabi
|
||||
from collections.abc import Mapping
|
||||
import time
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from frozendict import frozendict
|
||||
|
||||
from .pbc_types import (Address, Signature, HashSha256, Transaction,
|
||||
SignedTransaction, SignedTransactionInnerPart)
|
||||
|
||||
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_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_CHAIN_ID = 'https://{hostname}/blockchain/chainId'
|
||||
URL_NONCE = 'https://{hostname}/{shard}blockchain/account/{address}'
|
||||
|
||||
MPC_DECIMALS = 10000
|
||||
|
||||
|
@ -51,13 +59,75 @@ class Balances:
|
|||
mpc: 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)
|
||||
class PbcClient:
|
||||
session: requests.Session
|
||||
sender_authentication: SenderAuthentication | None = None
|
||||
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,
|
||||
url: str,
|
||||
data: Mapping[str, Any] = frozendict(),
|
||||
|
@ -93,7 +163,7 @@ class PbcClient:
|
|||
shard='',
|
||||
)
|
||||
|
||||
json_data, date = self.get_json(url, data=data)
|
||||
json_data, date = self._get_json(url, data=data)
|
||||
return json_data['coins']['coins']
|
||||
|
||||
def get_account_balances(self, address: str) -> Balances:
|
||||
|
@ -110,7 +180,7 @@ class PbcClient:
|
|||
{'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] = {}
|
||||
mpc = Decimal(account_data['mpcTokens']) / MPC_DECIMALS
|
||||
|
@ -125,6 +195,24 @@ class PbcClient:
|
|||
|
||||
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]:
|
||||
# TODO: Rename to get_contract_state_json
|
||||
url = URL_CONTRACT_STATE.format(
|
||||
|
@ -133,7 +221,7 @@ class PbcClient:
|
|||
address=address,
|
||||
)
|
||||
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]:
|
||||
"""
|
||||
|
@ -174,6 +262,6 @@ class PbcClient:
|
|||
shard=shard_id_for_address(address),
|
||||
address=address,
|
||||
)
|
||||
meta, _ = self.get_json(url, method='GET')
|
||||
meta, _ = self._get_json(url, method='GET')
|
||||
abi_bytes = base64.b64decode(meta['abi'])
|
||||
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