cryptochief-crypto-processing-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cryptochief/__init__.py +336 -0
- cryptochief/_models.py +93 -0
- cryptochief/_version.py +3 -0
- cryptochief/amount.py +97 -0
- cryptochief/assets.py +32 -0
- cryptochief/chains.py +115 -0
- cryptochief/client.py +189 -0
- cryptochief/contract/__init__.py +69 -0
- cryptochief/contract/base58.py +40 -0
- cryptochief/contract/borsh.py +141 -0
- cryptochief/contract/evm_abi.py +321 -0
- cryptochief/contract/keccak.py +16 -0
- cryptochief/contract/tron_address.py +60 -0
- cryptochief/errors.py +111 -0
- cryptochief/pagination.py +32 -0
- cryptochief/poll.py +56 -0
- cryptochief/rsa.py +70 -0
- cryptochief/services/__init__.py +1 -0
- cryptochief/services/base.py +24 -0
- cryptochief/services/blockchain.py +76 -0
- cryptochief/services/currencies.py +55 -0
- cryptochief/services/payins.py +145 -0
- cryptochief/services/payouts.py +175 -0
- cryptochief/services/static_deposits.py +77 -0
- cryptochief/services/sweeps.py +85 -0
- cryptochief/services/transactions.py +470 -0
- cryptochief/services/wallets.py +86 -0
- cryptochief/services/withdrawals.py +51 -0
- cryptochief/sign.py +112 -0
- cryptochief/ton/__init__.py +19 -0
- cryptochief/ton/address.py +109 -0
- cryptochief/ton/messages.py +105 -0
- cryptochief/ton/rpc.py +159 -0
- cryptochief/transport.py +48 -0
- cryptochief/webhook.py +191 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/METADATA +346 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/RECORD +39 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/WHEEL +4 -0
- cryptochief_crypto_processing_python-0.1.0.dist-info/licenses/LICENSE +21 -0
cryptochief/client.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""The asynchronous Crypto Chief client and its low-level signed transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Mapping, Optional, Union
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
from .errors import CryptoChiefError, is_retryable
|
|
13
|
+
from .rsa import RsaKeyNotConfiguredError, decrypt_rsa_oaep, load_rsa_private_key_pem
|
|
14
|
+
from .services.blockchain import BlockchainService
|
|
15
|
+
from .services.currencies import CurrenciesService
|
|
16
|
+
from .services.payins import PayInsService
|
|
17
|
+
from .services.payouts import PayoutsService
|
|
18
|
+
from .services.static_deposits import StaticDepositsService
|
|
19
|
+
from .services.sweeps import SweepsService
|
|
20
|
+
from .services.transactions import TransactionsService
|
|
21
|
+
from .services.wallets import WalletsService
|
|
22
|
+
from .services.withdrawals import WithdrawalsService
|
|
23
|
+
from .sign import sign_value
|
|
24
|
+
from .ton.rpc import TonRpc
|
|
25
|
+
from .transport import backoff_delay, network_error, parse_api_error
|
|
26
|
+
|
|
27
|
+
#: SDK version, reported in the default ``User-Agent``.
|
|
28
|
+
VERSION = __version__
|
|
29
|
+
|
|
30
|
+
#: Production processing API endpoint. Test-mode projects share this host.
|
|
31
|
+
DEFAULT_BASE_URL = "https://api-processing.crypto-chief.com"
|
|
32
|
+
|
|
33
|
+
_MAX_RAW_IN_ERROR = 512
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CryptoChiefClient:
|
|
37
|
+
"""Entry point to the Crypto Chief processing API.
|
|
38
|
+
|
|
39
|
+
Construct once and reuse - the client is stateless beyond its configuration.
|
|
40
|
+
It owns an :class:`httpx.AsyncClient`, so close it when done (or use it as an
|
|
41
|
+
async context manager)::
|
|
42
|
+
|
|
43
|
+
async with CryptoChiefClient(merchant_id="M", api_key="K") as client:
|
|
44
|
+
est = await client.payouts.estimate(EstimatePayoutRequest(
|
|
45
|
+
network=Chain.ETH_SEPOLIA, coin="ETH", amount="0.0001",
|
|
46
|
+
to_address="0x...",
|
|
47
|
+
))
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
merchant_id: str,
|
|
54
|
+
api_key: str,
|
|
55
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
56
|
+
timeout: float = 60.0,
|
|
57
|
+
retries: int = 3,
|
|
58
|
+
retry_backoff: Optional[Mapping[str, float]] = None,
|
|
59
|
+
user_agent: Optional[str] = None,
|
|
60
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
61
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
62
|
+
rsa_private_key: Optional[Union[str, bytes, Any]] = None,
|
|
63
|
+
ton_rpc_base_url: Optional[str] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
if not merchant_id:
|
|
66
|
+
raise CryptoChiefError("cryptochief: merchant_id is required")
|
|
67
|
+
if not api_key:
|
|
68
|
+
raise CryptoChiefError("cryptochief: api_key is required")
|
|
69
|
+
|
|
70
|
+
self.merchant_id = merchant_id
|
|
71
|
+
self._api_key = api_key
|
|
72
|
+
self.base_url = base_url.rstrip("/")
|
|
73
|
+
self._timeout = timeout
|
|
74
|
+
self._retries = retries
|
|
75
|
+
backoff = retry_backoff or {}
|
|
76
|
+
self._base_ms = backoff.get("base_ms", 200)
|
|
77
|
+
self._max_ms = backoff.get("max_ms", 5000)
|
|
78
|
+
self._user_agent = user_agent or f"cryptochief-python/{VERSION}"
|
|
79
|
+
|
|
80
|
+
self._owns_http = http_client is None
|
|
81
|
+
self._http = http_client or httpx.AsyncClient(timeout=timeout, transport=transport)
|
|
82
|
+
|
|
83
|
+
self._rsa_input = rsa_private_key
|
|
84
|
+
self._rsa_key: Any = None
|
|
85
|
+
self._rsa_error: Optional[CryptoChiefError] = None
|
|
86
|
+
|
|
87
|
+
self._ton_rpc_base_url = ton_rpc_base_url
|
|
88
|
+
self._ton_rpc: Optional[TonRpc] = None
|
|
89
|
+
|
|
90
|
+
self.payouts = PayoutsService(self)
|
|
91
|
+
self.transactions = TransactionsService(self)
|
|
92
|
+
self.pay_ins = PayInsService(self)
|
|
93
|
+
self.wallets = WalletsService(self)
|
|
94
|
+
self.sweeps = SweepsService(self)
|
|
95
|
+
self.withdrawals = WithdrawalsService(self)
|
|
96
|
+
self.static_deposits = StaticDepositsService(self)
|
|
97
|
+
self.blockchain = BlockchainService(self)
|
|
98
|
+
self.currencies = CurrenciesService(self)
|
|
99
|
+
|
|
100
|
+
async def request(self, path: str, body: Any = None) -> Any:
|
|
101
|
+
"""Low-level signed POST against an API path (e.g. ``/v1/payout/estimate``).
|
|
102
|
+
|
|
103
|
+
Canonicalizes + signs the body, sends it, retries transient failures, and
|
|
104
|
+
returns the parsed JSON. Service methods are thin wrappers over this;
|
|
105
|
+
reach for it directly only to hit an endpoint the SDK doesn't model yet.
|
|
106
|
+
"""
|
|
107
|
+
canonical, signature = sign_value(body, self._api_key)
|
|
108
|
+
url = self.base_url + path
|
|
109
|
+
headers = {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
"Accept": "application/json",
|
|
112
|
+
"Merchant": self.merchant_id,
|
|
113
|
+
"Signature": signature,
|
|
114
|
+
"User-Agent": self._user_agent,
|
|
115
|
+
}
|
|
116
|
+
body_bytes = canonical.encode("utf-8")
|
|
117
|
+
attempts = self._retries + 1
|
|
118
|
+
last_err: Optional[Exception] = None
|
|
119
|
+
|
|
120
|
+
for attempt in range(attempts):
|
|
121
|
+
if attempt > 0:
|
|
122
|
+
await asyncio.sleep(backoff_delay(attempt, self._base_ms, self._max_ms))
|
|
123
|
+
try:
|
|
124
|
+
resp = await self._http.post(url, content=body_bytes, headers=headers)
|
|
125
|
+
except httpx.HTTPError as err:
|
|
126
|
+
last_err = network_error(str(err))
|
|
127
|
+
if not is_retryable(last_err):
|
|
128
|
+
raise last_err
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
text = resp.text
|
|
132
|
+
status = resp.status_code
|
|
133
|
+
if 200 <= status < 300:
|
|
134
|
+
if not text:
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
return json.loads(text)
|
|
138
|
+
except ValueError as err:
|
|
139
|
+
raise CryptoChiefError(
|
|
140
|
+
f"cryptochief: decode {path} response: {err} "
|
|
141
|
+
f"(raw={text[:_MAX_RAW_IN_ERROR]})"
|
|
142
|
+
) from err
|
|
143
|
+
|
|
144
|
+
api_err = parse_api_error(status, text)
|
|
145
|
+
if status >= 500:
|
|
146
|
+
last_err = api_err
|
|
147
|
+
continue
|
|
148
|
+
raise api_err
|
|
149
|
+
|
|
150
|
+
raise last_err or CryptoChiefError("cryptochief: retry budget exhausted")
|
|
151
|
+
|
|
152
|
+
async def aclose(self) -> None:
|
|
153
|
+
"""Close the underlying HTTP client (only if this client created it)."""
|
|
154
|
+
if self._owns_http:
|
|
155
|
+
await self._http.aclose()
|
|
156
|
+
|
|
157
|
+
async def __aenter__(self) -> "CryptoChiefClient":
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
161
|
+
await self.aclose()
|
|
162
|
+
|
|
163
|
+
def rsa_decrypt(self, encrypted: str) -> str:
|
|
164
|
+
"""Decrypt a wallet ``private_key_encrypted`` field. Used by ``wallets``."""
|
|
165
|
+
if self._rsa_error:
|
|
166
|
+
raise self._rsa_error
|
|
167
|
+
if self._rsa_key is None:
|
|
168
|
+
if self._rsa_input is None:
|
|
169
|
+
raise RsaKeyNotConfiguredError()
|
|
170
|
+
try:
|
|
171
|
+
if isinstance(self._rsa_input, (str, bytes, bytearray)):
|
|
172
|
+
self._rsa_key = load_rsa_private_key_pem(self._rsa_input)
|
|
173
|
+
else:
|
|
174
|
+
self._rsa_key = self._rsa_input # already a private-key object
|
|
175
|
+
except CryptoChiefError as err:
|
|
176
|
+
self._rsa_error = err
|
|
177
|
+
raise
|
|
178
|
+
return decrypt_rsa_oaep(self._rsa_key, encrypted)
|
|
179
|
+
|
|
180
|
+
def ton_rpc(self) -> TonRpc:
|
|
181
|
+
"""Lazily built TON RPC helper, sharing the merchant credential + HTTP client."""
|
|
182
|
+
if self._ton_rpc is None:
|
|
183
|
+
self._ton_rpc = TonRpc(
|
|
184
|
+
merchant_id=self.merchant_id,
|
|
185
|
+
http=self._http,
|
|
186
|
+
base_url=self._ton_rpc_base_url,
|
|
187
|
+
user_agent=self._user_agent,
|
|
188
|
+
)
|
|
189
|
+
return self._ton_rpc
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Contract-call encoders: EVM/TRON ABI, Solana Borsh/Anchor, base58, TRON addresses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base58 import base58_decode, base58_encode
|
|
6
|
+
from .borsh import (
|
|
7
|
+
BorshValue,
|
|
8
|
+
anchor_discriminator,
|
|
9
|
+
borsh_bool,
|
|
10
|
+
borsh_bytes,
|
|
11
|
+
borsh_fixed_bytes,
|
|
12
|
+
borsh_i8,
|
|
13
|
+
borsh_i16,
|
|
14
|
+
borsh_i32,
|
|
15
|
+
borsh_i64,
|
|
16
|
+
borsh_option,
|
|
17
|
+
borsh_pubkey,
|
|
18
|
+
borsh_string,
|
|
19
|
+
borsh_struct,
|
|
20
|
+
borsh_u8,
|
|
21
|
+
borsh_u16,
|
|
22
|
+
borsh_u32,
|
|
23
|
+
borsh_u64,
|
|
24
|
+
borsh_u128,
|
|
25
|
+
borsh_vec,
|
|
26
|
+
decode_solana_pubkey,
|
|
27
|
+
encode_anchor_instruction,
|
|
28
|
+
)
|
|
29
|
+
from .evm_abi import (
|
|
30
|
+
canonical_signature,
|
|
31
|
+
encode_evm_call,
|
|
32
|
+
encode_evm_call_hex,
|
|
33
|
+
evm_selector,
|
|
34
|
+
)
|
|
35
|
+
from .keccak import keccak_256
|
|
36
|
+
from .tron_address import hex_to_tron, tron_to_hex
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"base58_decode",
|
|
40
|
+
"base58_encode",
|
|
41
|
+
"BorshValue",
|
|
42
|
+
"anchor_discriminator",
|
|
43
|
+
"borsh_bool",
|
|
44
|
+
"borsh_bytes",
|
|
45
|
+
"borsh_fixed_bytes",
|
|
46
|
+
"borsh_i8",
|
|
47
|
+
"borsh_i16",
|
|
48
|
+
"borsh_i32",
|
|
49
|
+
"borsh_i64",
|
|
50
|
+
"borsh_option",
|
|
51
|
+
"borsh_pubkey",
|
|
52
|
+
"borsh_string",
|
|
53
|
+
"borsh_struct",
|
|
54
|
+
"borsh_u8",
|
|
55
|
+
"borsh_u16",
|
|
56
|
+
"borsh_u32",
|
|
57
|
+
"borsh_u64",
|
|
58
|
+
"borsh_u128",
|
|
59
|
+
"borsh_vec",
|
|
60
|
+
"decode_solana_pubkey",
|
|
61
|
+
"encode_anchor_instruction",
|
|
62
|
+
"canonical_signature",
|
|
63
|
+
"encode_evm_call",
|
|
64
|
+
"encode_evm_call_hex",
|
|
65
|
+
"evm_selector",
|
|
66
|
+
"keccak_256",
|
|
67
|
+
"hex_to_tron",
|
|
68
|
+
"tron_to_hex",
|
|
69
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Base58 (Bitcoin / Tron / Solana alphabet).
|
|
2
|
+
|
|
3
|
+
Shared by the TRON address codec and Solana pubkey decoding. Pure ``int``
|
|
4
|
+
arithmetic - no external dependency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..errors import CryptoChiefError
|
|
10
|
+
|
|
11
|
+
_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
12
|
+
_DECODE = {c: i for i, c in enumerate(_ALPHABET)}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def base58_encode(data: bytes) -> str:
|
|
16
|
+
zeros = 0
|
|
17
|
+
while zeros < len(data) and data[zeros] == 0:
|
|
18
|
+
zeros += 1
|
|
19
|
+
num = int.from_bytes(data, "big")
|
|
20
|
+
out = ""
|
|
21
|
+
while num > 0:
|
|
22
|
+
num, rem = divmod(num, 58)
|
|
23
|
+
out = _ALPHABET[rem] + out
|
|
24
|
+
return _ALPHABET[0] * zeros + out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def base58_decode(s: str) -> bytes:
|
|
28
|
+
if s == "":
|
|
29
|
+
raise CryptoChiefError("cryptochief: base58: empty input")
|
|
30
|
+
zeros = 0
|
|
31
|
+
while zeros < len(s) and s[zeros] == _ALPHABET[0]:
|
|
32
|
+
zeros += 1
|
|
33
|
+
num = 0
|
|
34
|
+
for ch in s:
|
|
35
|
+
v = _DECODE.get(ch, -1)
|
|
36
|
+
if v < 0:
|
|
37
|
+
raise CryptoChiefError(f"cryptochief: base58: invalid char {ch!r}")
|
|
38
|
+
num = num * 58 + v
|
|
39
|
+
body = num.to_bytes((num.bit_length() + 7) // 8, "big") if num > 0 else b""
|
|
40
|
+
return b"\x00" * zeros + body
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Borsh encoding + Anchor instruction building for Solana.
|
|
2
|
+
|
|
3
|
+
Anchor instruction data is ``[8-byte discriminator][Borsh-encoded args]``. Borsh
|
|
4
|
+
has no on-wire type tags, so the caller must describe each argument's type
|
|
5
|
+
explicitly - the ``borsh_*`` constructors below force that. Each returns a
|
|
6
|
+
:class:`BorshValue`; pass them to :func:`encode_anchor_instruction`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from typing import List, Optional, Union
|
|
13
|
+
|
|
14
|
+
from ..errors import CryptoChiefError
|
|
15
|
+
from .base58 import base58_decode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BorshError(CryptoChiefError):
|
|
19
|
+
def __init__(self, message: str) -> None:
|
|
20
|
+
super().__init__(f"cryptochief/anchor: {message}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BorshValue:
|
|
24
|
+
"""A value paired with its Borsh encoding, ready to concatenate."""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("_data",)
|
|
27
|
+
|
|
28
|
+
def __init__(self, data: bytes) -> None:
|
|
29
|
+
self._data = data
|
|
30
|
+
|
|
31
|
+
def encode(self) -> bytes:
|
|
32
|
+
return self._data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _le(value: int, width: int) -> bytes:
|
|
36
|
+
return (value & ((1 << (8 * width)) - 1)).to_bytes(width, "little")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Unsigned little-endian integers.
|
|
40
|
+
def borsh_u8(n: int) -> BorshValue:
|
|
41
|
+
return BorshValue(_le(n, 1))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def borsh_u16(n: int) -> BorshValue:
|
|
45
|
+
return BorshValue(_le(n, 2))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def borsh_u32(n: int) -> BorshValue:
|
|
49
|
+
return BorshValue(_le(n, 4))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def borsh_u64(n: int) -> BorshValue:
|
|
53
|
+
return BorshValue(_le(n, 8))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Signed little-endian integers (two's complement - same wire bytes as unsigned).
|
|
57
|
+
borsh_i8 = borsh_u8
|
|
58
|
+
borsh_i16 = borsh_u16
|
|
59
|
+
borsh_i32 = borsh_u32
|
|
60
|
+
borsh_i64 = borsh_u64
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def borsh_u128(n: int) -> BorshValue:
|
|
64
|
+
"""128-bit unsigned little-endian. Must be non-negative and < 2^128."""
|
|
65
|
+
if n < 0:
|
|
66
|
+
raise BorshError("u128 negative")
|
|
67
|
+
if n >= (1 << 128):
|
|
68
|
+
raise BorshError("u128 overflow")
|
|
69
|
+
return BorshValue(_le(n, 16))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def borsh_bool(b: bool) -> BorshValue:
|
|
73
|
+
"""1-byte boolean (0x00 / 0x01)."""
|
|
74
|
+
return BorshValue(b"\x01" if b else b"\x00")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def borsh_string(s: str) -> BorshValue:
|
|
78
|
+
"""UTF-8 string: 4-byte LE length prefix + bytes."""
|
|
79
|
+
data = s.encode("utf-8")
|
|
80
|
+
return BorshValue(_le(len(data), 4) + data)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def borsh_bytes(b: bytes) -> BorshValue:
|
|
84
|
+
"""Raw byte slice: 4-byte LE length prefix + bytes (same wire form as a string)."""
|
|
85
|
+
b = bytes(b)
|
|
86
|
+
return BorshValue(_le(len(b), 4) + b)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def borsh_fixed_bytes(b: bytes, n: int) -> BorshValue:
|
|
90
|
+
"""Fixed-length bytes with NO length prefix (Anchor's ``[u8; N]``)."""
|
|
91
|
+
b = bytes(b)
|
|
92
|
+
if len(b) != n:
|
|
93
|
+
raise BorshError(f"borsh_fixed_bytes: expected {n} bytes, got {len(b)}")
|
|
94
|
+
return BorshValue(b)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def borsh_pubkey(pk: Union[str, bytes]) -> BorshValue:
|
|
98
|
+
"""A Solana 32-byte pubkey (base58 string or raw 32 bytes)."""
|
|
99
|
+
return BorshValue(decode_solana_pubkey(pk))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def borsh_option(inner: Optional[BorshValue]) -> BorshValue:
|
|
103
|
+
"""Nullable value: ``None`` -> 0x00; otherwise 0x01 + inner encoding."""
|
|
104
|
+
if inner is None:
|
|
105
|
+
return BorshValue(b"\x00")
|
|
106
|
+
return BorshValue(b"\x01" + inner.encode())
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def borsh_vec(items: List[BorshValue]) -> BorshValue:
|
|
110
|
+
"""Homogeneous ``Vec<T>``: 4-byte LE length + elements."""
|
|
111
|
+
body = b"".join(it.encode() for it in items)
|
|
112
|
+
return BorshValue(_le(len(items), 4) + body)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def borsh_struct(*fields: BorshValue) -> BorshValue:
|
|
116
|
+
"""Heterogeneous struct / tuple: fields in order, no length prefix."""
|
|
117
|
+
return BorshValue(b"".join(f.encode() for f in fields))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def anchor_discriminator(method: str) -> bytes:
|
|
121
|
+
"""The 8-byte Anchor instruction discriminator: ``sha256("global:" + method)[:8]``."""
|
|
122
|
+
return hashlib.sha256(f"global:{method}".encode("utf-8")).digest()[:8]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def encode_anchor_instruction(method: str, *args: BorshValue) -> bytes:
|
|
126
|
+
"""Raw Anchor instruction data: 8-byte discriminator + Borsh-encoded args."""
|
|
127
|
+
parts = [anchor_discriminator(method)]
|
|
128
|
+
parts.extend(a.encode() for a in args)
|
|
129
|
+
return b"".join(parts)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def decode_solana_pubkey(pk: Union[str, bytes]) -> bytes:
|
|
133
|
+
"""Decode a Solana pubkey (base58 string or raw 32 bytes) to its 32-byte form."""
|
|
134
|
+
if isinstance(pk, (bytes, bytearray)):
|
|
135
|
+
if len(pk) != 32:
|
|
136
|
+
raise BorshError(f"solana pubkey: want 32 bytes, got {len(pk)}")
|
|
137
|
+
return bytes(pk)
|
|
138
|
+
raw = base58_decode(pk)
|
|
139
|
+
if len(raw) != 32:
|
|
140
|
+
raise BorshError(f"solana pubkey: decoded length {len(raw)}, want 32")
|
|
141
|
+
return raw
|