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,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Protocol
|
|
7
|
+
from urllib.parse import parse_qs, urlencode, urljoin, urlparse
|
|
8
|
+
|
|
9
|
+
from .common import JsonObject
|
|
10
|
+
|
|
11
|
+
DEFAULT_ONE_BUSINESS_URL = "https://swig-one-business.vercel.app"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class OneBusinessGrantAccessResult:
|
|
16
|
+
status: str
|
|
17
|
+
swig_pubkey: str
|
|
18
|
+
wallet_address: str
|
|
19
|
+
role_id: int
|
|
20
|
+
authority_public_key: str
|
|
21
|
+
signature: str | None = None
|
|
22
|
+
state: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OneBusinessGrantAccessCallbackError(Exception):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
code: str,
|
|
30
|
+
description: str | None = None,
|
|
31
|
+
state: str | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(description or code)
|
|
34
|
+
self.code = code
|
|
35
|
+
self.description = description
|
|
36
|
+
self.state = state
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OneBusinessLocation(Protocol):
|
|
40
|
+
def assign(self, url: str) -> None: ...
|
|
41
|
+
|
|
42
|
+
def replace(self, url: str) -> None: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_one_business_grant_access_url(
|
|
46
|
+
*,
|
|
47
|
+
swig_pubkey: str,
|
|
48
|
+
authority_public_key: str,
|
|
49
|
+
actions: list[JsonObject] | None = None,
|
|
50
|
+
app_name: str | None = None,
|
|
51
|
+
one_business_url: str = DEFAULT_ONE_BUSINESS_URL,
|
|
52
|
+
redirect_uri: str | None = None,
|
|
53
|
+
state: str | None = None,
|
|
54
|
+
) -> str:
|
|
55
|
+
base_url = one_business_url.strip()
|
|
56
|
+
if not base_url:
|
|
57
|
+
raise ValueError("one_business_url is required")
|
|
58
|
+
if not swig_pubkey.strip() or not authority_public_key.strip():
|
|
59
|
+
raise ValueError("swig_pubkey and authority_public_key are required")
|
|
60
|
+
query: dict[str, str] = {
|
|
61
|
+
"swig_pubkey": swig_pubkey.strip(),
|
|
62
|
+
"authority_public_key": authority_public_key.strip(),
|
|
63
|
+
}
|
|
64
|
+
if app_name is not None:
|
|
65
|
+
query["app_name"] = app_name
|
|
66
|
+
if redirect_uri is not None:
|
|
67
|
+
query["redirect_uri"] = redirect_uri
|
|
68
|
+
if state is not None:
|
|
69
|
+
query["state"] = state
|
|
70
|
+
if actions is not None:
|
|
71
|
+
encoded = base64.urlsafe_b64encode(
|
|
72
|
+
json.dumps(actions, separators=(",", ":")).encode("utf-8")
|
|
73
|
+
).decode("ascii")
|
|
74
|
+
query["actions"] = encoded.rstrip("=")
|
|
75
|
+
normalized_base = base_url if base_url.endswith("/") else f"{base_url}/"
|
|
76
|
+
return f"{urljoin(normalized_base, 'authorize/grant-access')}?{urlencode(query)}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def redirect_to_one_business_grant_access(
|
|
80
|
+
*,
|
|
81
|
+
location: OneBusinessLocation,
|
|
82
|
+
swig_pubkey: str,
|
|
83
|
+
authority_public_key: str,
|
|
84
|
+
actions: list[JsonObject] | None = None,
|
|
85
|
+
app_name: str | None = None,
|
|
86
|
+
one_business_url: str = DEFAULT_ONE_BUSINESS_URL,
|
|
87
|
+
redirect_uri: str | None = None,
|
|
88
|
+
state: str | None = None,
|
|
89
|
+
mode: Literal["assign", "replace"] = "assign",
|
|
90
|
+
) -> None:
|
|
91
|
+
url = build_one_business_grant_access_url(
|
|
92
|
+
swig_pubkey=swig_pubkey,
|
|
93
|
+
authority_public_key=authority_public_key,
|
|
94
|
+
actions=actions,
|
|
95
|
+
app_name=app_name,
|
|
96
|
+
one_business_url=one_business_url,
|
|
97
|
+
redirect_uri=redirect_uri,
|
|
98
|
+
state=state,
|
|
99
|
+
)
|
|
100
|
+
if mode == "replace":
|
|
101
|
+
location.replace(url)
|
|
102
|
+
return
|
|
103
|
+
location.assign(url)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def complete_one_business_grant_access(
|
|
107
|
+
callback_url: str,
|
|
108
|
+
) -> OneBusinessGrantAccessResult:
|
|
109
|
+
params = parse_qs(urlparse(callback_url).query, keep_blank_values=True)
|
|
110
|
+
error_code = _read_param(params, "error")
|
|
111
|
+
state = _read_param(params, "state")
|
|
112
|
+
if error_code:
|
|
113
|
+
raise OneBusinessGrantAccessCallbackError(
|
|
114
|
+
code=error_code,
|
|
115
|
+
description=_read_param(params, "error_description"),
|
|
116
|
+
state=state,
|
|
117
|
+
)
|
|
118
|
+
swig_pubkey = _required_param(params, "swig_pubkey")
|
|
119
|
+
wallet_address = _required_param(params, "wallet_address")
|
|
120
|
+
authority_public_key = _required_param(params, "authority_public_key")
|
|
121
|
+
role_id_value = _required_param(params, "role_id")
|
|
122
|
+
try:
|
|
123
|
+
role_id = int(role_id_value)
|
|
124
|
+
except ValueError as parse_error:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"Grant access callback has an invalid role_id"
|
|
127
|
+
) from parse_error
|
|
128
|
+
if role_id < 0:
|
|
129
|
+
raise ValueError("Grant access callback has an invalid role_id")
|
|
130
|
+
return OneBusinessGrantAccessResult(
|
|
131
|
+
status=(
|
|
132
|
+
_read_param(params, "status")
|
|
133
|
+
or _read_param(params, "grant_status")
|
|
134
|
+
or "granted"
|
|
135
|
+
),
|
|
136
|
+
swig_pubkey=swig_pubkey,
|
|
137
|
+
wallet_address=wallet_address,
|
|
138
|
+
role_id=role_id,
|
|
139
|
+
authority_public_key=authority_public_key,
|
|
140
|
+
signature=_read_param(params, "signature"),
|
|
141
|
+
state=state,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _required_param(params: dict[str, list[str]], name: str) -> str:
|
|
146
|
+
value = _read_param(params, name)
|
|
147
|
+
if not value:
|
|
148
|
+
raise ValueError(f"Grant access callback is missing {name}")
|
|
149
|
+
return value
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _read_param(params: dict[str, list[str]], name: str) -> str | None:
|
|
153
|
+
camel = _snake_to_camel(name)
|
|
154
|
+
for key in (name, camel):
|
|
155
|
+
values = params.get(key)
|
|
156
|
+
if values and values[0].strip():
|
|
157
|
+
return values[0].strip()
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _snake_to_camel(value: str) -> str:
|
|
162
|
+
head, *tail = value.split("_")
|
|
163
|
+
return head + "".join(part.capitalize() for part in tail)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import inspect
|
|
5
|
+
import struct
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TypeAlias
|
|
9
|
+
|
|
10
|
+
from .signing import PasskeySigningFn, PasskeySigningResult
|
|
11
|
+
|
|
12
|
+
R1_AUTHENTICATION_TYPE_WEBAUTHN_RAW_CLIENT_DATA_JSON = 2
|
|
13
|
+
SECP256R1_SCALAR_SIZE = 32
|
|
14
|
+
P256_ORDER = int(
|
|
15
|
+
"ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551",
|
|
16
|
+
16,
|
|
17
|
+
)
|
|
18
|
+
P256_HALF_ORDER = P256_ORDER >> 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class WebAuthnAssertion:
|
|
23
|
+
authenticator_data: bytes
|
|
24
|
+
client_data_json: bytes
|
|
25
|
+
signature: bytes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
WebAuthnAssertionFn: TypeAlias = Callable[
|
|
29
|
+
[bytes], WebAuthnAssertion | Awaitable[WebAuthnAssertion]
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_secp256r1_passkey_signing_fn(
|
|
34
|
+
get_assertion: WebAuthnAssertionFn,
|
|
35
|
+
) -> PasskeySigningFn:
|
|
36
|
+
async def sign(message: bytes) -> PasskeySigningResult:
|
|
37
|
+
assertion = get_assertion(message)
|
|
38
|
+
if inspect.isawaitable(assertion):
|
|
39
|
+
assertion = await assertion
|
|
40
|
+
client_data_hash = hashlib.sha256(assertion.client_data_json).digest()
|
|
41
|
+
return PasskeySigningResult(
|
|
42
|
+
signature=secp256r1_der_to_raw_signature(assertion.signature),
|
|
43
|
+
prefix=_raw_webauthn_prefix(
|
|
44
|
+
assertion.client_data_json,
|
|
45
|
+
assertion.authenticator_data,
|
|
46
|
+
),
|
|
47
|
+
message=assertion.authenticator_data + client_data_hash,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return sign
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def secp256r1_der_to_raw_signature(der_signature: bytes) -> bytes:
|
|
54
|
+
r, s = _parse_der_signature(der_signature)
|
|
55
|
+
if s > P256_HALF_ORDER:
|
|
56
|
+
s = P256_ORDER - s
|
|
57
|
+
return r.to_bytes(SECP256R1_SCALAR_SIZE, "big") + s.to_bytes(
|
|
58
|
+
SECP256R1_SCALAR_SIZE, "big"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _raw_webauthn_prefix(client_json: bytes, auth_data: bytes) -> bytes:
|
|
63
|
+
if len(auth_data) > 0xFFFF:
|
|
64
|
+
raise ValueError("WebAuthn authenticatorData is too large")
|
|
65
|
+
if len(client_json) > 0xFFFF:
|
|
66
|
+
raise ValueError("WebAuthn clientDataJSON is too large")
|
|
67
|
+
return b"".join(
|
|
68
|
+
(
|
|
69
|
+
struct.pack(
|
|
70
|
+
"<HH",
|
|
71
|
+
R1_AUTHENTICATION_TYPE_WEBAUTHN_RAW_CLIENT_DATA_JSON,
|
|
72
|
+
len(auth_data),
|
|
73
|
+
),
|
|
74
|
+
auth_data,
|
|
75
|
+
struct.pack("<H", len(client_json)),
|
|
76
|
+
client_json,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_der_signature(value: bytes) -> tuple[int, int]:
|
|
82
|
+
offset = 0
|
|
83
|
+
if not value or value[offset] != 0x30:
|
|
84
|
+
raise ValueError("Invalid secp256r1 DER signature")
|
|
85
|
+
sequence_length, offset = _read_der_length(value, offset + 1)
|
|
86
|
+
if offset + sequence_length != len(value):
|
|
87
|
+
raise ValueError("Invalid secp256r1 DER signature length")
|
|
88
|
+
r_bytes, offset = _read_der_integer(value, offset)
|
|
89
|
+
s_bytes, offset = _read_der_integer(value, offset)
|
|
90
|
+
if offset != len(value):
|
|
91
|
+
raise ValueError("Invalid secp256r1 DER signature trailing data")
|
|
92
|
+
return int.from_bytes(r_bytes, "big"), int.from_bytes(s_bytes, "big")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_der_integer(value: bytes, offset: int) -> tuple[bytes, int]:
|
|
96
|
+
if offset >= len(value) or value[offset] != 0x02:
|
|
97
|
+
raise ValueError("Invalid secp256r1 DER signature integer")
|
|
98
|
+
length, start = _read_der_length(value, offset + 1)
|
|
99
|
+
end = start + length
|
|
100
|
+
if length == 0 or end > len(value):
|
|
101
|
+
raise ValueError("Invalid secp256r1 DER signature integer length")
|
|
102
|
+
integer = value[start:end]
|
|
103
|
+
if integer[0] == 0 and len(integer) > 1:
|
|
104
|
+
integer = integer[1:]
|
|
105
|
+
if len(integer) > SECP256R1_SCALAR_SIZE:
|
|
106
|
+
raise ValueError("Secp256r1 signature integer is too large")
|
|
107
|
+
return integer, end
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_der_length(value: bytes, offset: int) -> tuple[int, int]:
|
|
111
|
+
if offset >= len(value):
|
|
112
|
+
raise ValueError("Invalid secp256r1 DER signature length")
|
|
113
|
+
first = value[offset]
|
|
114
|
+
if first < 0x80:
|
|
115
|
+
return first, offset + 1
|
|
116
|
+
length_bytes = first & 0x7F
|
|
117
|
+
if length_bytes == 0 or length_bytes > 2:
|
|
118
|
+
raise ValueError("Unsupported secp256r1 DER signature length")
|
|
119
|
+
end = offset + 1 + length_bytes
|
|
120
|
+
if end > len(value):
|
|
121
|
+
raise ValueError("Invalid secp256r1 DER signature length")
|
|
122
|
+
return int.from_bytes(value[offset + 1 : end], "big"), end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal, TypeAlias
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
from .common import Network
|
|
9
|
+
from .core import HttpClient
|
|
10
|
+
|
|
11
|
+
PaymasterKind: TypeAlias = Literal["api", "idp", "unspecified"]
|
|
12
|
+
PaymasterBalanceKind: TypeAlias = Literal["api", "idp"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class PaymasterBalance:
|
|
17
|
+
configured: bool
|
|
18
|
+
kind: PaymasterKind
|
|
19
|
+
id: str
|
|
20
|
+
address: str
|
|
21
|
+
label: str
|
|
22
|
+
balance_lamports: str
|
|
23
|
+
balance_sol: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PaymasterClient:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
http: HttpClient,
|
|
30
|
+
default_network: Network | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._http = http
|
|
33
|
+
self._default_network = default_network
|
|
34
|
+
|
|
35
|
+
async def get_balance(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
network: Network | None = None,
|
|
39
|
+
kind: PaymasterBalanceKind | None = None,
|
|
40
|
+
) -> PaymasterBalance:
|
|
41
|
+
query: dict[str, str] = {}
|
|
42
|
+
resolved_network = network or self._default_network
|
|
43
|
+
if resolved_network is not None:
|
|
44
|
+
query["network"] = resolved_network
|
|
45
|
+
if kind is not None:
|
|
46
|
+
query["kind"] = (
|
|
47
|
+
"PAYMASTER_KIND_API" if kind == "api" else "PAYMASTER_KIND_IDP"
|
|
48
|
+
)
|
|
49
|
+
suffix = f"?{urlencode(query)}" if query else ""
|
|
50
|
+
return normalize_paymaster_balance(
|
|
51
|
+
await self._http.get(f"/paymaster/balance{suffix}")
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def get_idp_balance(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
network: Network | None = None,
|
|
58
|
+
) -> PaymasterBalance:
|
|
59
|
+
return await self.get_balance(network=network, kind="idp")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_paymaster_balance(value: object) -> PaymasterBalance:
|
|
63
|
+
if not isinstance(value, Mapping):
|
|
64
|
+
raise ValueError("Paymaster balance response must be an object")
|
|
65
|
+
configured = value.get("configured", False)
|
|
66
|
+
if not isinstance(configured, bool):
|
|
67
|
+
raise ValueError("Paymaster balance response has invalid configured")
|
|
68
|
+
return PaymasterBalance(
|
|
69
|
+
configured=configured,
|
|
70
|
+
kind=_normalize_kind(value.get("kind")),
|
|
71
|
+
id=_optional_string(value.get("id")) or "",
|
|
72
|
+
address=_optional_string(value.get("address")) or "",
|
|
73
|
+
label=_optional_string(value.get("label")) or "",
|
|
74
|
+
balance_lamports=str(
|
|
75
|
+
value.get("balanceLamports", value.get("balance_lamports", "0"))
|
|
76
|
+
),
|
|
77
|
+
balance_sol=_number(
|
|
78
|
+
value.get("balanceSol", value.get("balance_sol", 0)),
|
|
79
|
+
"balanceSol",
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_kind(value: object) -> PaymasterKind:
|
|
85
|
+
if value in ("api", "PAYMASTER_KIND_API", 1):
|
|
86
|
+
return "api"
|
|
87
|
+
if value in ("idp", "PAYMASTER_KIND_IDP", 2):
|
|
88
|
+
return "idp"
|
|
89
|
+
if value in (None, "unspecified", "PAYMASTER_KIND_UNSPECIFIED", 0):
|
|
90
|
+
return "unspecified"
|
|
91
|
+
raise ValueError("Paymaster balance response has invalid kind")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _number(value: object, field: str) -> float:
|
|
95
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
96
|
+
return float(value)
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
try:
|
|
99
|
+
return float(value)
|
|
100
|
+
except ValueError:
|
|
101
|
+
pass
|
|
102
|
+
raise ValueError(f"Paymaster balance response is missing {field}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _optional_string(value: object) -> str | None:
|
|
106
|
+
return value if isinstance(value, str) else None
|