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,1393 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from collections.abc import Mapping, Sequence
5
+ from dataclasses import dataclass, replace
6
+ from typing import Literal, TypeAlias
7
+ from urllib.parse import quote, urlencode
8
+
9
+ from .common import (
10
+ Amount,
11
+ Network,
12
+ WalletAddressInfo,
13
+ WalletAuthority,
14
+ normalize_amount,
15
+ require_network,
16
+ to_proto_network,
17
+ wallet_address_info_from_wire,
18
+ wallet_authority_to_wire,
19
+ )
20
+ from .core import HttpClient
21
+ from .transactions import (
22
+ PreparedTransaction,
23
+ PreparedTransactionsResult,
24
+ normalize_prepared_transaction,
25
+ normalize_prepared_transactions_result,
26
+ )
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class WalletReference:
31
+ swig_config_address: str
32
+ wallet_address: str | None = None
33
+ network: Network | None = None
34
+ requester_authority: WalletAuthority | None = None
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class IdpWalletSession:
39
+ config_address: str
40
+ wallet_address: str
41
+ auth_flow: Literal["session", "role"] | None = None
42
+ updated_at: int | None = None
43
+ requester_authority: WalletAuthority | None = None
44
+ authority_public_key: str | None = None
45
+ role_id: int | None = None
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class RecoveryOptions:
50
+ guardian_pubkey: str | None = None
51
+ delay_seconds: int | None = None
52
+ target_role_id: int | None = None
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class RecoverySetupPlan:
57
+ requester_authority: WalletAuthority
58
+ guardian_pubkey: str
59
+ delay_seconds: int
60
+ target_role_id: int | None = None
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class CreateWalletResult:
65
+ wallet: WalletAddressInfo
66
+ transactions: tuple[PreparedTransaction, ...]
67
+ client_authority_transactions: tuple[PreparedTransaction, ...]
68
+ operator_signed_transactions: tuple[PreparedTransaction, ...]
69
+ fee_payer_only_transactions: tuple[PreparedTransaction, ...]
70
+ creation_transaction: PreparedTransaction | None = None
71
+ add_authority_transaction: PreparedTransaction | None = None
72
+ configure_recovery_transaction: PreparedTransaction | None = None
73
+ recovery_setup: RecoverySetupPlan | None = None
74
+ network: Network | None = None
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class PreparedRecoverySetupResult:
79
+ transactions: tuple[PreparedTransaction, ...]
80
+ client_authority_transactions: tuple[PreparedTransaction, ...]
81
+ operator_signed_transactions: tuple[PreparedTransaction, ...]
82
+ fee_payer_only_transactions: tuple[PreparedTransaction, ...]
83
+ wallet: WalletAddressInfo | None = None
84
+ add_authority_transaction: PreparedTransaction | None = None
85
+ configure_recovery_transaction: PreparedTransaction | None = None
86
+ network: Network | None = None
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class SolanaAccountMeta:
91
+ pubkey: str
92
+ is_signer: bool = False
93
+ is_writable: bool = False
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class SolanaInstructionInput:
98
+ program_id: str
99
+ accounts: tuple[SolanaAccountMeta, ...]
100
+ data: str | bytes
101
+
102
+
103
+ @dataclass(frozen=True, slots=True)
104
+ class TransferSolOperation:
105
+ destination: str
106
+ amount: Amount
107
+ type: Literal["transferSol"] = "transferSol"
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class TransferTokenOperation:
112
+ mint: str
113
+ destination_owner: str
114
+ amount: Amount
115
+ type: Literal["transferToken"] = "transferToken"
116
+
117
+
118
+ PrepareOperation: TypeAlias = TransferSolOperation | TransferTokenOperation
119
+
120
+
121
+ @dataclass(frozen=True, slots=True)
122
+ class SwigUsdBalance:
123
+ swig_config_address: str
124
+ wallet_address: str
125
+ usd_value: float
126
+
127
+
128
+ @dataclass(frozen=True, slots=True)
129
+ class SwigTokenBalance:
130
+ mint_address: str
131
+ token_program: int
132
+ token_symbol: str
133
+ token_name: str
134
+ decimals: int
135
+ amount_raw: str
136
+ ui_amount: float
137
+ usd_price: float
138
+ usd_value: float
139
+
140
+
141
+ @dataclass(frozen=True, slots=True)
142
+ class ListSwigTokenBalancesResult:
143
+ swig_config_address: str
144
+ wallet_address: str
145
+ balances: tuple[SwigTokenBalance, ...]
146
+ total_usd_value: float
147
+
148
+
149
+ @dataclass(frozen=True, slots=True)
150
+ class SwigTokenTransaction:
151
+ transaction_signature: str
152
+ slot: int
153
+ owner_address: str
154
+ wallet_address: str
155
+ is_subaccount: bool
156
+ token_account_address: str
157
+ mint_address: str
158
+ token_program: int
159
+ direction: Literal["inflow", "outflow"]
160
+ amount_raw: str
161
+ decimals: int
162
+ ui_amount: float
163
+ usd_price: float
164
+ usd_value: float
165
+ token_symbol: str
166
+ token_name: str
167
+ block_time: str | None = None
168
+
169
+
170
+ @dataclass(frozen=True, slots=True)
171
+ class ListSwigTokenTransactionsResult:
172
+ swig_config_address: str
173
+ wallet_address: str
174
+ transactions: tuple[SwigTokenTransaction, ...]
175
+
176
+
177
+ @dataclass(frozen=True, slots=True)
178
+ class Policy:
179
+ id: str
180
+ name: str
181
+ description: str | None
182
+ authority: Mapping[str, object] | None = None
183
+ actions: tuple[Mapping[str, object], ...] = ()
184
+ guardian_enabled: bool = False
185
+ initial_key_source: str | None = None
186
+ guardian_authority: Mapping[str, object] | None = None
187
+ guardian_key_source: str | None = None
188
+ guardian_delay_seconds: int | str | None = None
189
+
190
+
191
+ class WalletsClient:
192
+ def __init__(
193
+ self,
194
+ http: HttpClient,
195
+ default_network: Network | None = None,
196
+ ) -> None:
197
+ self._http = http
198
+ self._default_network = default_network
199
+
200
+ async def create(
201
+ self,
202
+ *,
203
+ fee_payer: str,
204
+ policy_id: str | None = None,
205
+ initial_user: WalletAuthority | None = None,
206
+ recovery: RecoveryOptions | None = None,
207
+ network: Network | None = None,
208
+ idempotency_key: str | None = None,
209
+ ) -> CreateWalletResult:
210
+ policy = await self.get_policy(policy_id) if policy_id else None
211
+ response = await self._http.post(
212
+ "/transaction/wallet/create",
213
+ {
214
+ "network": to_proto_network(
215
+ require_network(network, self._default_network)
216
+ ),
217
+ "feePayer": fee_payer,
218
+ "policyId": policy_id,
219
+ "initialUser": (
220
+ wallet_authority_to_wire(initial_user)
221
+ if initial_user is not None
222
+ else None
223
+ ),
224
+ },
225
+ )
226
+ created = normalize_create_wallet_response(response)
227
+ resolved_network = created.network or network or self._default_network
228
+ recovery_setup = (
229
+ _recovery_setup_plan(policy, initial_user, recovery)
230
+ if policy is not None
231
+ else None
232
+ )
233
+ return replace(
234
+ created,
235
+ wallet=replace(created.wallet, network=resolved_network),
236
+ network=resolved_network,
237
+ recovery_setup=recovery_setup,
238
+ )
239
+
240
+ async def get_policy(self, policy_id: str) -> Policy:
241
+ return _normalize_policy(
242
+ await self._http.get(f"/wallet/policies/{quote(policy_id, safe='')}")
243
+ )
244
+
245
+ def use(
246
+ self,
247
+ wallet: str | WalletReference,
248
+ *,
249
+ network: Network | None = None,
250
+ requester_authority: WalletAuthority | None = None,
251
+ ) -> WalletHandle:
252
+ if isinstance(wallet, str):
253
+ reference = WalletReference(
254
+ swig_config_address=wallet,
255
+ network=network or self._default_network,
256
+ requester_authority=requester_authority,
257
+ )
258
+ else:
259
+ reference = WalletReference(
260
+ swig_config_address=wallet.swig_config_address,
261
+ wallet_address=wallet.wallet_address,
262
+ network=network or wallet.network or self._default_network,
263
+ requester_authority=(requester_authority or wallet.requester_authority),
264
+ )
265
+ return WalletHandle(self, reference)
266
+
267
+ def from_idp_session(
268
+ self,
269
+ session: IdpWalletSession,
270
+ *,
271
+ network: Network | None = None,
272
+ requester_authority: WalletAuthority | None = None,
273
+ ) -> WalletHandle:
274
+ return WalletHandle(
275
+ self,
276
+ WalletReference(
277
+ swig_config_address=session.config_address,
278
+ wallet_address=session.wallet_address,
279
+ network=network or self._default_network,
280
+ requester_authority=(
281
+ requester_authority or session.requester_authority
282
+ ),
283
+ ),
284
+ )
285
+
286
+ async def prepare(
287
+ self,
288
+ wallet: WalletHandle,
289
+ *,
290
+ fee_payer: str,
291
+ operations: Sequence[PrepareOperation],
292
+ requester_authority: WalletAuthority | None = None,
293
+ network: Network | None = None,
294
+ idempotency_key: str | None = None,
295
+ ) -> PreparedTransactionsResult:
296
+ authority = _requester_authority(wallet, requester_authority)
297
+ response = await self._http.post(
298
+ "/transaction/prepare",
299
+ {
300
+ **_base_write_request(
301
+ wallet, fee_payer, network, self._default_network
302
+ ),
303
+ "requesterAuthority": wallet_authority_to_wire(authority),
304
+ "operations": [_operation_to_wire(item) for item in operations],
305
+ },
306
+ )
307
+ return normalize_prepared_transactions_result(response)
308
+
309
+ async def transfer_sol(
310
+ self,
311
+ wallet: WalletHandle,
312
+ *,
313
+ fee_payer: str,
314
+ destination: str,
315
+ amount: Amount,
316
+ requester_authority: WalletAuthority | None = None,
317
+ network: Network | None = None,
318
+ idempotency_key: str | None = None,
319
+ ) -> PreparedTransaction:
320
+ authority = _requester_authority(wallet, requester_authority)
321
+ return normalize_prepared_transaction(
322
+ await self._http.post(
323
+ "/transaction/transfer/sol",
324
+ {
325
+ **_base_write_request(
326
+ wallet, fee_payer, network, self._default_network
327
+ ),
328
+ "requesterAuthority": wallet_authority_to_wire(authority),
329
+ "destination": destination,
330
+ "lamports": normalize_amount(amount),
331
+ },
332
+ )
333
+ )
334
+
335
+ async def transfer_token(
336
+ self,
337
+ wallet: WalletHandle,
338
+ *,
339
+ fee_payer: str,
340
+ mint: str,
341
+ destination_owner: str,
342
+ amount: Amount,
343
+ requester_authority: WalletAuthority | None = None,
344
+ network: Network | None = None,
345
+ idempotency_key: str | None = None,
346
+ ) -> PreparedTransaction:
347
+ authority = _requester_authority(wallet, requester_authority)
348
+ return normalize_prepared_transaction(
349
+ await self._http.post(
350
+ "/transaction/transfer/spl-token",
351
+ {
352
+ **_base_write_request(
353
+ wallet, fee_payer, network, self._default_network
354
+ ),
355
+ "requesterAuthority": wallet_authority_to_wire(authority),
356
+ "mint": mint,
357
+ "destinationOwner": destination_owner,
358
+ "amount": normalize_amount(amount),
359
+ },
360
+ )
361
+ )
362
+
363
+ async def jupiter_swap(
364
+ self,
365
+ wallet: WalletHandle,
366
+ *,
367
+ fee_payer: str,
368
+ input_mint: str,
369
+ output_mint: str,
370
+ amount: Amount,
371
+ requester_authority: WalletAuthority | None = None,
372
+ slippage_bps: int | None = None,
373
+ destination_account: str | None = None,
374
+ wrap_and_unwrap_sol: bool | None = None,
375
+ tip_amount_lamports: Amount | None = None,
376
+ compute_unit_price_percentile: str | None = None,
377
+ max_accounts: int | None = None,
378
+ mode: str | None = None,
379
+ blockhash_slots_to_expiry: int | None = None,
380
+ network: Network | None = None,
381
+ idempotency_key: str | None = None,
382
+ ) -> PreparedTransaction:
383
+ authority = _requester_authority(wallet, requester_authority)
384
+ return normalize_prepared_transaction(
385
+ await self._http.post(
386
+ "/transaction/swap/jupiter",
387
+ {
388
+ **_base_write_request(
389
+ wallet, fee_payer, network, self._default_network
390
+ ),
391
+ "requesterAuthority": wallet_authority_to_wire(authority),
392
+ "inputMint": input_mint,
393
+ "outputMint": output_mint,
394
+ "amount": normalize_amount(amount),
395
+ "slippageBps": slippage_bps,
396
+ "destinationAccount": destination_account,
397
+ "wrapAndUnwrapSol": wrap_and_unwrap_sol,
398
+ "tipAmountLamports": (
399
+ normalize_amount(tip_amount_lamports)
400
+ if tip_amount_lamports is not None
401
+ else None
402
+ ),
403
+ "computeUnitPricePercentile": compute_unit_price_percentile,
404
+ "maxAccounts": max_accounts,
405
+ "mode": mode,
406
+ "blockhashSlotsToExpiry": blockhash_slots_to_expiry,
407
+ },
408
+ )
409
+ )
410
+
411
+ async def add_recovery_authority(
412
+ self,
413
+ wallet: WalletHandle,
414
+ *,
415
+ fee_payer: str,
416
+ requester_authority: WalletAuthority | None = None,
417
+ network: Network | None = None,
418
+ idempotency_key: str | None = None,
419
+ ) -> PreparedTransaction:
420
+ authority = _requester_authority(wallet, requester_authority)
421
+ return normalize_prepared_transaction(
422
+ await self._http.post(
423
+ "/transaction/wallet/recovery-authority/add",
424
+ {
425
+ **_base_write_request(
426
+ wallet, fee_payer, network, self._default_network
427
+ ),
428
+ "requesterAuthority": wallet_authority_to_wire(authority),
429
+ },
430
+ )
431
+ )
432
+
433
+ async def configure_recovery(
434
+ self,
435
+ wallet: WalletHandle,
436
+ *,
437
+ fee_payer: str,
438
+ guardian_pubkey: str,
439
+ delay_seconds: int | None = None,
440
+ target_role_id: int | None = None,
441
+ network: Network | None = None,
442
+ idempotency_key: str | None = None,
443
+ ) -> PreparedTransaction:
444
+ return normalize_prepared_transaction(
445
+ await self._http.post(
446
+ "/transaction/recovery/configure",
447
+ {
448
+ **_base_write_request(
449
+ wallet, fee_payer, network, self._default_network
450
+ ),
451
+ "guardianPubkey": guardian_pubkey,
452
+ "delaySeconds": delay_seconds,
453
+ "targetRoleId": target_role_id,
454
+ },
455
+ )
456
+ )
457
+
458
+ async def prepare_recovery_setup(
459
+ self,
460
+ wallet: WalletHandle,
461
+ *,
462
+ fee_payer: str,
463
+ guardian_pubkey: str,
464
+ requester_authority: WalletAuthority | None = None,
465
+ delay_seconds: int | None = None,
466
+ target_role_id: int | None = None,
467
+ network: Network | None = None,
468
+ idempotency_key: str | None = None,
469
+ ) -> PreparedRecoverySetupResult:
470
+ add_authority = await self.add_recovery_authority(
471
+ wallet,
472
+ fee_payer=fee_payer,
473
+ requester_authority=requester_authority,
474
+ network=network,
475
+ idempotency_key=idempotency_key,
476
+ )
477
+ configure = await self.configure_recovery(
478
+ wallet,
479
+ fee_payer=fee_payer,
480
+ guardian_pubkey=guardian_pubkey,
481
+ delay_seconds=delay_seconds,
482
+ target_role_id=target_role_id,
483
+ network=network,
484
+ idempotency_key=idempotency_key,
485
+ )
486
+ transactions = (add_authority, configure)
487
+ resolved_network = network or wallet.network or self._default_network
488
+ wallet_info = add_authority.wallet or configure.wallet
489
+ return PreparedRecoverySetupResult(
490
+ wallet=(
491
+ replace(wallet_info, network=resolved_network)
492
+ if wallet_info is not None
493
+ else None
494
+ ),
495
+ transactions=transactions,
496
+ client_authority_transactions=tuple(
497
+ item for item in transactions if item.signature_requests
498
+ ),
499
+ operator_signed_transactions=tuple(
500
+ item for item in transactions if item.kind == "configure-recovery"
501
+ ),
502
+ fee_payer_only_transactions=tuple(
503
+ item
504
+ for item in transactions
505
+ if not item.signature_requests and item.kind != "configure-recovery"
506
+ ),
507
+ add_authority_transaction=add_authority,
508
+ configure_recovery_transaction=configure,
509
+ network=resolved_network,
510
+ )
511
+
512
+ async def start_recovery(
513
+ self,
514
+ wallet: WalletHandle,
515
+ *,
516
+ fee_payer: str,
517
+ guardian_pubkey: str,
518
+ new_authority: str,
519
+ network: Network | None = None,
520
+ idempotency_key: str | None = None,
521
+ ) -> PreparedTransaction:
522
+ return normalize_prepared_transaction(
523
+ await self._http.post(
524
+ "/transaction/recovery/start",
525
+ {
526
+ **_base_write_request(
527
+ wallet, fee_payer, network, self._default_network
528
+ ),
529
+ "guardianPubkey": guardian_pubkey,
530
+ "newAuthority": new_authority,
531
+ },
532
+ )
533
+ )
534
+
535
+ async def cancel_recovery(
536
+ self,
537
+ wallet: WalletHandle,
538
+ *,
539
+ fee_payer: str,
540
+ requester_authority: WalletAuthority | None = None,
541
+ network: Network | None = None,
542
+ idempotency_key: str | None = None,
543
+ ) -> PreparedTransaction:
544
+ authority = _requester_authority(wallet, requester_authority)
545
+ return normalize_prepared_transaction(
546
+ await self._http.post(
547
+ "/transaction/recovery/cancel",
548
+ {
549
+ **_base_write_request(
550
+ wallet, fee_payer, network, self._default_network
551
+ ),
552
+ "requesterAuthority": wallet_authority_to_wire(authority),
553
+ },
554
+ )
555
+ )
556
+
557
+ async def execute_recovery(
558
+ self,
559
+ wallet: WalletHandle,
560
+ *,
561
+ fee_payer: str,
562
+ new_authority: str,
563
+ network: Network | None = None,
564
+ idempotency_key: str | None = None,
565
+ ) -> PreparedTransaction:
566
+ return normalize_prepared_transaction(
567
+ await self._http.post(
568
+ "/transaction/recovery/execute",
569
+ {
570
+ **_base_write_request(
571
+ wallet, fee_payer, network, self._default_network
572
+ ),
573
+ "newAuthority": new_authority,
574
+ },
575
+ )
576
+ )
577
+
578
+ async def execute(
579
+ self,
580
+ wallet: WalletHandle,
581
+ *,
582
+ instructions: Sequence[SolanaInstructionInput],
583
+ address_lookup_table_accounts: Sequence[str] | None = None,
584
+ network: Network | None = None,
585
+ idempotency_key: str | None = None,
586
+ ) -> PreparedTransaction:
587
+ return normalize_prepared_transaction(
588
+ await self._http.post(
589
+ f"/v1/wallets/{quote(wallet.swig_config_address, safe='')}/execute",
590
+ {
591
+ "wallet": wallet.swig_config_address,
592
+ "network": network or wallet.network or self._default_network,
593
+ "instructions": [
594
+ _instruction_to_wire(item) for item in instructions
595
+ ],
596
+ "addressLookupTableAccounts": address_lookup_table_accounts,
597
+ "idempotencyKey": idempotency_key,
598
+ },
599
+ )
600
+ )
601
+
602
+ async def get_usd_balance(
603
+ self,
604
+ wallet: WalletHandle,
605
+ *,
606
+ network: Network | None = None,
607
+ ) -> SwigUsdBalance:
608
+ return _normalize_usd_balance(
609
+ await self._http.get(
610
+ _wallet_read_path(
611
+ wallet,
612
+ "balance/usd",
613
+ network or wallet.network or self._default_network,
614
+ )
615
+ )
616
+ )
617
+
618
+ async def list_token_balances(
619
+ self,
620
+ wallet: WalletHandle,
621
+ *,
622
+ network: Network | None = None,
623
+ ) -> ListSwigTokenBalancesResult:
624
+ return _normalize_token_balances(
625
+ await self._http.get(
626
+ _wallet_read_path(
627
+ wallet,
628
+ "token-balances",
629
+ network or wallet.network or self._default_network,
630
+ )
631
+ )
632
+ )
633
+
634
+ async def list_token_transactions(
635
+ self,
636
+ wallet: WalletHandle,
637
+ *,
638
+ network: Network | None = None,
639
+ limit: int | None = None,
640
+ ) -> ListSwigTokenTransactionsResult:
641
+ return _normalize_token_transactions(
642
+ await self._http.get(
643
+ _wallet_read_path(
644
+ wallet,
645
+ "token-transactions",
646
+ network or wallet.network or self._default_network,
647
+ limit,
648
+ )
649
+ )
650
+ )
651
+
652
+
653
+ class WalletHandle:
654
+ def __init__(self, wallets: WalletsClient, reference: WalletReference) -> None:
655
+ self._wallets = wallets
656
+ self.swig_config_address = reference.swig_config_address
657
+ self.wallet_address = reference.wallet_address
658
+ self.network = reference.network
659
+ self.requester_authority = reference.requester_authority
660
+ self.transfer = WalletTransferClient(wallets, self)
661
+ self.swap = WalletSwapClient(wallets, self)
662
+ self.recovery = WalletRecoveryClient(wallets, self)
663
+
664
+ async def prepare(
665
+ self,
666
+ *,
667
+ fee_payer: str,
668
+ operations: Sequence[PrepareOperation],
669
+ requester_authority: WalletAuthority | None = None,
670
+ network: Network | None = None,
671
+ idempotency_key: str | None = None,
672
+ ) -> PreparedTransactionsResult:
673
+ return await self._wallets.prepare(
674
+ self,
675
+ fee_payer=fee_payer,
676
+ operations=operations,
677
+ requester_authority=requester_authority,
678
+ network=network,
679
+ idempotency_key=idempotency_key,
680
+ )
681
+
682
+ async def execute(
683
+ self,
684
+ *,
685
+ instructions: Sequence[SolanaInstructionInput],
686
+ address_lookup_table_accounts: Sequence[str] | None = None,
687
+ network: Network | None = None,
688
+ idempotency_key: str | None = None,
689
+ ) -> PreparedTransaction:
690
+ return await self._wallets.execute(
691
+ self,
692
+ instructions=instructions,
693
+ address_lookup_table_accounts=address_lookup_table_accounts,
694
+ network=network,
695
+ idempotency_key=idempotency_key,
696
+ )
697
+
698
+ async def get_usd_balance(
699
+ self, *, network: Network | None = None
700
+ ) -> SwigUsdBalance:
701
+ return await self._wallets.get_usd_balance(self, network=network)
702
+
703
+ async def list_token_balances(
704
+ self, *, network: Network | None = None
705
+ ) -> ListSwigTokenBalancesResult:
706
+ return await self._wallets.list_token_balances(self, network=network)
707
+
708
+ async def list_token_transactions(
709
+ self,
710
+ *,
711
+ network: Network | None = None,
712
+ limit: int | None = None,
713
+ ) -> ListSwigTokenTransactionsResult:
714
+ return await self._wallets.list_token_transactions(
715
+ self, network=network, limit=limit
716
+ )
717
+
718
+
719
+ class WalletTransferClient:
720
+ def __init__(self, wallets: WalletsClient, wallet: WalletHandle) -> None:
721
+ self._wallets = wallets
722
+ self._wallet = wallet
723
+
724
+ async def __call__(
725
+ self,
726
+ *,
727
+ fee_payer: str,
728
+ amount: Amount,
729
+ destination: str | None = None,
730
+ mint: str | None = None,
731
+ destination_owner: str | None = None,
732
+ requester_authority: WalletAuthority | None = None,
733
+ network: Network | None = None,
734
+ idempotency_key: str | None = None,
735
+ ) -> PreparedTransaction:
736
+ if mint:
737
+ if destination_owner is None:
738
+ raise ValueError("destination_owner is required for a token transfer")
739
+ return await self.token(
740
+ fee_payer=fee_payer,
741
+ mint=mint,
742
+ destination_owner=destination_owner,
743
+ amount=amount,
744
+ requester_authority=requester_authority,
745
+ network=network,
746
+ idempotency_key=idempotency_key,
747
+ )
748
+ if destination is None:
749
+ raise ValueError("destination is required for a SOL transfer")
750
+ return await self.sol(
751
+ fee_payer=fee_payer,
752
+ destination=destination,
753
+ amount=amount,
754
+ requester_authority=requester_authority,
755
+ network=network,
756
+ idempotency_key=idempotency_key,
757
+ )
758
+
759
+ async def sol(
760
+ self,
761
+ *,
762
+ fee_payer: str,
763
+ destination: str,
764
+ amount: Amount,
765
+ requester_authority: WalletAuthority | None = None,
766
+ network: Network | None = None,
767
+ idempotency_key: str | None = None,
768
+ ) -> PreparedTransaction:
769
+ return await self._wallets.transfer_sol(
770
+ self._wallet,
771
+ fee_payer=fee_payer,
772
+ destination=destination,
773
+ amount=amount,
774
+ requester_authority=requester_authority,
775
+ network=network,
776
+ idempotency_key=idempotency_key,
777
+ )
778
+
779
+ async def token(
780
+ self,
781
+ *,
782
+ fee_payer: str,
783
+ mint: str,
784
+ destination_owner: str,
785
+ amount: Amount,
786
+ requester_authority: WalletAuthority | None = None,
787
+ network: Network | None = None,
788
+ idempotency_key: str | None = None,
789
+ ) -> PreparedTransaction:
790
+ return await self._wallets.transfer_token(
791
+ self._wallet,
792
+ fee_payer=fee_payer,
793
+ mint=mint,
794
+ destination_owner=destination_owner,
795
+ amount=amount,
796
+ requester_authority=requester_authority,
797
+ network=network,
798
+ idempotency_key=idempotency_key,
799
+ )
800
+
801
+ async def spl_token(
802
+ self,
803
+ *,
804
+ fee_payer: str,
805
+ mint: str,
806
+ destination_owner: str,
807
+ amount: Amount,
808
+ requester_authority: WalletAuthority | None = None,
809
+ network: Network | None = None,
810
+ idempotency_key: str | None = None,
811
+ ) -> PreparedTransaction:
812
+ return await self.token(
813
+ fee_payer=fee_payer,
814
+ mint=mint,
815
+ destination_owner=destination_owner,
816
+ amount=amount,
817
+ requester_authority=requester_authority,
818
+ network=network,
819
+ idempotency_key=idempotency_key,
820
+ )
821
+
822
+
823
+ class WalletSwapClient:
824
+ def __init__(self, wallets: WalletsClient, wallet: WalletHandle) -> None:
825
+ self._wallets = wallets
826
+ self._wallet = wallet
827
+
828
+ async def __call__(
829
+ self,
830
+ *,
831
+ fee_payer: str,
832
+ input_mint: str,
833
+ output_mint: str,
834
+ amount: Amount,
835
+ requester_authority: WalletAuthority | None = None,
836
+ slippage_bps: int | None = None,
837
+ destination_account: str | None = None,
838
+ wrap_and_unwrap_sol: bool | None = None,
839
+ tip_amount_lamports: Amount | None = None,
840
+ compute_unit_price_percentile: str | None = None,
841
+ max_accounts: int | None = None,
842
+ mode: str | None = None,
843
+ blockhash_slots_to_expiry: int | None = None,
844
+ network: Network | None = None,
845
+ idempotency_key: str | None = None,
846
+ ) -> PreparedTransaction:
847
+ return await self.jupiter(
848
+ fee_payer=fee_payer,
849
+ input_mint=input_mint,
850
+ output_mint=output_mint,
851
+ amount=amount,
852
+ requester_authority=requester_authority,
853
+ slippage_bps=slippage_bps,
854
+ destination_account=destination_account,
855
+ wrap_and_unwrap_sol=wrap_and_unwrap_sol,
856
+ tip_amount_lamports=tip_amount_lamports,
857
+ compute_unit_price_percentile=compute_unit_price_percentile,
858
+ max_accounts=max_accounts,
859
+ mode=mode,
860
+ blockhash_slots_to_expiry=blockhash_slots_to_expiry,
861
+ network=network,
862
+ idempotency_key=idempotency_key,
863
+ )
864
+
865
+ async def jupiter(
866
+ self,
867
+ *,
868
+ fee_payer: str,
869
+ input_mint: str,
870
+ output_mint: str,
871
+ amount: Amount,
872
+ requester_authority: WalletAuthority | None = None,
873
+ slippage_bps: int | None = None,
874
+ destination_account: str | None = None,
875
+ wrap_and_unwrap_sol: bool | None = None,
876
+ tip_amount_lamports: Amount | None = None,
877
+ compute_unit_price_percentile: str | None = None,
878
+ max_accounts: int | None = None,
879
+ mode: str | None = None,
880
+ blockhash_slots_to_expiry: int | None = None,
881
+ network: Network | None = None,
882
+ idempotency_key: str | None = None,
883
+ ) -> PreparedTransaction:
884
+ return await self._wallets.jupiter_swap(
885
+ self._wallet,
886
+ fee_payer=fee_payer,
887
+ input_mint=input_mint,
888
+ output_mint=output_mint,
889
+ amount=amount,
890
+ requester_authority=requester_authority,
891
+ slippage_bps=slippage_bps,
892
+ destination_account=destination_account,
893
+ wrap_and_unwrap_sol=wrap_and_unwrap_sol,
894
+ tip_amount_lamports=tip_amount_lamports,
895
+ compute_unit_price_percentile=compute_unit_price_percentile,
896
+ max_accounts=max_accounts,
897
+ mode=mode,
898
+ blockhash_slots_to_expiry=blockhash_slots_to_expiry,
899
+ network=network,
900
+ idempotency_key=idempotency_key,
901
+ )
902
+
903
+
904
+ class WalletRecoveryClient:
905
+ def __init__(self, wallets: WalletsClient, wallet: WalletHandle) -> None:
906
+ self._wallets = wallets
907
+ self._wallet = wallet
908
+
909
+ async def prepare_setup(
910
+ self,
911
+ *,
912
+ fee_payer: str,
913
+ guardian_pubkey: str,
914
+ requester_authority: WalletAuthority | None = None,
915
+ delay_seconds: int | None = None,
916
+ target_role_id: int | None = None,
917
+ network: Network | None = None,
918
+ idempotency_key: str | None = None,
919
+ ) -> PreparedRecoverySetupResult:
920
+ return await self._wallets.prepare_recovery_setup(
921
+ self._wallet,
922
+ fee_payer=fee_payer,
923
+ guardian_pubkey=guardian_pubkey,
924
+ requester_authority=requester_authority,
925
+ delay_seconds=delay_seconds,
926
+ target_role_id=target_role_id,
927
+ network=network,
928
+ idempotency_key=idempotency_key,
929
+ )
930
+
931
+ async def start(
932
+ self,
933
+ *,
934
+ fee_payer: str,
935
+ guardian_pubkey: str,
936
+ new_authority: str,
937
+ network: Network | None = None,
938
+ idempotency_key: str | None = None,
939
+ ) -> PreparedTransaction:
940
+ return await self._wallets.start_recovery(
941
+ self._wallet,
942
+ fee_payer=fee_payer,
943
+ guardian_pubkey=guardian_pubkey,
944
+ new_authority=new_authority,
945
+ network=network,
946
+ idempotency_key=idempotency_key,
947
+ )
948
+
949
+ async def cancel(
950
+ self,
951
+ *,
952
+ fee_payer: str,
953
+ requester_authority: WalletAuthority | None = None,
954
+ network: Network | None = None,
955
+ idempotency_key: str | None = None,
956
+ ) -> PreparedTransaction:
957
+ return await self._wallets.cancel_recovery(
958
+ self._wallet,
959
+ fee_payer=fee_payer,
960
+ requester_authority=requester_authority,
961
+ network=network,
962
+ idempotency_key=idempotency_key,
963
+ )
964
+
965
+ async def execute(
966
+ self,
967
+ *,
968
+ fee_payer: str,
969
+ new_authority: str,
970
+ network: Network | None = None,
971
+ idempotency_key: str | None = None,
972
+ ) -> PreparedTransaction:
973
+ return await self._wallets.execute_recovery(
974
+ self._wallet,
975
+ fee_payer=fee_payer,
976
+ new_authority=new_authority,
977
+ network=network,
978
+ idempotency_key=idempotency_key,
979
+ )
980
+
981
+
982
+ def normalize_create_wallet_response(response: object) -> CreateWalletResult:
983
+ body = _mapping(response, "Create wallet response")
984
+ raw_transactions = body.get("transactions")
985
+ if isinstance(raw_transactions, Sequence) and not isinstance(
986
+ raw_transactions, (str, bytes)
987
+ ):
988
+ transactions = tuple(
989
+ normalize_prepared_transaction(item) for item in raw_transactions
990
+ )
991
+ else:
992
+ transactions = (normalize_prepared_transaction(body),)
993
+ creation = next(
994
+ (item for item in transactions if item.kind == "create-swig-wallet"),
995
+ transactions[0] if transactions else None,
996
+ )
997
+ add_authority = next(
998
+ (item for item in transactions if item.kind == "add-authority"), None
999
+ )
1000
+ configure = next(
1001
+ (item for item in transactions if item.kind == "configure-recovery"), None
1002
+ )
1003
+ wallet_value = body.get("wallet")
1004
+ wallet = (
1005
+ wallet_address_info_from_wire(wallet_value)
1006
+ if wallet_value is not None
1007
+ else creation.wallet
1008
+ if creation is not None
1009
+ else None
1010
+ )
1011
+ if wallet is None:
1012
+ raise ValueError("Create wallet response is missing wallet")
1013
+ return CreateWalletResult(
1014
+ wallet=wallet,
1015
+ transactions=transactions,
1016
+ client_authority_transactions=tuple(
1017
+ item for item in transactions if item.signature_requests
1018
+ ),
1019
+ operator_signed_transactions=tuple(
1020
+ item for item in transactions if item.kind == "configure-recovery"
1021
+ ),
1022
+ fee_payer_only_transactions=tuple(
1023
+ item
1024
+ for item in transactions
1025
+ if item.kind not in ("add-authority", "configure-recovery")
1026
+ ),
1027
+ creation_transaction=creation,
1028
+ add_authority_transaction=add_authority,
1029
+ configure_recovery_transaction=configure,
1030
+ network=creation.network if creation else None,
1031
+ )
1032
+
1033
+
1034
+ def _base_write_request(
1035
+ wallet: WalletHandle,
1036
+ fee_payer: str,
1037
+ network: Network | None,
1038
+ default_network: Network | None,
1039
+ ) -> dict[str, object]:
1040
+ return {
1041
+ "network": to_proto_network(
1042
+ require_network(network, wallet.network, default_network)
1043
+ ),
1044
+ "feePayer": fee_payer,
1045
+ "swigAddress": wallet.swig_config_address,
1046
+ }
1047
+
1048
+
1049
+ def _requester_authority(
1050
+ wallet: WalletHandle,
1051
+ authority: WalletAuthority | None,
1052
+ ) -> WalletAuthority:
1053
+ resolved = authority or wallet.requester_authority
1054
+ if resolved is None:
1055
+ raise ValueError("requester_authority is required")
1056
+ return resolved
1057
+
1058
+
1059
+ def _operation_to_wire(operation: PrepareOperation) -> dict[str, object]:
1060
+ if isinstance(operation, TransferSolOperation):
1061
+ return {
1062
+ "transferSol": {
1063
+ "destination": operation.destination,
1064
+ "lamports": normalize_amount(operation.amount),
1065
+ }
1066
+ }
1067
+ return {
1068
+ "transferToken": {
1069
+ "mint": operation.mint,
1070
+ "destinationOwner": operation.destination_owner,
1071
+ "amount": normalize_amount(operation.amount),
1072
+ }
1073
+ }
1074
+
1075
+
1076
+ def _instruction_to_wire(value: SolanaInstructionInput) -> dict[str, object]:
1077
+ data = (
1078
+ value.data
1079
+ if isinstance(value.data, str)
1080
+ else base64.b64encode(value.data).decode("ascii")
1081
+ )
1082
+ return {
1083
+ "programId": value.program_id,
1084
+ "accounts": [
1085
+ {
1086
+ "pubkey": account.pubkey,
1087
+ "isSigner": account.is_signer,
1088
+ "isWritable": account.is_writable,
1089
+ }
1090
+ for account in value.accounts
1091
+ ],
1092
+ "data": data,
1093
+ }
1094
+
1095
+
1096
+ def _wallet_read_path(
1097
+ wallet: WalletHandle,
1098
+ route: str,
1099
+ network: Network | None,
1100
+ limit: int | None = None,
1101
+ ) -> str:
1102
+ query: dict[str, str] = {}
1103
+ if network is not None:
1104
+ query["network"] = to_proto_network(network)
1105
+ if limit is not None:
1106
+ query["limit"] = str(limit)
1107
+ suffix = f"?{urlencode(query)}" if query else ""
1108
+ return f"/wallet/swig/{quote(wallet.swig_config_address, safe='')}/{route}{suffix}"
1109
+
1110
+
1111
+ def _normalize_usd_balance(value: object) -> SwigUsdBalance:
1112
+ body = _mapping(value, "USD balance response")
1113
+ return SwigUsdBalance(
1114
+ swig_config_address=_required_string(
1115
+ _pick(body, "swigConfigAddress", "swig_config_address"),
1116
+ "swigConfigAddress",
1117
+ ),
1118
+ wallet_address=_required_string(
1119
+ _pick(body, "walletAddress", "wallet_address"), "walletAddress"
1120
+ ),
1121
+ usd_value=_required_number(_pick(body, "usdValue", "usd_value"), "usdValue"),
1122
+ )
1123
+
1124
+
1125
+ def _normalize_token_balances(value: object) -> ListSwigTokenBalancesResult:
1126
+ body = _mapping(value, "Token balances response")
1127
+ balances = body.get("balances", [])
1128
+ if not isinstance(balances, Sequence) or isinstance(balances, (str, bytes)):
1129
+ raise ValueError("Token balances response has invalid balances")
1130
+ return ListSwigTokenBalancesResult(
1131
+ swig_config_address=_required_string(
1132
+ _pick(body, "swigConfigAddress", "swig_config_address"),
1133
+ "swigConfigAddress",
1134
+ ),
1135
+ wallet_address=_required_string(
1136
+ _pick(body, "walletAddress", "wallet_address"), "walletAddress"
1137
+ ),
1138
+ balances=tuple(_normalize_token_balance(item) for item in balances),
1139
+ total_usd_value=_required_number(
1140
+ _pick(body, "totalUsdValue", "total_usd_value"), "totalUsdValue"
1141
+ ),
1142
+ )
1143
+
1144
+
1145
+ def _normalize_token_balance(value: object) -> SwigTokenBalance:
1146
+ body = _mapping(value, "Token balance")
1147
+ return SwigTokenBalance(
1148
+ mint_address=_required_string(
1149
+ _pick(body, "mintAddress", "mint_address"), "mintAddress"
1150
+ ),
1151
+ token_program=_required_int(
1152
+ _pick(body, "tokenProgram", "token_program"), "tokenProgram"
1153
+ ),
1154
+ token_symbol=_required_string(
1155
+ _pick(body, "tokenSymbol", "token_symbol") or "", "tokenSymbol"
1156
+ ),
1157
+ token_name=_required_string(
1158
+ _pick(body, "tokenName", "token_name") or "", "tokenName"
1159
+ ),
1160
+ decimals=_required_int(body.get("decimals"), "decimals"),
1161
+ amount_raw=_required_string(
1162
+ _pick(body, "amountRaw", "amount_raw"), "amountRaw"
1163
+ ),
1164
+ ui_amount=_required_number(_pick(body, "uiAmount", "ui_amount"), "uiAmount"),
1165
+ usd_price=_required_number(_pick(body, "usdPrice", "usd_price"), "usdPrice"),
1166
+ usd_value=_required_number(_pick(body, "usdValue", "usd_value"), "usdValue"),
1167
+ )
1168
+
1169
+
1170
+ def _normalize_token_transactions(
1171
+ value: object,
1172
+ ) -> ListSwigTokenTransactionsResult:
1173
+ body = _mapping(value, "Token transactions response")
1174
+ transactions = body.get("transactions", [])
1175
+ if not isinstance(transactions, Sequence) or isinstance(transactions, (str, bytes)):
1176
+ raise ValueError("Token transactions response has invalid transactions")
1177
+ return ListSwigTokenTransactionsResult(
1178
+ swig_config_address=_required_string(
1179
+ _pick(body, "swigConfigAddress", "swig_config_address"),
1180
+ "swigConfigAddress",
1181
+ ),
1182
+ wallet_address=_required_string(
1183
+ _pick(body, "walletAddress", "wallet_address"), "walletAddress"
1184
+ ),
1185
+ transactions=tuple(_normalize_token_transaction(item) for item in transactions),
1186
+ )
1187
+
1188
+
1189
+ def _normalize_token_transaction(value: object) -> SwigTokenTransaction:
1190
+ body = _mapping(value, "Token transaction")
1191
+ direction = body.get("direction")
1192
+ if direction in ("inflow", "SWIG_TOKEN_TRANSACTION_DIRECTION_INFLOW", 1):
1193
+ normalized_direction: Literal["inflow", "outflow"] = "inflow"
1194
+ elif direction in (
1195
+ "outflow",
1196
+ "SWIG_TOKEN_TRANSACTION_DIRECTION_OUTFLOW",
1197
+ 2,
1198
+ ):
1199
+ normalized_direction = "outflow"
1200
+ else:
1201
+ raise ValueError("Token transaction response has invalid direction")
1202
+ is_subaccount = _pick(body, "isSubaccount", "is_subaccount")
1203
+ if not isinstance(is_subaccount, bool):
1204
+ raise ValueError("Response is missing isSubaccount")
1205
+ return SwigTokenTransaction(
1206
+ transaction_signature=_required_string(
1207
+ _pick(body, "transactionSignature", "transaction_signature"),
1208
+ "transactionSignature",
1209
+ ),
1210
+ slot=_required_int(body.get("slot"), "slot"),
1211
+ block_time=_optional_string(_pick(body, "blockTime", "block_time")),
1212
+ owner_address=_required_string(
1213
+ _pick(body, "ownerAddress", "owner_address"), "ownerAddress"
1214
+ ),
1215
+ wallet_address=_required_string(
1216
+ _pick(body, "walletAddress", "wallet_address"), "walletAddress"
1217
+ ),
1218
+ is_subaccount=is_subaccount,
1219
+ token_account_address=_required_string(
1220
+ _pick(body, "tokenAccountAddress", "token_account_address"),
1221
+ "tokenAccountAddress",
1222
+ ),
1223
+ mint_address=_required_string(
1224
+ _pick(body, "mintAddress", "mint_address"), "mintAddress"
1225
+ ),
1226
+ token_program=_required_int(
1227
+ _pick(body, "tokenProgram", "token_program"), "tokenProgram"
1228
+ ),
1229
+ direction=normalized_direction,
1230
+ amount_raw=_required_string(
1231
+ _pick(body, "amountRaw", "amount_raw"), "amountRaw"
1232
+ ),
1233
+ decimals=_required_int(body.get("decimals"), "decimals"),
1234
+ ui_amount=_required_number(_pick(body, "uiAmount", "ui_amount"), "uiAmount"),
1235
+ usd_price=_required_number(_pick(body, "usdPrice", "usd_price"), "usdPrice"),
1236
+ usd_value=_required_number(_pick(body, "usdValue", "usd_value"), "usdValue"),
1237
+ token_symbol=_required_string(
1238
+ _pick(body, "tokenSymbol", "token_symbol") or "", "tokenSymbol"
1239
+ ),
1240
+ token_name=_required_string(
1241
+ _pick(body, "tokenName", "token_name") or "", "tokenName"
1242
+ ),
1243
+ )
1244
+
1245
+
1246
+ def _normalize_policy(value: object) -> Policy:
1247
+ body = _mapping(value, "Policy response")
1248
+ description = body.get("description")
1249
+ if description is not None and not isinstance(description, str):
1250
+ raise ValueError("Policy response has invalid description")
1251
+ actions_value = body.get("actions", [])
1252
+ if not isinstance(actions_value, Sequence) or isinstance(
1253
+ actions_value, (str, bytes)
1254
+ ):
1255
+ raise ValueError("Policy response has invalid actions")
1256
+ actions = tuple(_mapping(item, "Policy action") for item in actions_value)
1257
+ authority = body.get("authority")
1258
+ guardian_authority = body.get("guardianAuthority")
1259
+ guardian_delay_value = body.get("guardianDelaySeconds")
1260
+ guardian_delay = (
1261
+ guardian_delay_value if isinstance(guardian_delay_value, (int, str)) else None
1262
+ )
1263
+ return Policy(
1264
+ id=_required_string(body.get("id"), "id"),
1265
+ name=_required_string(body.get("name"), "name"),
1266
+ description=description,
1267
+ authority=(
1268
+ _mapping(authority, "Policy authority") if authority is not None else None
1269
+ ),
1270
+ actions=actions,
1271
+ guardian_enabled=body.get("guardianEnabled") is True,
1272
+ initial_key_source=_optional_string(body.get("initialKeySource")),
1273
+ guardian_authority=(
1274
+ _mapping(guardian_authority, "Policy guardian authority")
1275
+ if guardian_authority is not None
1276
+ else None
1277
+ ),
1278
+ guardian_key_source=_optional_string(body.get("guardianKeySource")),
1279
+ guardian_delay_seconds=guardian_delay,
1280
+ )
1281
+
1282
+
1283
+ def _recovery_setup_plan(
1284
+ policy: Policy,
1285
+ initial_user: WalletAuthority | None,
1286
+ recovery: RecoveryOptions | None,
1287
+ ) -> RecoverySetupPlan | None:
1288
+ authority = initial_user or _authority_from_policy(policy.authority)
1289
+ guardian = (
1290
+ recovery.guardian_pubkey if recovery else None
1291
+ ) or _public_key_from_policy(policy.guardian_authority)
1292
+ if not policy.guardian_enabled or authority is None or guardian is None:
1293
+ return None
1294
+ return RecoverySetupPlan(
1295
+ requester_authority=authority,
1296
+ guardian_pubkey=guardian,
1297
+ delay_seconds=(
1298
+ recovery.delay_seconds
1299
+ if recovery and recovery.delay_seconds is not None
1300
+ else _policy_delay(policy.guardian_delay_seconds)
1301
+ ),
1302
+ target_role_id=(recovery.target_role_id if recovery else None),
1303
+ )
1304
+
1305
+
1306
+ def _authority_from_policy(
1307
+ value: Mapping[str, object] | None,
1308
+ ) -> WalletAuthority | None:
1309
+ if value is None:
1310
+ return None
1311
+ for scheme in ("ed25519", "secp256k1", "secp256r1"):
1312
+ nested = value.get(scheme)
1313
+ if isinstance(nested, Mapping):
1314
+ public_key = nested.get("publicKey")
1315
+ if isinstance(public_key, str):
1316
+ return {scheme: {"publicKey": public_key}}
1317
+ authority_type = value.get("type")
1318
+ public_key = value.get("publicKey")
1319
+ if not isinstance(public_key, str):
1320
+ return None
1321
+ schemes = {
1322
+ "Ed25519": "ed25519",
1323
+ "Secp256k1": "secp256k1",
1324
+ "Secp256r1": "secp256r1",
1325
+ }
1326
+ policy_scheme = (
1327
+ schemes.get(authority_type) if isinstance(authority_type, str) else None
1328
+ )
1329
+ return {policy_scheme: {"publicKey": public_key}} if policy_scheme else None
1330
+
1331
+
1332
+ def _public_key_from_policy(value: Mapping[str, object] | None) -> str | None:
1333
+ if value is None:
1334
+ return None
1335
+ direct = value.get("publicKey")
1336
+ if isinstance(direct, str):
1337
+ return direct
1338
+ for scheme in ("ed25519", "secp256k1", "secp256r1"):
1339
+ nested = value.get(scheme)
1340
+ if isinstance(nested, Mapping):
1341
+ public_key = nested.get("publicKey")
1342
+ if isinstance(public_key, str):
1343
+ return public_key
1344
+ return None
1345
+
1346
+
1347
+ def _policy_delay(value: int | str | None) -> int:
1348
+ if isinstance(value, int) and value >= 0:
1349
+ return value
1350
+ if isinstance(value, str) and value.isdigit():
1351
+ return int(value)
1352
+ return 86_400
1353
+
1354
+
1355
+ def _mapping(value: object, label: str) -> Mapping[str, object]:
1356
+ if isinstance(value, Mapping):
1357
+ return value
1358
+ raise ValueError(f"{label} must be an object")
1359
+
1360
+
1361
+ def _pick(value: Mapping[str, object], *keys: str) -> object:
1362
+ for key in keys:
1363
+ if key in value:
1364
+ return value[key]
1365
+ return None
1366
+
1367
+
1368
+ def _required_string(value: object, field: str) -> str:
1369
+ if isinstance(value, str):
1370
+ return value
1371
+ raise ValueError(f"Response is missing {field}")
1372
+
1373
+
1374
+ def _optional_string(value: object) -> str | None:
1375
+ return value if isinstance(value, str) else None
1376
+
1377
+
1378
+ def _required_number(value: object, field: str) -> float:
1379
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
1380
+ return float(value)
1381
+ if isinstance(value, str) and value.strip():
1382
+ try:
1383
+ return float(value)
1384
+ except ValueError:
1385
+ pass
1386
+ raise ValueError(f"Response is missing {field}")
1387
+
1388
+
1389
+ def _required_int(value: object, field: str) -> int:
1390
+ number = _required_number(value, field)
1391
+ if not number.is_integer():
1392
+ raise ValueError(f"Response is missing {field}")
1393
+ return int(number)