swig-developer-sdk 0.7.0__tar.gz → 0.8.0__tar.gz

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.
Files changed (31) hide show
  1. swig_developer_sdk-0.8.0/.gitignore +17 -0
  2. swig_developer_sdk-0.8.0/LICENSE.txt +13 -0
  3. swig_developer_sdk-0.8.0/PKG-INFO +538 -0
  4. swig_developer_sdk-0.8.0/README.md +512 -0
  5. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/pyproject.toml +3 -3
  6. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/__init__.py +44 -24
  7. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/common.py +1 -1
  8. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/core.py +13 -5
  9. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/proxy.py +158 -199
  10. swig_developer_sdk-0.8.0/src/swig_developer_sdk/ramp.py +684 -0
  11. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/transactions.py +78 -5
  12. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/wallets.py +187 -39
  13. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/tests/test_client.py +28 -0
  14. swig_developer_sdk-0.8.0/tests/test_proxy.py +247 -0
  15. swig_developer_sdk-0.8.0/tests/test_resource_clients.py +355 -0
  16. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/uv.lock +1 -1
  17. swig_developer_sdk-0.7.0/.gitignore +0 -164
  18. swig_developer_sdk-0.7.0/PKG-INFO +0 -93
  19. swig_developer_sdk-0.7.0/README.md +0 -80
  20. swig_developer_sdk-0.7.0/src/swig_developer_sdk/ramp.py +0 -724
  21. swig_developer_sdk-0.7.0/tests/test_proxy.py +0 -103
  22. swig_developer_sdk-0.7.0/tests/test_resource_clients.py +0 -135
  23. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/scripts/local_transaction_e2e.py +0 -0
  24. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/client.py +0 -0
  25. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/evm.py +0 -0
  26. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/one_business.py +0 -0
  27. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/passkeys.py +0 -0
  28. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/paymaster.py +0 -0
  29. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/src/swig_developer_sdk/signing.py +0 -0
  30. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/tests/test_helpers.py +0 -0
  31. {swig_developer_sdk-0.7.0 → swig_developer_sdk-0.8.0}/tests/test_signing.py +0 -0
