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.
@@ -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()