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,724 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import os
5
+ import re
6
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
7
+ from dataclasses import dataclass, fields, is_dataclass
8
+ from typing import Literal, TypeAlias, TypeVar, cast
9
+ from urllib.parse import unquote
10
+
11
+ import httpx
12
+
13
+ from .client import SwigClient
14
+ from .common import DEFAULT_BACKEND_URL, Network, WalletAuthority, normalize_network
15
+ from .paymaster import PaymasterBalanceKind
16
+ from .ramp import (
17
+ ActiveRampDirection,
18
+ ActiveRampTransactionStatus,
19
+ CreateRampSessionArgs,
20
+ QuoteRampArgs,
21
+ RampCustomerContext,
22
+ RampPaymentMethodType,
23
+ RampServiceProvider,
24
+ RampWalletContext,
25
+ )
26
+ from .wallets import (
27
+ RecoveryOptions,
28
+ TransferSolOperation,
29
+ TransferTokenOperation,
30
+ WalletReference,
31
+ )
32
+
33
+ PostProxyRoute: TypeAlias = Literal[
34
+ "wallet/create",
35
+ "prepare",
36
+ "transfer/sol",
37
+ "transfer/spl-token",
38
+ "swap/jupiter",
39
+ "ramp/quote",
40
+ "ramp/sessions",
41
+ ]
42
+ ReadProxyRoute: TypeAlias = Literal[
43
+ "wallet/balance/usd",
44
+ "wallet/token-balances",
45
+ "wallet/token-transactions",
46
+ "paymaster/balance",
47
+ "ramp/options",
48
+ "ramp/transaction",
49
+ "ramp/wallet-transactions",
50
+ ]
51
+ ProxyRoute: TypeAlias = PostProxyRoute | ReadProxyRoute
52
+ T = TypeVar("T")
53
+ MaybeAwaitable: TypeAlias = T | Awaitable[T]
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class ProxyWalletReference(WalletReference):
58
+ role_id: int | None = None
59
+ authority_public_key: str | None = None
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class SwigRouteContext:
64
+ method: Literal["GET", "POST"]
65
+ path: str
66
+ route: ProxyRoute
67
+ body: Mapping[str, object]
68
+ query: Mapping[str, str]
69
+ wallet: ProxyWalletReference | None = None
70
+ network: Network | None = None
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class SwigProxyConfig:
75
+ api_key: str | None = None
76
+ transaction_api_url: str | None = None
77
+ base_url: str | None = None
78
+ network: Network | None = None
79
+ fee_payer: str | Callable[[SwigRouteContext], MaybeAwaitable[str | None]] | None = (
80
+ None
81
+ )
82
+ resolve_requester_authority: (
83
+ Callable[[SwigRouteContext], MaybeAwaitable[WalletAuthority | None]] | None
84
+ ) = None
85
+ resolve_requester_pubkey: (
86
+ Callable[[SwigRouteContext], MaybeAwaitable[str | None]] | None
87
+ ) = None
88
+ resolve_ramp_customer: (
89
+ Callable[[SwigRouteContext], MaybeAwaitable[RampCustomerContext | None]] | None
90
+ ) = None
91
+ transport: httpx.AsyncBaseTransport | None = None
92
+
93
+
94
+ @dataclass(frozen=True, slots=True)
95
+ class ProxyResponse:
96
+ status: int
97
+ body: Mapping[str, object]
98
+
99
+
100
+ class SwigProxyRouteError(Exception):
101
+ def __init__(self, message: str, status: int = 400) -> None:
102
+ super().__init__(message)
103
+ self.status = status
104
+
105
+
106
+ class SwigProxyHandler:
107
+ def __init__(self, config: SwigProxyConfig | None = None) -> None:
108
+ self._config = config or SwigProxyConfig()
109
+
110
+ async def handle(
111
+ self,
112
+ *,
113
+ method: Literal["GET", "POST"],
114
+ path: str,
115
+ body: Mapping[str, object] | None = None,
116
+ query: Mapping[str, str] | None = None,
117
+ ) -> ProxyResponse:
118
+ try:
119
+ if method == "POST":
120
+ value = await self._handle_post(path, body or {}, query or {})
121
+ elif method == "GET":
122
+ value = await self._handle_get(path, query or {})
123
+ else:
124
+ raise SwigProxyRouteError("Unsupported method", 405)
125
+ serialized = _to_wire(value)
126
+ if not isinstance(serialized, Mapping):
127
+ raise TypeError("Proxy response must be an object")
128
+ return ProxyResponse(200, serialized)
129
+ except SwigProxyRouteError as error:
130
+ return ProxyResponse(error.status, {"error": str(error)})
131
+ except Exception as error:
132
+ return ProxyResponse(400, {"error": str(error)})
133
+
134
+ async def _handle_post(
135
+ self,
136
+ path: str,
137
+ body: Mapping[str, object],
138
+ query: Mapping[str, str],
139
+ ) -> Mapping[str, object]:
140
+ route = _resolve_post_route(path)
141
+ network = _read_network(body.get("network")) or self._config.network
142
+ wallet = None if route.startswith("ramp/") else _read_wallet(body.get("wallet"))
143
+ context = SwigRouteContext(
144
+ method="POST",
145
+ path=path,
146
+ route=route,
147
+ body=body,
148
+ query=query,
149
+ wallet=wallet,
150
+ network=network,
151
+ )
152
+ swig = self._client(network)
153
+ if route == "wallet/create":
154
+ fee_payer = await self._fee_payer(context)
155
+ policy_id = _optional_string(body.get("policyId"))
156
+ initial_user = _read_authority(body.get("initialUser"))
157
+ if policy_id is None and initial_user is None:
158
+ raise SwigProxyRouteError("policyId or initialUser is required")
159
+ created = await swig.wallets.create(
160
+ fee_payer=fee_payer,
161
+ policy_id=policy_id,
162
+ initial_user=initial_user,
163
+ recovery=_read_recovery_options(body.get("recovery")),
164
+ network=network,
165
+ idempotency_key=_optional_string(body.get("idempotencyKey")),
166
+ )
167
+ if not created.transactions:
168
+ raise SwigProxyRouteError(
169
+ "Wallet creation response is missing transaction", 502
170
+ )
171
+ return {"prepared": created}
172
+ if route == "ramp/quote":
173
+ customer = await self._ramp_customer(context)
174
+ return cast(
175
+ Mapping[str, object],
176
+ _to_wire(await swig.ramp.quote(_quote_args(body, customer))),
177
+ )
178
+ if route == "ramp/sessions":
179
+ customer = await self._ramp_customer(context)
180
+ return cast(
181
+ Mapping[str, object],
182
+ _to_wire(await swig.ramp.create_session(_session_args(body, customer))),
183
+ )
184
+
185
+ required_wallet = _require_wallet(wallet)
186
+ requester_authority = await self._requester_authority(context)
187
+ fee_payer = await self._fee_payer(context)
188
+ handle = swig.wallets.use(
189
+ required_wallet,
190
+ network=network,
191
+ requester_authority=requester_authority,
192
+ )
193
+ idempotency_key = _optional_string(body.get("idempotencyKey"))
194
+ prepared: object
195
+ if route == "prepare":
196
+ prepared = await handle.prepare(
197
+ fee_payer=fee_payer,
198
+ requester_authority=requester_authority,
199
+ network=network,
200
+ idempotency_key=idempotency_key,
201
+ operations=_read_operations(body.get("operations")),
202
+ )
203
+ elif route == "transfer/sol":
204
+ prepared = await handle.transfer.sol(
205
+ fee_payer=fee_payer,
206
+ requester_authority=requester_authority,
207
+ network=network,
208
+ idempotency_key=idempotency_key,
209
+ destination=_required_string(body, "destination"),
210
+ amount=_read_amount(body),
211
+ )
212
+ elif route == "transfer/spl-token":
213
+ prepared = await handle.transfer.token(
214
+ fee_payer=fee_payer,
215
+ requester_authority=requester_authority,
216
+ network=network,
217
+ idempotency_key=idempotency_key,
218
+ mint=_required_string(body, "mint"),
219
+ destination_owner=_required_string(body, "destinationOwner"),
220
+ amount=_read_amount(body),
221
+ )
222
+ else:
223
+ prepared = await handle.swap.jupiter(
224
+ fee_payer=fee_payer,
225
+ requester_authority=requester_authority,
226
+ network=network,
227
+ idempotency_key=idempotency_key,
228
+ input_mint=_required_string(body, "inputMint"),
229
+ output_mint=_required_string(body, "outputMint"),
230
+ amount=_read_amount(body),
231
+ slippage_bps=_optional_int(body.get("slippageBps")),
232
+ destination_account=_optional_string(body.get("destinationAccount")),
233
+ wrap_and_unwrap_sol=_optional_bool(body.get("wrapAndUnwrapSol")),
234
+ tip_amount_lamports=_optional_string(body.get("tipAmountLamports")),
235
+ compute_unit_price_percentile=_optional_string(
236
+ body.get("computeUnitPricePercentile")
237
+ ),
238
+ max_accounts=_optional_int(body.get("maxAccounts")),
239
+ mode=_optional_string(body.get("mode")),
240
+ blockhash_slots_to_expiry=_optional_int(
241
+ body.get("blockhashSlotsToExpiry")
242
+ ),
243
+ )
244
+ return {"prepared": prepared}
245
+
246
+ async def _handle_get(
247
+ self,
248
+ path: str,
249
+ query: Mapping[str, str],
250
+ ) -> Mapping[str, object]:
251
+ route, identifier = _resolve_read_route(path)
252
+ network = _read_network(query.get("network")) or self._config.network
253
+ context = SwigRouteContext(
254
+ method="GET",
255
+ path=path,
256
+ route=route,
257
+ body={},
258
+ query=query,
259
+ network=network,
260
+ )
261
+ swig = self._client(network)
262
+ if route == "paymaster/balance":
263
+ kind_value = query.get("kind", "").strip().upper()
264
+ kind: PaymasterBalanceKind | None
265
+ if kind_value in ("",):
266
+ kind = None
267
+ elif kind_value in ("API", "PAYMASTER_KIND_API"):
268
+ kind = "api"
269
+ elif kind_value in ("IDP", "PAYMASTER_KIND_IDP"):
270
+ kind = "idp"
271
+ else:
272
+ raise SwigProxyRouteError("kind must be api or idp")
273
+ return cast(
274
+ Mapping[str, object],
275
+ _to_wire(await swig.paymaster.get_balance(network=network, kind=kind)),
276
+ )
277
+ if route == "ramp/options":
278
+ customer = await self._resolved_ramp_customer(context)
279
+ return cast(
280
+ Mapping[str, object],
281
+ _to_wire(
282
+ await swig.ramp.get_options(
283
+ partner_application_id=(
284
+ customer.partner_application_id
285
+ if customer is not None
286
+ else query.get("partnerApplicationId")
287
+ ),
288
+ country_code=query.get("countryCode"),
289
+ fiat_currency_code=query.get("fiatCurrencyCode"),
290
+ )
291
+ ),
292
+ )
293
+ if route == "ramp/transaction":
294
+ return cast(
295
+ Mapping[str, object],
296
+ _to_wire(await swig.ramp.get_transaction(transaction_id=identifier)),
297
+ )
298
+ if route == "ramp/wallet-transactions":
299
+ direction = _active_direction(query.get("direction"))
300
+ status = _active_status(query.get("status"))
301
+ return cast(
302
+ Mapping[str, object],
303
+ _to_wire(
304
+ await swig.ramp.list_transactions(
305
+ wallet_id=identifier,
306
+ network=network,
307
+ direction=direction,
308
+ status=status,
309
+ limit=_optional_int(query.get("limit")),
310
+ )
311
+ ),
312
+ )
313
+
314
+ handle = swig.wallets.use(identifier, network=network)
315
+ result: object
316
+ if route == "wallet/balance/usd":
317
+ result = await handle.get_usd_balance(network=network)
318
+ elif route == "wallet/token-balances":
319
+ result = await handle.list_token_balances(network=network)
320
+ else:
321
+ result = await handle.list_token_transactions(
322
+ network=network,
323
+ limit=_optional_int(query.get("limit")),
324
+ )
325
+ return cast(Mapping[str, object], _to_wire(result))
326
+
327
+ def _client(self, network: Network | None) -> SwigClient:
328
+ api_key = self._config.api_key or _read_env(
329
+ "SWIG_DEVELOPER_API_KEY", "SWIG_API_KEY"
330
+ )
331
+ if not api_key:
332
+ raise SwigProxyRouteError("SWIG_DEVELOPER_API_KEY is required", 500)
333
+ base_url = (
334
+ self._config.transaction_api_url
335
+ or self._config.base_url
336
+ or _read_env(
337
+ "SWIG_TRANSACTION_API_URL",
338
+ "SWIG_BACKEND_URL",
339
+ "NEXT_PUBLIC_SWIG_BACKEND_URL",
340
+ )
341
+ )
342
+ return SwigClient(
343
+ api_key=api_key,
344
+ base_url=base_url or DEFAULT_BACKEND_URL,
345
+ network=network,
346
+ transport=self._config.transport,
347
+ )
348
+
349
+ async def _fee_payer(self, context: SwigRouteContext) -> str:
350
+ configured = self._config.fee_payer
351
+ if callable(configured):
352
+ configured = await _resolve(configured(context))
353
+ value = (
354
+ configured
355
+ or _optional_string(context.body.get("feePayer"))
356
+ or _read_env(
357
+ "SWIG_FEE_PAYER",
358
+ "SWIG_TRANSFER_FEE_PAYER",
359
+ "SWIG_TRANSACTION_FEE_PAYER",
360
+ )
361
+ )
362
+ if not value:
363
+ raise SwigProxyRouteError("feePayer is required")
364
+ return value
365
+
366
+ async def _requester_authority(self, context: SwigRouteContext) -> WalletAuthority:
367
+ authority = (
368
+ context.wallet.requester_authority if context.wallet else None
369
+ ) or _read_authority(context.body.get("requesterAuthority"))
370
+ if authority is None and self._config.resolve_requester_authority:
371
+ authority = await _resolve(
372
+ self._config.resolve_requester_authority(context)
373
+ )
374
+ if authority is None and self._config.resolve_requester_pubkey:
375
+ public_key = await _resolve(self._config.resolve_requester_pubkey(context))
376
+ if public_key:
377
+ authority = {"ed25519": {"publicKey": public_key}}
378
+ if authority is None:
379
+ raise SwigProxyRouteError("requesterAuthority is required")
380
+ return authority
381
+
382
+ async def _resolved_ramp_customer(
383
+ self, context: SwigRouteContext
384
+ ) -> RampCustomerContext | None:
385
+ if self._config.resolve_ramp_customer is None:
386
+ return None
387
+ return await _resolve(self._config.resolve_ramp_customer(context))
388
+
389
+ async def _ramp_customer(self, context: SwigRouteContext) -> RampCustomerContext:
390
+ resolved = await self._resolved_ramp_customer(context)
391
+ if resolved is not None:
392
+ return resolved
393
+ return _read_ramp_customer(context.body.get("customer"))
394
+
395
+
396
+ def create_swig_proxy_handler(
397
+ config: SwigProxyConfig | None = None,
398
+ ) -> SwigProxyHandler:
399
+ return SwigProxyHandler(config)
400
+
401
+
402
+ def _resolve_post_route(path: str) -> PostProxyRoute:
403
+ routes: tuple[PostProxyRoute, ...] = (
404
+ "wallet/create",
405
+ "prepare",
406
+ "transfer/sol",
407
+ "transfer/spl-token",
408
+ "swap/jupiter",
409
+ "ramp/quote",
410
+ "ramp/sessions",
411
+ )
412
+ normalized = path.rstrip("/")
413
+ for route in routes:
414
+ if normalized == f"/{route}" or normalized.endswith(f"/{route}"):
415
+ return route
416
+ raise SwigProxyRouteError("Unsupported Swig route", 404)
417
+
418
+
419
+ def _resolve_read_route(path: str) -> tuple[ReadProxyRoute, str]:
420
+ normalized = path.rstrip("/")
421
+ if normalized.endswith("/paymaster/balance"):
422
+ return "paymaster/balance", ""
423
+ if normalized.endswith("/ramp/options"):
424
+ return "ramp/options", ""
425
+ match = re.search(r"/ramp/transactions/([^/]+)$", normalized)
426
+ if match:
427
+ return "ramp/transaction", unquote(match.group(1))
428
+ match = re.search(r"/ramp/wallets/([^/]+)/transactions$", normalized)
429
+ if match:
430
+ return "ramp/wallet-transactions", unquote(match.group(1))
431
+ match = re.search(
432
+ r"/wallet/([^/]+)/(balance/usd|token-balances|token-transactions)$",
433
+ normalized,
434
+ )
435
+ if not match:
436
+ raise SwigProxyRouteError("Unsupported Swig route", 404)
437
+ route_map: dict[str, ReadProxyRoute] = {
438
+ "balance/usd": "wallet/balance/usd",
439
+ "token-balances": "wallet/token-balances",
440
+ "token-transactions": "wallet/token-transactions",
441
+ }
442
+ return route_map[match.group(2)], unquote(match.group(1))
443
+
444
+
445
+ def _read_wallet(value: object) -> ProxyWalletReference | None:
446
+ if value is None:
447
+ return None
448
+ body = _mapping(value, "wallet")
449
+ return ProxyWalletReference(
450
+ swig_config_address=_required_string(body, "swigConfigAddress"),
451
+ wallet_address=_optional_string(body.get("walletAddress")),
452
+ role_id=_optional_int(body.get("roleId")),
453
+ authority_public_key=_optional_string(body.get("authorityPublicKey")),
454
+ requester_authority=_read_authority(body.get("requesterAuthority")),
455
+ network=_read_network(body.get("network")),
456
+ )
457
+
458
+
459
+ def _read_authority(value: object) -> WalletAuthority | None:
460
+ if value is None:
461
+ return None
462
+ body = _mapping(value, "authority")
463
+ for scheme in ("ed25519", "secp256k1", "secp256r1"):
464
+ nested = body.get(scheme)
465
+ if isinstance(nested, Mapping):
466
+ public_key = _optional_string(nested.get("publicKey"))
467
+ if public_key:
468
+ return {scheme: {"publicKey": public_key}}
469
+ proof = body.get("programExecProof")
470
+ if isinstance(proof, Mapping):
471
+ role_id = _optional_int(proof.get("roleId"))
472
+ zk_proof = _optional_string(proof.get("zkProof"))
473
+ if role_id is not None and zk_proof:
474
+ return {"programExecProof": {"roleId": role_id, "zkProof": zk_proof}}
475
+ raise SwigProxyRouteError("authority must include a supported authority")
476
+
477
+
478
+ def _read_recovery_options(value: object) -> RecoveryOptions | None:
479
+ if value is None:
480
+ return None
481
+ body = _mapping(value, "recovery")
482
+ return RecoveryOptions(
483
+ guardian_pubkey=_optional_string(body.get("guardianPubkey")),
484
+ delay_seconds=_optional_int(body.get("delaySeconds")),
485
+ target_role_id=_optional_int(body.get("targetRoleId")),
486
+ )
487
+
488
+
489
+ def _read_operations(
490
+ value: object,
491
+ ) -> tuple[TransferSolOperation | TransferTokenOperation, ...]:
492
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)) or not value:
493
+ raise SwigProxyRouteError("operations must be a non-empty array")
494
+ operations: list[TransferSolOperation | TransferTokenOperation] = []
495
+ for index, item in enumerate(value):
496
+ body = _mapping(item, f"operations[{index}]")
497
+ if body.get("type") == "transferSol":
498
+ operations.append(
499
+ TransferSolOperation(
500
+ _required_string(body, "destination"), _read_amount(body)
501
+ )
502
+ )
503
+ elif body.get("type") == "transferToken":
504
+ operations.append(
505
+ TransferTokenOperation(
506
+ _required_string(body, "mint"),
507
+ _required_string(body, "destinationOwner"),
508
+ _read_amount(body),
509
+ )
510
+ )
511
+ else:
512
+ raise SwigProxyRouteError(
513
+ f"operations[{index}].type must be transferSol or transferToken"
514
+ )
515
+ return tuple(operations)
516
+
517
+
518
+ def _quote_args(
519
+ body: Mapping[str, object], customer: RampCustomerContext
520
+ ) -> QuoteRampArgs:
521
+ return QuoteRampArgs(
522
+ customer=customer,
523
+ wallet=_read_ramp_wallet(body.get("wallet")),
524
+ direction=_required_active_direction(_required_string(body, "direction")),
525
+ source_amount=_required_string(body, "sourceAmount"),
526
+ source_currency_code=_required_string(body, "sourceCurrencyCode"),
527
+ destination_currency_code=_required_string(body, "destinationCurrencyCode"),
528
+ country_code=_required_string(body, "countryCode"),
529
+ subdivision=_optional_string(body.get("subdivision")),
530
+ payment_method_type=_payment_method(body.get("paymentMethodType")),
531
+ service_providers=_service_providers(body.get("serviceProviders")),
532
+ )
533
+
534
+
535
+ def _session_args(
536
+ body: Mapping[str, object], customer: RampCustomerContext
537
+ ) -> CreateRampSessionArgs:
538
+ provider = _service_provider(body.get("serviceProvider"))
539
+ return CreateRampSessionArgs(
540
+ customer=customer,
541
+ wallet=_read_ramp_wallet(body.get("wallet")),
542
+ direction=_required_active_direction(_required_string(body, "direction")),
543
+ selected_quote_id=_required_string(body, "selectedQuoteId"),
544
+ source_amount=_required_string(body, "sourceAmount"),
545
+ source_currency_code=_required_string(body, "sourceCurrencyCode"),
546
+ destination_currency_code=_required_string(body, "destinationCurrencyCode"),
547
+ country_code=_required_string(body, "countryCode"),
548
+ subdivision=_optional_string(body.get("subdivision")),
549
+ service_provider=provider,
550
+ payment_method_type=_payment_method(body.get("paymentMethodType")),
551
+ redirect_url=_optional_string(body.get("redirectUrl")),
552
+ )
553
+
554
+
555
+ def _read_ramp_customer(value: object) -> RampCustomerContext:
556
+ body = _mapping(value, "customer")
557
+ customer_type = body.get("customerType")
558
+ if customer_type not in ("individual", "business", "unspecified"):
559
+ raise SwigProxyRouteError("customer.customerType is required")
560
+ return RampCustomerContext(
561
+ customer_type=customer_type,
562
+ partner_application_id=_optional_string(body.get("partnerApplicationId")),
563
+ swig_user_id=_optional_string(body.get("swigUserId")),
564
+ external_customer_id=_optional_string(body.get("externalCustomerId")),
565
+ external_business_id=_optional_string(body.get("externalBusinessId")),
566
+ )
567
+
568
+
569
+ def _read_ramp_wallet(value: object) -> RampWalletContext:
570
+ body = _mapping(value, "wallet")
571
+ network = _read_network(body.get("network"))
572
+ if network is None:
573
+ raise SwigProxyRouteError("wallet.network is required")
574
+ return RampWalletContext(
575
+ wallet_id=_required_string(body, "walletId"),
576
+ wallet_address=_required_string(body, "walletAddress"),
577
+ network=network,
578
+ )
579
+
580
+
581
+ def _read_amount(body: Mapping[str, object]) -> str:
582
+ amount = _required_string(body, "amount")
583
+ if not amount.isdigit() or int(amount) <= 0:
584
+ raise SwigProxyRouteError("amount must be a positive integer string")
585
+ return amount
586
+
587
+
588
+ def _active_direction(value: object) -> ActiveRampDirection | None:
589
+ if value in ("onramp", "offramp", "transfer"):
590
+ return value
591
+ return None
592
+
593
+
594
+ def _required_active_direction(value: object) -> ActiveRampDirection:
595
+ direction = _active_direction(value)
596
+ if direction is None:
597
+ raise SwigProxyRouteError("direction is required")
598
+ return direction
599
+
600
+
601
+ def _active_status(value: object) -> ActiveRampTransactionStatus | None:
602
+ statuses = (
603
+ "created",
604
+ "pending",
605
+ "settling",
606
+ "settled",
607
+ "failed",
608
+ "declined",
609
+ "cancelled",
610
+ "refunded",
611
+ )
612
+ return cast(ActiveRampTransactionStatus, value) if value in statuses else None
613
+
614
+
615
+ def _payment_method(value: object) -> RampPaymentMethodType | None:
616
+ values = (
617
+ "other",
618
+ "credit-debit-card",
619
+ "ach",
620
+ "bank-transfer",
621
+ "apple-pay",
622
+ "google-pay",
623
+ "pix",
624
+ "unspecified",
625
+ )
626
+ if value is None:
627
+ return None
628
+ if value not in values:
629
+ raise SwigProxyRouteError("Invalid ramp payment method type")
630
+ return cast(RampPaymentMethodType, value)
631
+
632
+
633
+ def _service_provider(value: object) -> RampServiceProvider:
634
+ if value not in ("other", "unspecified"):
635
+ raise SwigProxyRouteError("Invalid ramp service provider")
636
+ return value
637
+
638
+
639
+ def _service_providers(value: object) -> tuple[RampServiceProvider, ...]:
640
+ if value is None:
641
+ return ()
642
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
643
+ raise SwigProxyRouteError("serviceProviders must be an array")
644
+ return tuple(_service_provider(item) for item in value)
645
+
646
+
647
+ def _require_wallet(value: ProxyWalletReference | None) -> ProxyWalletReference:
648
+ if value is None:
649
+ raise SwigProxyRouteError("wallet is required")
650
+ return value
651
+
652
+
653
+ def _read_network(value: object) -> Network | None:
654
+ if isinstance(value, str) and value in ("1", "2"):
655
+ value = int(value)
656
+ return normalize_network(value)
657
+
658
+
659
+ def _required_string(body: Mapping[str, object], key: str) -> str:
660
+ value = _optional_string(body.get(key))
661
+ if not value:
662
+ raise SwigProxyRouteError(f"{key} is required")
663
+ return value
664
+
665
+
666
+ def _optional_string(value: object) -> str | None:
667
+ return value.strip() if isinstance(value, str) and value.strip() else None
668
+
669
+
670
+ def _optional_int(value: object) -> int | None:
671
+ if isinstance(value, int) and not isinstance(value, bool):
672
+ return value
673
+ if isinstance(value, str):
674
+ try:
675
+ return int(value)
676
+ except ValueError:
677
+ return None
678
+ return None
679
+
680
+
681
+ def _optional_bool(value: object) -> bool | None:
682
+ return value if isinstance(value, bool) else None
683
+
684
+
685
+ def _mapping(value: object, label: str) -> Mapping[str, object]:
686
+ if isinstance(value, Mapping):
687
+ return value
688
+ raise SwigProxyRouteError(f"{label} must be an object")
689
+
690
+
691
+ def _read_env(*names: str) -> str | None:
692
+ for name in names:
693
+ value = os.environ.get(name, "").strip()
694
+ if value:
695
+ return value
696
+ return None
697
+
698
+
699
+ def _to_wire(value: object) -> object:
700
+ if is_dataclass(value) and not isinstance(value, type):
701
+ result: dict[str, object] = {}
702
+ for field in fields(value):
703
+ item = getattr(value, field.name)
704
+ if item is not None:
705
+ result[_snake_to_camel(field.name)] = _to_wire(item)
706
+ return result
707
+ if isinstance(value, Mapping):
708
+ return {
709
+ str(key): _to_wire(item) for key, item in value.items() if item is not None
710
+ }
711
+ if isinstance(value, (list, tuple)):
712
+ return [_to_wire(item) for item in value]
713
+ return value
714
+
715
+
716
+ def _snake_to_camel(value: str) -> str:
717
+ head, *tail = value.split("_")
718
+ return head + "".join(part.capitalize() for part in tail)
719
+
720
+
721
+ async def _resolve(value: MaybeAwaitable[T]) -> T:
722
+ if inspect.isawaitable(value):
723
+ return await cast(Awaitable[T], value)
724
+ return value