@@ -0,0 +1,17 @@
1
+ node_modules/
2
+ dist/
3
+ *.tgz
4
+ *.tsbuildinfo
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ .DS_Store
9
+ __pycache__/
10
+ *.py[cod]
11
+ .mypy_cache/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .venv/
15
+ *.egg-info/
16
+ python/dist/
17
+ output/
@@ -0,0 +1,13 @@
1
+ Copyright 2026 Swig
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,538 @@
1
+ Metadata-Version: 2.5
2
+ Name: swig-developer-sdk
3
+ Version: 0.8.0
4
+ Summary: Python SDK for preparing and locally signing Swig wallet transactions
5
+ Project-URL: Repository, https://github.com/anagrambuild/swig-developer-sdk
6
+ Author: Anagram
7
+ License: Copyright 2026 Swig
8
+
9
+ Licensed under the Apache License, Version 2.0 (the "License");
10
+ you may not use this file except in compliance with the License.
11
+ You may obtain a copy of the License at
12
+
13
+ http://www.apache.org/licenses/LICENSE-2.0
14
+
15
+ Unless required by applicable law or agreed to in writing, software
16
+ distributed under the License is distributed on an "AS IS" BASIS,
17
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ See the License for the specific language governing permissions and
19
+ limitations under the License.
20
+ License-File: LICENSE.txt
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: base58<3,>=2.1
23
+ Requires-Dist: httpx<1,>=0.28
24
+ Requires-Dist: solders<0.29,>=0.28
25
+ Description-Content-Type: text/markdown
26
+
27
+ # swig-developer-sdk
28
+
29
+ Python SDK for preparing Swig wallet operations on a server and signing them
30
+ locally. The Swig API prepares wallet transactions; this SDK invokes
31
+ application-owned signers and inserts signatures with `solders`. No signing
32
+ material is sent to the API.
33
+
34
+ - Version: `0.8.0`
35
+ - Source: <https://github.com/anagrambuild/swig-developer-sdk>
36
+ - Default API base URL: `https://api.onswig.com`
37
+
38
+ ```bash
39
+ pip install swig-developer-sdk
40
+ ```
41
+
42
+ This is the Python parity package for `@swig-wallet/developer-sdk`. Every
43
+ client method is `async`; the transport is `httpx`.
44
+
45
+ ## How it works
46
+
47
+ 1. Your server creates a `SwigClient` with an API key.
48
+ 2. Your server prepares a wallet operation and receives one or more unsigned
49
+ transactions.
50
+ 3. Your client signs any transaction whose `signature_requests` is non-empty.
51
+ 4. Your app submits the transactions in order, directly or through Swig's
52
+ paymaster sponsorship.
53
+
54
+ The API key stays on the server. Browser code calls a route in your own app —
55
+ see [Proxy handler](#proxy-handler).
56
+
57
+ ## Create a client
58
+
59
+ Create an API key from the [Swig dashboard](https://dashboard.onswig.com).
60
+
61
+ ```python
62
+ from swig_developer_sdk import SwigClient
63
+
64
+ swig = SwigClient(api_key="sk_...", network="devnet")
65
+ ```
66
+
67
+ Requests authenticate with `Authorization: Bearer <api-key>`. Override the base
68
+ URL for non-production deployments:
69
+
70
+ ```python
71
+ from swig_developer_sdk import RetryOptions, SwigClient
72
+
73
+ swig = SwigClient(
74
+ api_key="sk_...",
75
+ base_url="http://localhost:8080",
76
+ network="devnet",
77
+ retry_options=RetryOptions(max_retries=3, retry_delay=1.0),
78
+ )
79
+ ```
80
+
81
+ ### Retry behavior
82
+
83
+ - `GET` requests use the configured retry policy.
84
+ - `POST` requests do **not** retry by default, because replaying a preparation
85
+ or submission request can duplicate work.
86
+ - Sponsorship `POST` requests retry only when `idempotency_key` is set. A
87
+ matching retry returns the original paymaster response.
88
+ - `4xx` responses raise `SwigDeveloperSdkError` immediately.
89
+
90
+ ## Create a wallet
91
+
92
+ ```python
93
+ created = await swig.wallets.create(
94
+ fee_payer=fee_payer,
95
+ initial_user={"ed25519": {"publicKey": user_public_key}},
96
+ )
97
+ ```
98
+
99
+ Use `secp256r1` for a passkey initial user or `secp256k1` for an EVM key. Pass
100
+ `policy_id` to create from a portal policy instead of an inline authority. A
101
+ network is required, either on the call or as the client default.
102
+
103
+ The result splits the prepared transactions:
104
+
105
+ | Field | What to do |
106
+ | --- | --- |
107
+ | `wallet` | `swig_config_address`, `wallet_address`, and resolved `network` |
108
+ | `transactions` | submit in this exact order |
109
+ | `client_authority_transactions` | get a client authority signature first |
110
+ | `fee_payer_only_transactions` | send or sponsor without a client authority signature |
111
+ | `creation_transaction` | the create transaction itself |
112
+
113
+ A prepared transaction needs a client authority signature when
114
+ `signature_requests` is non-empty.
115
+
116
+ ## Attach to an existing wallet
117
+
118
+ ```python
119
+ wallet = swig.wallets.use(
120
+ "SWIG_CONFIG_ADDRESS",
121
+ requester_authority={"ed25519": {"publicKey": user_public_key}},
122
+ )
123
+ ```
124
+
125
+ `swig.wallets.use` also accepts a `WalletReference`, and
126
+ `swig.wallets.from_idp_session(session)` builds the same handle from an IdP
127
+ session.
128
+
129
+ ### Prepare transfers and swaps
130
+
131
+ ```python
132
+ prepared = await wallet.transfer.sol(
133
+ fee_payer=fee_payer,
134
+ destination=destination,
135
+ amount=1_000_000,
136
+ )
137
+
138
+ prepared_token = await wallet.transfer.token(
139
+ fee_payer=fee_payer,
140
+ mint=mint,
141
+ destination_owner=destination_owner,
142
+ amount=10_000,
143
+ )
144
+
145
+ prepared_swap = await wallet.swap.jupiter(
146
+ fee_payer=fee_payer,
147
+ input_mint=input_mint,
148
+ output_mint=output_mint,
149
+ amount=10_000,
150
+ slippage_bps=100,
151
+ destination_account=destination_account,
152
+ wrap_and_unwrap_sol=True,
153
+ )
154
+ ```
155
+
156
+ `wallet.transfer(...)` and `wallet.swap(...)` are callable like their
157
+ TypeScript counterparts; `spl_token` is an alias for `token`.
158
+
159
+ ### Prepare grouped operations
160
+
161
+ ```python
162
+ from swig_developer_sdk import TransferSolOperation, TransferTokenOperation
163
+
164
+ prepared = await wallet.prepare(
165
+ fee_payer=fee_payer,
166
+ operations=(
167
+ TransferSolOperation(destination=destination, amount=1_000_000),
168
+ TransferTokenOperation(
169
+ mint=mint,
170
+ destination_owner=destination_owner,
171
+ amount=10_000,
172
+ ),
173
+ ),
174
+ )
175
+ ```
176
+
177
+ ### Build a custom transaction
178
+
179
+ ```python
180
+ from swig_developer_sdk import SolanaAccountMeta, SolanaInstructionInput
181
+
182
+ prepared = await wallet.build_transaction(
183
+ fee_payer=fee_payer,
184
+ instructions=(
185
+ SolanaInstructionInput(
186
+ program_id=program_id,
187
+ accounts=(SolanaAccountMeta(pubkey=destination, is_writable=True),),
188
+ data=instruction_data,
189
+ ),
190
+ ),
191
+ address_lookup_table_accounts=(lookup_table_address,),
192
+ )
193
+ ```
194
+
195
+ The returned transaction is still signed and submitted by your application.
196
+
197
+ ## Read wallet state
198
+
199
+ ```python
200
+ usd = await wallet.get_usd_balance()
201
+ # usd.swig_config_address, usd.wallet_address, usd.usd_value
202
+
203
+ tokens = await wallet.list_token_balances()
204
+ # tokens.balances[].mint_address, asset_kind, ui_amount, usd_value
205
+
206
+ activity = await wallet.list_token_transactions(limit=25)
207
+ # activity.transactions[].transaction_signature, direction, asset_kind, ui_amount
208
+ ```
209
+
210
+ ### Roles
211
+
212
+ `list_roles()` returns the on-chain roles attached to the Swig, which is how
213
+ you inspect who currently holds authority and what each authority may do.
214
+
215
+ ```python
216
+ result = await wallet.list_roles()
217
+
218
+ for role in result.roles:
219
+ print(role.role_id, role.authority_type, role.authority_value)
220
+ for action in role.actions:
221
+ print(action.action_index, action.action_code, action.action_data)
222
+ ```
223
+
224
+ `authority_value` is the authority's public key material and `authority_type`
225
+ is the protocol authority type discriminant. `action_data` is the raw
226
+ per-action payload, so read it against the protocol's action definitions rather
227
+ than assuming a fixed shape.
228
+
229
+ Policy metadata is a separate read:
230
+
231
+ ```python
232
+ policy = await swig.wallets.get_policy(policy_id)
233
+ ```
234
+
235
+ ## Fiat ramps
236
+
237
+ Ramp is split into `swig.ramp.onramp` and `swig.ramp.offramp`. Every ramp call
238
+ requires an `environment` of `"sandbox"` or `"production"`; the SDK encodes it
239
+ as the MELD enum on the wire. Options, quotes, and session creation also require
240
+ `organization_meld_configuration_id`. Quotes additionally require
241
+ `external_customer_id`, `swig_config_address`, and a network (from
242
+ `QuoteRampArgs.network` or the client default).
243
+
244
+ | Client method | Route |
245
+ | --- | --- |
246
+ | `swig.ramp.onramp.get_options` | `GET /wallet/api/ramp/onramp/options` |
247
+ | `swig.ramp.onramp.quote` | `POST /wallet/api/ramp/onramp/quote` |
248
+ | `swig.ramp.onramp.create_session` | `POST /wallet/api/ramp/onramp/session` |
249
+ | `swig.ramp.onramp.get_session` | `GET /wallet/api/ramp/onramp/session/{session_id}` |
250
+ | `swig.ramp.offramp.get_options` | `GET /wallet/api/ramp/offramp/options` |
251
+ | `swig.ramp.offramp.quote` | `POST /wallet/api/ramp/offramp/quote` |
252
+ | `swig.ramp.offramp.create_session` | `POST /wallet/api/ramp/offramp/session` |
253
+ | `swig.ramp.offramp.prepare_authorization` | `POST /wallet/api/ramp/offramp/session/{session_id}/prepare` |
254
+ | `swig.ramp.offramp.submit_authorization` | `POST /wallet/api/ramp/offramp/session/{session_id}/submit` |
255
+ | `swig.ramp.offramp.get_session` | `GET /wallet/api/ramp/offramp/session/{session_id}` |
256
+
257
+ ### Onramp
258
+
259
+ ```python
260
+ import os
261
+
262
+ from swig_developer_sdk import QuoteRampArgs
263
+
264
+ environment = "sandbox"
265
+ configuration_id = os.environ["SWIG_MELD_CONFIG_ID"]
266
+
267
+ options = await swig.ramp.onramp.get_options(
268
+ organization_meld_configuration_id=configuration_id,
269
+ environment=environment,
270
+ country_code="US",
271
+ )
272
+
273
+ result = await swig.ramp.onramp.quote(
274
+ QuoteRampArgs(
275
+ organization_meld_configuration_id=configuration_id,
276
+ environment=environment,
277
+ external_customer_id=external_customer_id,
278
+ swig_config_address=swig_config_address,
279
+ network="devnet",
280
+ source_amount="100.00",
281
+ source_currency_code=options.fiat_currency_codes[0],
282
+ destination_currency_code=options.crypto_currency_codes[0],
283
+ country_code="US",
284
+ payment_method_type=options.payment_method_types[0],
285
+ )
286
+ )
287
+
288
+ session = await swig.ramp.onramp.create_session(
289
+ organization_meld_configuration_id=configuration_id,
290
+ quote_id=result.quotes[0].quote_id,
291
+ environment=environment,
292
+ )
293
+
294
+ # Send session.launch_url to the user. Treat it as a user-specific secret.
295
+ state = await swig.ramp.onramp.get_session(
296
+ session_id=session.session_id,
297
+ environment=environment,
298
+ )
299
+ ```
300
+
301
+ `state.status` is one of `unspecified`, `created`, `pending`, `settling`,
302
+ `settled`, `failed`, `declined`, `cancelled`, or `refunded`.
303
+
304
+ ### Offramp
305
+
306
+ Offramp adds an on-chain authorization step: the user's wallet must sign the
307
+ transfer to the provider before the session can settle.
308
+
309
+ ```python
310
+ from swig_developer_sdk import QuoteRampArgs, sign_prepared_swig_transaction
311
+
312
+ options = await swig.ramp.offramp.get_options(
313
+ organization_meld_configuration_id=configuration_id,
314
+ environment=environment,
315
+ country_code="US",
316
+ )
317
+
318
+ result = await swig.ramp.offramp.quote(
319
+ QuoteRampArgs(
320
+ organization_meld_configuration_id=configuration_id,
321
+ environment=environment,
322
+ external_customer_id=external_customer_id,
323
+ swig_config_address=swig_config_address,
324
+ network="mainnet",
325
+ source_amount="25.00",
326
+ source_currency_code=options.crypto_currencies[0].currency_code,
327
+ destination_currency_code=options.fiat_currency_codes[0],
328
+ country_code="US",
329
+ payment_method_type=options.payment_method_types[0],
330
+ )
331
+ )
332
+
333
+ session = await swig.ramp.offramp.create_session(
334
+ organization_meld_configuration_id=configuration_id,
335
+ quote_id=result.quotes[0].quote_id,
336
+ environment=environment,
337
+ )
338
+
339
+ authorization = await swig.ramp.offramp.prepare_authorization(
340
+ session_id=session.session_id,
341
+ environment=environment,
342
+ fee_payer=fee_payer,
343
+ requester_authority={"secp256r1": {"publicKey": passkey_public_key}},
344
+ )
345
+
346
+ # Show authorization.display to the user, then sign.
347
+ signed = await sign_prepared_swig_transaction(
348
+ authorization.prepared_transaction,
349
+ secp256r1=application_passkey_signer,
350
+ )
351
+
352
+ submitted = await swig.ramp.offramp.submit_authorization(
353
+ session_id=session.session_id,
354
+ environment=environment,
355
+ authorization_id=authorization.authorization_id,
356
+ signed_transaction=signed.transaction,
357
+ )
358
+ # submitted.solana_signature
359
+ ```
360
+
361
+ `authorization.display` carries the human-readable transfer summary
362
+ (`source_amount`, `destination_amount`, `service_provider`, wallet addresses,
363
+ and optional `payment_method_type` / `provider_destination_amount`). Offramp
364
+ sessions add `transfer-required` and `transfer-submitted` to the status set.
365
+
366
+ ## Sign locally
367
+
368
+ The generic signer helper works with an application-owned Ed25519 signer. The
369
+ Swig signer helper patches secp256r1 or secp256k1 signatures into both legacy
370
+ and versioned Solana transactions.
371
+
372
+ ```python
373
+ from swig_developer_sdk import (
374
+ sign_prepared_swig_transaction,
375
+ sign_prepared_transaction,
376
+ )
377
+
378
+ signed = await sign_prepared_transaction(
379
+ prepared,
380
+ sign_transaction=application_ed25519_signer,
381
+ )
382
+
383
+ signed = await sign_prepared_swig_transaction(
384
+ prepared,
385
+ secp256r1=application_passkey_signer,
386
+ )
387
+ ```
388
+
389
+ `sign_prepared_swig_transactions(...)` signs an ordered sequence, which is what
390
+ `created.client_authority_transactions` needs.
391
+
392
+ WebAuthn and EIP-1193 adapters are callback-based, so applications can connect
393
+ a browser, hardware, wallet, or remote signer without changing the preparation
394
+ API:
395
+
396
+ ```python
397
+ from swig_developer_sdk import (
398
+ create_secp256k1_evm_signing_fn,
399
+ create_secp256r1_passkey_signing_fn,
400
+ )
401
+
402
+ passkey_signer = create_secp256r1_passkey_signing_fn(get_webauthn_assertion)
403
+ evm_signer = create_secp256k1_evm_signing_fn(
404
+ provider=eip1193_provider,
405
+ address=evm_address,
406
+ )
407
+ ```
408
+
409
+ ## Submit and sponsor
410
+
411
+ ```python
412
+ from swig_developer_sdk import SponsorSignedTransactionArgs
413
+
414
+ submitted = await swig.transactions.sponsor(
415
+ SponsorSignedTransactionArgs(
416
+ transaction=signed.transaction,
417
+ network="mainnet",
418
+ idempotency_key=idempotency_key,
419
+ )
420
+ )
421
+ # submitted.request_id, submitted.signature, submitted.spent_by_paymaster
422
+ ```
423
+
424
+ Pass `idempotency_key` whenever your application may retry; that is the only
425
+ case in which the SDK retries a sponsorship POST.
426
+
427
+ For single-transaction sponsorship, `network` resolves from the call and then
428
+ the client default. If neither is set, the paymaster defaults to mainnet.
429
+
430
+ A returned signature means the Solana RPC accepted the transaction. It may
431
+ still be pending and is not confirmation or finality; track it through your
432
+ RPC provider when your product needs either.
433
+
434
+ ### Bundle sponsorship
435
+
436
+ ```python
437
+ from swig_developer_sdk import SponsorSignedTransactionBundleArgs
438
+
439
+ bundle = await swig.transactions.sponsor_bundle(
440
+ SponsorSignedTransactionBundleArgs(
441
+ transactions=(signed_create.transaction, signed_add_authority.transaction),
442
+ network="mainnet",
443
+ idempotency_key=idempotency_key,
444
+ )
445
+ )
446
+ # bundle.request_id, bundle.bundle_id, bundle.signatures,
447
+ # bundle.estimated_spent_by_paymaster
448
+ ```
449
+
450
+ Constraints enforced before the request is sent:
451
+
452
+ - mainnet only — any other network raises `ValueError`
453
+ - one to five transactions per bundle
454
+ - `signatures` come back in bundle order
455
+ - `estimated_spent_by_paymaster` is an estimate, not a settled charge
456
+
457
+ A returned `bundle_id` means Jito accepted the bundle. It may still be pending;
458
+ acceptance is not confirmation or finality.
459
+
460
+ ## Paymaster balance
461
+
462
+ ```python
463
+ balance = await swig.paymaster.get_balance(network="mainnet")
464
+ idp_balance = await swig.paymaster.get_idp_balance(network="mainnet")
465
+ # balance.configured, balance.address, balance.balance_lamports, balance.balance_sol
466
+ ```
467
+
468
+ ## Proxy handler
469
+
470
+ Python ships a framework-neutral proxy handler instead of one framework
471
+ adapter. Adapt your framework's request and response objects at the route
472
+ boundary:
473
+
474
+ ```python
475
+ from swig_developer_sdk import SwigProxyConfig, create_swig_proxy_handler
476
+
477
+ swig_handler = create_swig_proxy_handler(
478
+ SwigProxyConfig(api_key=swig_api_key, network="devnet")
479
+ )
480
+
481
+ response = await swig_handler.handle(
482
+ method="POST",
483
+ path="/transfer/sol",
484
+ body=request_body,
485
+ )
486
+ # return response.body with response.status
487
+ ```
488
+
489
+ The handler covers the same routes as the TypeScript adapters: wallet create,
490
+ grouped prepare, SOL and SPL transfers, Jupiter swap, wallet USD balance, token
491
+ balances, token transactions, roles, paymaster balance, and the onramp and
492
+ offramp routes.
493
+
494
+ `SwigProxyConfig` accepts `api_key`, `transaction_api_url`, `network`,
495
+ `fee_payer` (a value or a callable resolved per request),
496
+ `resolve_requester_authority`, and an optional `httpx` transport. It falls back
497
+ to `SWIG_DEVELOPER_API_KEY` / `SWIG_API_KEY`, `SWIG_TRANSACTION_API_URL`, and
498
+ `SWIG_FEE_PAYER` when those fields are unset. A fee payer is required for every
499
+ preparation route; the handler returns a `400` body when it cannot resolve one.
500
+
501
+ ## Errors
502
+
503
+ ```python
504
+ from swig_developer_sdk import SwigDeveloperSdkError
505
+
506
+ try:
507
+ await wallet.transfer.sol(
508
+ fee_payer=fee_payer,
509
+ destination=destination,
510
+ amount=1_000_000,
511
+ )
512
+ except SwigDeveloperSdkError as error:
513
+ print(error.status_code, error.code, error)
514
+ ```
515
+
516
+ Keep API keys, signed transactions, and ramp launch URLs out of logs.
517
+
518
+ ## Local end-to-end script
519
+
520
+ `scripts/local_transaction_e2e.py` exercises wallet creation, real P-256
521
+ signing, direct and grouped SOL transfers, an SPL-token transfer, the Python
522
+ proxy, the live paymaster balance endpoint, and a sponsored transfer where the
523
+ user pays no network fee. Every transaction is submitted to Surfpool and
524
+ verified on-chain; the paymaster flow also retries with the same idempotency
525
+ key and verifies that no balance change repeats.
526
+
527
+ It requires a locally running backend stack, and defaults to
528
+ `http://localhost:8080` for the Developer API and `http://localhost:8899` for
529
+ Surfpool. Override with `SWIG_TRANSACTION_API_URL`, `SOLANA_RPC_URL`, or
530
+ `SWIG_DATABASE_URL`.
531
+
532
+ Jupiter is not covered by this script, since it needs a mainnet-backed
533
+ Surfpool.
534
+
535
+ ## Parity
536
+
537
+ See the repository [parity matrix](../PARITY.md) for the complete mapped
538
+ surface against `@swig-wallet/developer-sdk`.