swig-developer-sdk 0.6.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.
- swig_developer_sdk/__init__.py +180 -0
- swig_developer_sdk/client.py +36 -0
- swig_developer_sdk/common.py +104 -0
- swig_developer_sdk/core.py +162 -0
- swig_developer_sdk/evm.py +64 -0
- swig_developer_sdk/one_business.py +163 -0
- swig_developer_sdk/passkeys.py +122 -0
- swig_developer_sdk/paymaster.py +106 -0
- swig_developer_sdk/proxy.py +724 -0
- swig_developer_sdk/ramp.py +724 -0
- swig_developer_sdk/signing.py +461 -0
- swig_developer_sdk/transactions.py +277 -0
- swig_developer_sdk/wallets.py +1393 -0
- swig_developer_sdk-0.6.0.dist-info/METADATA +89 -0
- swig_developer_sdk-0.6.0.dist-info/RECORD +16 -0
- swig_developer_sdk-0.6.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from .client import SwigClient, SwigServerClient
|
|
2
|
+
from .common import (
|
|
3
|
+
DEFAULT_BACKEND_URL,
|
|
4
|
+
Amount,
|
|
5
|
+
JsonObject,
|
|
6
|
+
JsonValue,
|
|
7
|
+
Network,
|
|
8
|
+
RetryOptions,
|
|
9
|
+
WalletAddressInfo,
|
|
10
|
+
WalletAuthority,
|
|
11
|
+
)
|
|
12
|
+
from .core import SwigDeveloperSdkError
|
|
13
|
+
from .evm import Eip1193Provider, create_secp256k1_evm_signing_fn
|
|
14
|
+
from .one_business import (
|
|
15
|
+
DEFAULT_ONE_BUSINESS_URL,
|
|
16
|
+
OneBusinessGrantAccessCallbackError,
|
|
17
|
+
OneBusinessGrantAccessResult,
|
|
18
|
+
OneBusinessLocation,
|
|
19
|
+
build_one_business_grant_access_url,
|
|
20
|
+
complete_one_business_grant_access,
|
|
21
|
+
redirect_to_one_business_grant_access,
|
|
22
|
+
)
|
|
23
|
+
from .passkeys import (
|
|
24
|
+
WebAuthnAssertion,
|
|
25
|
+
create_secp256r1_passkey_signing_fn,
|
|
26
|
+
secp256r1_der_to_raw_signature,
|
|
27
|
+
)
|
|
28
|
+
from .paymaster import PaymasterBalance, PaymasterClient
|
|
29
|
+
from .proxy import (
|
|
30
|
+
ProxyResponse,
|
|
31
|
+
ProxyWalletReference,
|
|
32
|
+
SwigProxyConfig,
|
|
33
|
+
SwigProxyHandler,
|
|
34
|
+
SwigRouteContext,
|
|
35
|
+
create_swig_proxy_handler,
|
|
36
|
+
)
|
|
37
|
+
from .ramp import (
|
|
38
|
+
CreateRampSessionArgs,
|
|
39
|
+
CreateRampSessionResult,
|
|
40
|
+
GetRampOptionsResult,
|
|
41
|
+
GetRampTransactionResult,
|
|
42
|
+
ListRampTransactionsResult,
|
|
43
|
+
QuoteRampArgs,
|
|
44
|
+
QuoteRampResult,
|
|
45
|
+
RampClient,
|
|
46
|
+
RampCustomerContext,
|
|
47
|
+
RampPaymentMethodType,
|
|
48
|
+
RampQuote,
|
|
49
|
+
RampServiceProvider,
|
|
50
|
+
RampTransaction,
|
|
51
|
+
RampTransactionStatus,
|
|
52
|
+
RampTransactionType,
|
|
53
|
+
RampWalletContext,
|
|
54
|
+
ramp_customer,
|
|
55
|
+
)
|
|
56
|
+
from .signing import (
|
|
57
|
+
PasskeySigningFn,
|
|
58
|
+
PasskeySigningResult,
|
|
59
|
+
PreparedTransactionSigner,
|
|
60
|
+
Secp256k1SigningFn,
|
|
61
|
+
Secp256k1SigningResult,
|
|
62
|
+
sign_prepared_swig_transaction,
|
|
63
|
+
sign_prepared_swig_transactions,
|
|
64
|
+
sign_prepared_transaction,
|
|
65
|
+
sign_prepared_transaction_with_signer,
|
|
66
|
+
)
|
|
67
|
+
from .transactions import (
|
|
68
|
+
ClientSignatureRequest,
|
|
69
|
+
PreparedTransaction,
|
|
70
|
+
PreparedTransactionsResult,
|
|
71
|
+
SignedPreparedTransaction,
|
|
72
|
+
SponsorSignedTransactionArgs,
|
|
73
|
+
SubmittedTransaction,
|
|
74
|
+
TransactionsClient,
|
|
75
|
+
)
|
|
76
|
+
from .wallets import (
|
|
77
|
+
CreateWalletResult,
|
|
78
|
+
IdpWalletSession,
|
|
79
|
+
ListSwigTokenBalancesResult,
|
|
80
|
+
ListSwigTokenTransactionsResult,
|
|
81
|
+
Policy,
|
|
82
|
+
PreparedRecoverySetupResult,
|
|
83
|
+
RecoveryOptions,
|
|
84
|
+
RecoverySetupPlan,
|
|
85
|
+
SolanaAccountMeta,
|
|
86
|
+
SolanaInstructionInput,
|
|
87
|
+
SwigTokenBalance,
|
|
88
|
+
SwigTokenTransaction,
|
|
89
|
+
SwigUsdBalance,
|
|
90
|
+
TransferSolOperation,
|
|
91
|
+
TransferTokenOperation,
|
|
92
|
+
WalletHandle,
|
|
93
|
+
WalletReference,
|
|
94
|
+
WalletsClient,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"DEFAULT_BACKEND_URL",
|
|
99
|
+
"DEFAULT_ONE_BUSINESS_URL",
|
|
100
|
+
"Amount",
|
|
101
|
+
"ClientSignatureRequest",
|
|
102
|
+
"CreateRampSessionArgs",
|
|
103
|
+
"CreateRampSessionResult",
|
|
104
|
+
"CreateWalletResult",
|
|
105
|
+
"Eip1193Provider",
|
|
106
|
+
"GetRampOptionsResult",
|
|
107
|
+
"GetRampTransactionResult",
|
|
108
|
+
"IdpWalletSession",
|
|
109
|
+
"JsonObject",
|
|
110
|
+
"JsonValue",
|
|
111
|
+
"ListRampTransactionsResult",
|
|
112
|
+
"ListSwigTokenBalancesResult",
|
|
113
|
+
"ListSwigTokenTransactionsResult",
|
|
114
|
+
"Network",
|
|
115
|
+
"OneBusinessGrantAccessCallbackError",
|
|
116
|
+
"OneBusinessGrantAccessResult",
|
|
117
|
+
"OneBusinessLocation",
|
|
118
|
+
"PasskeySigningFn",
|
|
119
|
+
"PasskeySigningResult",
|
|
120
|
+
"PaymasterBalance",
|
|
121
|
+
"PaymasterClient",
|
|
122
|
+
"Policy",
|
|
123
|
+
"PreparedRecoverySetupResult",
|
|
124
|
+
"PreparedTransaction",
|
|
125
|
+
"PreparedTransactionSigner",
|
|
126
|
+
"PreparedTransactionsResult",
|
|
127
|
+
"ProxyResponse",
|
|
128
|
+
"ProxyWalletReference",
|
|
129
|
+
"QuoteRampArgs",
|
|
130
|
+
"QuoteRampResult",
|
|
131
|
+
"RampClient",
|
|
132
|
+
"RampCustomerContext",
|
|
133
|
+
"RampPaymentMethodType",
|
|
134
|
+
"RampQuote",
|
|
135
|
+
"RampServiceProvider",
|
|
136
|
+
"RampTransaction",
|
|
137
|
+
"RampTransactionStatus",
|
|
138
|
+
"RampTransactionType",
|
|
139
|
+
"RampWalletContext",
|
|
140
|
+
"RecoveryOptions",
|
|
141
|
+
"RecoverySetupPlan",
|
|
142
|
+
"RetryOptions",
|
|
143
|
+
"Secp256k1SigningFn",
|
|
144
|
+
"Secp256k1SigningResult",
|
|
145
|
+
"SignedPreparedTransaction",
|
|
146
|
+
"SolanaAccountMeta",
|
|
147
|
+
"SolanaInstructionInput",
|
|
148
|
+
"SponsorSignedTransactionArgs",
|
|
149
|
+
"SubmittedTransaction",
|
|
150
|
+
"SwigClient",
|
|
151
|
+
"SwigDeveloperSdkError",
|
|
152
|
+
"SwigProxyConfig",
|
|
153
|
+
"SwigProxyHandler",
|
|
154
|
+
"SwigRouteContext",
|
|
155
|
+
"SwigServerClient",
|
|
156
|
+
"SwigTokenBalance",
|
|
157
|
+
"SwigTokenTransaction",
|
|
158
|
+
"SwigUsdBalance",
|
|
159
|
+
"TransactionsClient",
|
|
160
|
+
"TransferSolOperation",
|
|
161
|
+
"TransferTokenOperation",
|
|
162
|
+
"WalletAddressInfo",
|
|
163
|
+
"WalletAuthority",
|
|
164
|
+
"WalletHandle",
|
|
165
|
+
"WalletReference",
|
|
166
|
+
"WalletsClient",
|
|
167
|
+
"WebAuthnAssertion",
|
|
168
|
+
"build_one_business_grant_access_url",
|
|
169
|
+
"complete_one_business_grant_access",
|
|
170
|
+
"create_secp256k1_evm_signing_fn",
|
|
171
|
+
"create_secp256r1_passkey_signing_fn",
|
|
172
|
+
"create_swig_proxy_handler",
|
|
173
|
+
"ramp_customer",
|
|
174
|
+
"redirect_to_one_business_grant_access",
|
|
175
|
+
"secp256r1_der_to_raw_signature",
|
|
176
|
+
"sign_prepared_swig_transaction",
|
|
177
|
+
"sign_prepared_swig_transactions",
|
|
178
|
+
"sign_prepared_transaction",
|
|
179
|
+
"sign_prepared_transaction_with_signer",
|
|
180
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .common import DEFAULT_BACKEND_URL, Network, RetryOptions
|
|
6
|
+
from .core import HttpClient
|
|
7
|
+
from .paymaster import PaymasterClient
|
|
8
|
+
from .ramp import RampClient
|
|
9
|
+
from .transactions import TransactionsClient
|
|
10
|
+
from .wallets import WalletsClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SwigClient:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
api_key: str,
|
|
18
|
+
base_url: str = DEFAULT_BACKEND_URL,
|
|
19
|
+
network: Network | None = None,
|
|
20
|
+
retry_options: RetryOptions | None = None,
|
|
21
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.default_network = network
|
|
24
|
+
http = HttpClient(
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
base_url=base_url,
|
|
27
|
+
retry=retry_options or RetryOptions(),
|
|
28
|
+
transport=transport,
|
|
29
|
+
)
|
|
30
|
+
self.paymaster = PaymasterClient(http, network)
|
|
31
|
+
self.ramp = RampClient(http, network)
|
|
32
|
+
self.transactions = TransactionsClient(http, network)
|
|
33
|
+
self.wallets = WalletsClient(http, network)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
SwigServerClient = SwigClient
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal, TypeAlias
|
|
6
|
+
|
|
7
|
+
Network: TypeAlias = Literal["mainnet", "devnet"]
|
|
8
|
+
Amount: TypeAlias = int | str
|
|
9
|
+
JsonScalar: TypeAlias = str | int | float | bool | None
|
|
10
|
+
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
|
11
|
+
JsonObject: TypeAlias = dict[str, JsonValue]
|
|
12
|
+
WalletAuthority: TypeAlias = Mapping[str, Mapping[str, object]]
|
|
13
|
+
|
|
14
|
+
DEFAULT_BACKEND_URL = "https://backend.prod.infra.onswig.com"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class RetryOptions:
|
|
19
|
+
max_retries: int = 3
|
|
20
|
+
retry_delay: float = 1.0
|
|
21
|
+
backoff_multiplier: float = 2.0
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if self.max_retries < 0:
|
|
25
|
+
raise ValueError("max_retries must be non-negative")
|
|
26
|
+
if self.retry_delay < 0:
|
|
27
|
+
raise ValueError("retry_delay must be non-negative")
|
|
28
|
+
if self.backoff_multiplier < 0:
|
|
29
|
+
raise ValueError("backoff_multiplier must be non-negative")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class WalletAddressInfo:
|
|
34
|
+
swig_config_address: str
|
|
35
|
+
wallet_address: str
|
|
36
|
+
network: Network | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def wallet_address_info_from_wire(value: object) -> WalletAddressInfo:
|
|
40
|
+
if not isinstance(value, Mapping):
|
|
41
|
+
raise ValueError("Response is missing wallet")
|
|
42
|
+
return WalletAddressInfo(
|
|
43
|
+
swig_config_address=_required_string(
|
|
44
|
+
value.get("swigConfigAddress", value.get("swig_config_address")),
|
|
45
|
+
"swigConfigAddress",
|
|
46
|
+
),
|
|
47
|
+
wallet_address=_required_string(
|
|
48
|
+
value.get("walletAddress", value.get("wallet_address")),
|
|
49
|
+
"walletAddress",
|
|
50
|
+
),
|
|
51
|
+
network=normalize_network(value.get("network")),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def wallet_authority_to_wire(authority: WalletAuthority) -> dict[str, object]:
|
|
56
|
+
for scheme in ("ed25519", "secp256k1", "secp256r1"):
|
|
57
|
+
value = authority.get(scheme)
|
|
58
|
+
if value is None:
|
|
59
|
+
continue
|
|
60
|
+
public_key = value.get("publicKey", value.get("public_key"))
|
|
61
|
+
if not isinstance(public_key, str) or not public_key.strip():
|
|
62
|
+
raise ValueError(f"{scheme} authority requires publicKey")
|
|
63
|
+
return {scheme: {"publicKey": public_key}}
|
|
64
|
+
|
|
65
|
+
proof = authority.get("programExecProof", authority.get("program_exec_proof"))
|
|
66
|
+
if proof is not None:
|
|
67
|
+
role_id = proof.get("roleId", proof.get("role_id"))
|
|
68
|
+
zk_proof = proof.get("zkProof", proof.get("zk_proof"))
|
|
69
|
+
if not isinstance(role_id, int) or not isinstance(zk_proof, str):
|
|
70
|
+
raise ValueError("programExecProof requires roleId and zkProof")
|
|
71
|
+
return {"programExecProof": {"roleId": role_id, "zkProof": zk_proof}}
|
|
72
|
+
|
|
73
|
+
raise ValueError("authority must include a supported authority")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def normalize_amount(amount: Amount) -> str:
|
|
77
|
+
return str(amount)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_proto_network(network: Network) -> str:
|
|
81
|
+
if network == "devnet":
|
|
82
|
+
return "NETWORK_DEVNET"
|
|
83
|
+
return "NETWORK_MAINNET"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def normalize_network(value: object) -> Network | None:
|
|
87
|
+
if value in ("devnet", "NETWORK_DEVNET", 1):
|
|
88
|
+
return "devnet"
|
|
89
|
+
if value in ("mainnet", "NETWORK_MAINNET", 2):
|
|
90
|
+
return "mainnet"
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def require_network(*values: Network | None) -> Network:
|
|
95
|
+
for value in values:
|
|
96
|
+
if value is not None:
|
|
97
|
+
return value
|
|
98
|
+
raise ValueError("network is required")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _required_string(value: object, field: str) -> str:
|
|
102
|
+
if isinstance(value, str):
|
|
103
|
+
return value
|
|
104
|
+
raise ValueError(f"Response is missing {field}")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import TypeVar, cast
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .common import JsonValue, RetryOptions
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SwigDeveloperSdkError(Exception):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
code: str,
|
|
19
|
+
status_code: int,
|
|
20
|
+
details: object | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.code = code
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.details = details
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_response(
|
|
29
|
+
cls,
|
|
30
|
+
response: httpx.Response,
|
|
31
|
+
body: object | None,
|
|
32
|
+
) -> SwigDeveloperSdkError:
|
|
33
|
+
if isinstance(body, Mapping):
|
|
34
|
+
nested = body.get("error")
|
|
35
|
+
if isinstance(nested, Mapping):
|
|
36
|
+
details = nested.get("details")
|
|
37
|
+
return cls(
|
|
38
|
+
_optional_string(nested.get("message"))
|
|
39
|
+
or f"Request failed with status {response.status_code}",
|
|
40
|
+
_optional_string(nested.get("code"))
|
|
41
|
+
or f"HTTP_{response.status_code}",
|
|
42
|
+
response.status_code,
|
|
43
|
+
details if details is not None else body,
|
|
44
|
+
)
|
|
45
|
+
details = body.get("details")
|
|
46
|
+
return cls(
|
|
47
|
+
_optional_string(body.get("message"))
|
|
48
|
+
or f"Request failed with status {response.status_code}",
|
|
49
|
+
_optional_string(body.get("code")) or f"HTTP_{response.status_code}",
|
|
50
|
+
response.status_code,
|
|
51
|
+
details if details is not None else body,
|
|
52
|
+
)
|
|
53
|
+
return cls(
|
|
54
|
+
f"Request failed with status {response.status_code}",
|
|
55
|
+
f"HTTP_{response.status_code}",
|
|
56
|
+
response.status_code,
|
|
57
|
+
body,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class HttpClient:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
api_key: str,
|
|
66
|
+
base_url: str,
|
|
67
|
+
retry: RetryOptions,
|
|
68
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
self._api_key = api_key
|
|
71
|
+
self._base_url = base_url.rstrip("/")
|
|
72
|
+
self._retry = retry
|
|
73
|
+
self._transport = transport
|
|
74
|
+
|
|
75
|
+
async def get(self, path: str) -> object:
|
|
76
|
+
return await self._request(path, method="GET")
|
|
77
|
+
|
|
78
|
+
async def post(self, path: str, body: Mapping[str, object]) -> object:
|
|
79
|
+
compacted = _compact(body)
|
|
80
|
+
if not isinstance(compacted, Mapping):
|
|
81
|
+
raise TypeError("Request body must be an object")
|
|
82
|
+
return await self._request(path, method="POST", body=compacted)
|
|
83
|
+
|
|
84
|
+
async def _request(
|
|
85
|
+
self,
|
|
86
|
+
path: str,
|
|
87
|
+
*,
|
|
88
|
+
method: str,
|
|
89
|
+
body: Mapping[str, object] | None = None,
|
|
90
|
+
) -> object:
|
|
91
|
+
last_error: SwigDeveloperSdkError | None = None
|
|
92
|
+
for attempt in range(self._retry.max_retries + 1):
|
|
93
|
+
try:
|
|
94
|
+
async with httpx.AsyncClient(transport=self._transport) as client:
|
|
95
|
+
response = await client.request(
|
|
96
|
+
method,
|
|
97
|
+
f"{self._base_url}{path}",
|
|
98
|
+
headers={
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
101
|
+
},
|
|
102
|
+
json=body,
|
|
103
|
+
)
|
|
104
|
+
response_body = _parse_response_body(response)
|
|
105
|
+
if 200 <= response.status_code < 300:
|
|
106
|
+
return _unwrap_data(response_body)
|
|
107
|
+
error = SwigDeveloperSdkError.from_response(response, response_body)
|
|
108
|
+
if 400 <= response.status_code < 500:
|
|
109
|
+
raise error
|
|
110
|
+
last_error = error
|
|
111
|
+
except SwigDeveloperSdkError as error:
|
|
112
|
+
if 400 <= error.status_code < 500:
|
|
113
|
+
raise
|
|
114
|
+
last_error = error
|
|
115
|
+
except httpx.HTTPError as error:
|
|
116
|
+
last_error = SwigDeveloperSdkError(str(error), "NETWORK_ERROR", 0)
|
|
117
|
+
except Exception as error:
|
|
118
|
+
last_error = SwigDeveloperSdkError(str(error), "NETWORK_ERROR", 0)
|
|
119
|
+
|
|
120
|
+
if attempt < self._retry.max_retries:
|
|
121
|
+
await asyncio.sleep(
|
|
122
|
+
self._retry.retry_delay * self._retry.backoff_multiplier**attempt
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if last_error is not None:
|
|
126
|
+
raise last_error
|
|
127
|
+
raise SwigDeveloperSdkError(
|
|
128
|
+
f"Request failed after {self._retry.max_retries} retries",
|
|
129
|
+
"RETRY_EXHAUSTED",
|
|
130
|
+
0,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_response_body(response: httpx.Response) -> object:
|
|
135
|
+
if not response.content:
|
|
136
|
+
return None
|
|
137
|
+
try:
|
|
138
|
+
return cast(JsonValue, response.json())
|
|
139
|
+
except ValueError:
|
|
140
|
+
return response.text
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _unwrap_data(body: object) -> object:
|
|
144
|
+
if isinstance(body, Mapping) and "data" in body:
|
|
145
|
+
return body["data"]
|
|
146
|
+
return body
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _optional_string(value: object) -> str | None:
|
|
150
|
+
return value if isinstance(value, str) else None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _compact(value: object) -> object:
|
|
154
|
+
if isinstance(value, Mapping):
|
|
155
|
+
return {
|
|
156
|
+
str(key): _compact(item) for key, item in value.items() if item is not None
|
|
157
|
+
}
|
|
158
|
+
if isinstance(value, list):
|
|
159
|
+
return [_compact(item) for item in value]
|
|
160
|
+
if isinstance(value, tuple):
|
|
161
|
+
return [_compact(item) for item in value]
|
|
162
|
+
return value
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Mapping, Sequence
|
|
5
|
+
from typing import Protocol, cast
|
|
6
|
+
|
|
7
|
+
from .signing import Secp256k1SigningFn, Secp256k1SigningResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Eip1193Provider(Protocol):
|
|
11
|
+
def request(self, args: Mapping[str, object]) -> object: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_secp256k1_evm_signing_fn(
|
|
15
|
+
*,
|
|
16
|
+
provider: Eip1193Provider,
|
|
17
|
+
address: str,
|
|
18
|
+
) -> Secp256k1SigningFn:
|
|
19
|
+
async def sign(message: bytes) -> Secp256k1SigningResult:
|
|
20
|
+
result = provider.request(
|
|
21
|
+
{
|
|
22
|
+
"method": "personal_sign",
|
|
23
|
+
"params": [_bytes_to_hex(message), address],
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
if inspect.isawaitable(result):
|
|
27
|
+
result = await cast(Awaitable[object], result)
|
|
28
|
+
if not isinstance(result, str):
|
|
29
|
+
raise ValueError("EVM provider returned a non-string signature")
|
|
30
|
+
return Secp256k1SigningResult(
|
|
31
|
+
signature=_parse_evm_signature(result),
|
|
32
|
+
prefix=f"\x19Ethereum Signed Message:\n{len(message)}".encode("ascii"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return sign
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_evm_signature(signature: str) -> bytes:
|
|
39
|
+
normalized = signature[2:] if signature.startswith("0x") else signature
|
|
40
|
+
if not normalized or any(
|
|
41
|
+
character not in "0123456789abcdefABCDEF" for character in normalized
|
|
42
|
+
):
|
|
43
|
+
raise ValueError("Invalid hex string")
|
|
44
|
+
if len(normalized) % 2:
|
|
45
|
+
raise ValueError("Hex strings must contain an even number of characters")
|
|
46
|
+
value = bytearray.fromhex(normalized)
|
|
47
|
+
if len(value) != 65:
|
|
48
|
+
raise ValueError("EVM signature must be 65 bytes")
|
|
49
|
+
value[64] = _normalize_recovery_byte(value[64])
|
|
50
|
+
return bytes(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _normalize_recovery_byte(value: int) -> int:
|
|
54
|
+
if value in (0, 1):
|
|
55
|
+
return value + 27
|
|
56
|
+
if value in (27, 28):
|
|
57
|
+
return value
|
|
58
|
+
if value >= 35:
|
|
59
|
+
return ((value - 35) % 2) + 27
|
|
60
|
+
raise ValueError("EVM signature has an invalid recovery byte")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _bytes_to_hex(value: Sequence[int]) -> str:
|
|
64
|
+
return "0x" + bytes(value).hex()
|