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,461 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import inspect
5
+ import struct
6
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from typing import Protocol, TypeAlias, TypeVar, cast
9
+
10
+ from solders.instruction import CompiledInstruction
11
+ from solders.message import Message, MessageV0
12
+ from solders.pubkey import Pubkey
13
+ from solders.signature import Signature
14
+ from solders.transaction import VersionedTransaction
15
+
16
+ from .transactions import (
17
+ ClientSignatureRequest,
18
+ PreparedTransaction,
19
+ SignedPreparedTransaction,
20
+ )
21
+
22
+ SECP256R1_PROGRAM_ID = Pubkey.from_string("Secp256r1SigVerify1111111111111111111111111")
23
+ SWIG_PROGRAM_ID = Pubkey.from_string("swigypWHEksbC64pWKwah1WTeh9JXwx8H1rJHLdbQMB")
24
+ SIGN_V2_DISCRIMINATOR = 11
25
+ SECP256R1_AUTHORITY_PAYLOAD_SIZE = 17
26
+ SECP256R1_PUBLIC_KEY_SIZE = 33
27
+ SECP256R1_SIGNATURE_SIZE = 64
28
+ SECP256R1_HEADER_SIZE = 16
29
+ SECP256K1_AUTHORITY_PAYLOAD_SIZE = 77
30
+ SECP256K1_SIGNATURE_SIZE = 65
31
+ SECP256K1_SIGNATURE_OFFSET = 12
32
+ U16_MAX = 65535
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class PasskeySigningResult:
37
+ signature: bytes
38
+ prefix: bytes | None = None
39
+ message: bytes | None = None
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class Secp256k1SigningResult:
44
+ signature: bytes
45
+ prefix: bytes | None = None
46
+ message: bytes | None = None
47
+
48
+
49
+ T = TypeVar("T")
50
+ MaybeAwaitable: TypeAlias = T | Awaitable[T]
51
+ PasskeySigningFn: TypeAlias = Callable[[bytes], MaybeAwaitable[PasskeySigningResult]]
52
+ Secp256k1SigningFn: TypeAlias = Callable[
53
+ [bytes], MaybeAwaitable[Secp256k1SigningResult]
54
+ ]
55
+ PreparedTransactionSigningFn: TypeAlias = Callable[
56
+ [str, PreparedTransaction], MaybeAwaitable[str]
57
+ ]
58
+ Secp256r1SigningFns: TypeAlias = PasskeySigningFn | Mapping[str, PasskeySigningFn]
59
+ Secp256k1SigningFns: TypeAlias = Secp256k1SigningFn | Mapping[str, Secp256k1SigningFn]
60
+
61
+
62
+ class PreparedTransactionSigner(Protocol):
63
+ def sign_prepared_transaction(
64
+ self, prepared: PreparedTransaction
65
+ ) -> MaybeAwaitable[SignedPreparedTransaction]: ...
66
+
67
+
68
+ async def sign_prepared_transaction(
69
+ prepared: PreparedTransaction,
70
+ *,
71
+ sign_transaction: PreparedTransactionSigningFn,
72
+ ) -> SignedPreparedTransaction:
73
+ transaction = await _resolve(sign_transaction(prepared.transaction, prepared))
74
+ return SignedPreparedTransaction(
75
+ transaction=transaction,
76
+ transaction_encoding=prepared.transaction_encoding,
77
+ network=prepared.network,
78
+ )
79
+
80
+
81
+ async def sign_prepared_transaction_with_signer(
82
+ prepared: PreparedTransaction,
83
+ signer: PreparedTransactionSigner,
84
+ ) -> SignedPreparedTransaction:
85
+ return await _resolve(signer.sign_prepared_transaction(prepared))
86
+
87
+
88
+ async def sign_prepared_swig_transaction(
89
+ prepared: PreparedTransaction,
90
+ *,
91
+ secp256r1: Secp256r1SigningFns | None = None,
92
+ secp256k1: Secp256k1SigningFns | None = None,
93
+ ) -> SignedPreparedTransaction:
94
+ if prepared.transaction_encoding not in (None, "base64"):
95
+ raise ValueError("Only base64 prepared transactions can be signed")
96
+
97
+ transaction = _MutableTransaction.from_bytes(base64.b64decode(prepared.transaction))
98
+ for request in prepared.signature_requests:
99
+ if request.scheme == "secp256r1":
100
+ await _apply_secp256r1(transaction, request, secp256r1)
101
+ elif request.scheme == "secp256k1":
102
+ await _apply_secp256k1(transaction, request, secp256k1)
103
+
104
+ return SignedPreparedTransaction(
105
+ transaction=base64.b64encode(transaction.serialize()).decode("ascii"),
106
+ transaction_encoding=prepared.transaction_encoding or "base64",
107
+ network=prepared.network,
108
+ )
109
+
110
+
111
+ async def sign_prepared_swig_transactions(
112
+ prepared_transactions: Sequence[PreparedTransaction],
113
+ *,
114
+ secp256r1: Secp256r1SigningFns | None = None,
115
+ secp256k1: Secp256k1SigningFns | None = None,
116
+ ) -> tuple[SignedPreparedTransaction, ...]:
117
+ signed: list[SignedPreparedTransaction] = []
118
+ for prepared in prepared_transactions:
119
+ signed.append(
120
+ await sign_prepared_swig_transaction(
121
+ prepared,
122
+ secp256r1=secp256r1,
123
+ secp256k1=secp256k1,
124
+ )
125
+ )
126
+ return tuple(signed)
127
+
128
+
129
+ async def _apply_secp256r1(
130
+ transaction: _MutableTransaction,
131
+ request: ClientSignatureRequest,
132
+ signing_fns: Secp256r1SigningFns | None,
133
+ ) -> None:
134
+ if signing_fns is None:
135
+ raise ValueError("No secp256r1 signing function registered")
136
+ public_key = _hex_to_bytes(request.signer)
137
+ if len(public_key) != SECP256R1_PUBLIC_KEY_SIZE:
138
+ raise ValueError("Secp256r1 signature request signer must be 33 bytes")
139
+ message_hash = _hex_to_bytes(request.message_hash)
140
+ signing_fn = _resolve_passkey_signing_fn(request, signing_fns)
141
+ result = await _resolve(signing_fn(message_hash))
142
+ if len(result.signature) != SECP256R1_SIGNATURE_SIZE:
143
+ raise ValueError("Secp256r1 signature must be 64 bytes")
144
+
145
+ instruction_index = _find_secp256r1_instruction(
146
+ transaction, public_key, message_hash
147
+ )
148
+ transaction.set_instruction_data(
149
+ instruction_index,
150
+ _encode_secp256r1_instruction(
151
+ public_key,
152
+ result.signature,
153
+ result.message or message_hash,
154
+ ),
155
+ )
156
+ if result.prefix is not None:
157
+ sign_v2_index = _find_following_sign_v2(transaction, instruction_index)
158
+ transaction.set_instruction_data(
159
+ sign_v2_index,
160
+ _patch_authority_prefix(
161
+ transaction.instruction_data(sign_v2_index),
162
+ result.prefix,
163
+ SECP256R1_AUTHORITY_PAYLOAD_SIZE,
164
+ "secp256r1",
165
+ ),
166
+ )
167
+
168
+
169
+ async def _apply_secp256k1(
170
+ transaction: _MutableTransaction,
171
+ request: ClientSignatureRequest,
172
+ signing_fns: Secp256k1SigningFns | None,
173
+ ) -> None:
174
+ if signing_fns is None:
175
+ raise ValueError("No secp256k1 signing function registered")
176
+ signing_fn = _resolve_secp256k1_signing_fn(request, signing_fns)
177
+ message = _normalize_hex(request.message_hash).encode("ascii")
178
+ result = await _resolve(signing_fn(message))
179
+ if len(result.signature) != SECP256K1_SIGNATURE_SIZE:
180
+ raise ValueError("Secp256k1 signature must be 65 bytes")
181
+ index = _find_secp256k1_sign_v2(transaction, request)
182
+ transaction.set_instruction_data(
183
+ index,
184
+ _patch_secp256k1_payload(transaction.instruction_data(index), request, result),
185
+ )
186
+
187
+
188
+ def _resolve_passkey_signing_fn(
189
+ request: ClientSignatureRequest,
190
+ signing_fns: Secp256r1SigningFns,
191
+ ) -> PasskeySigningFn:
192
+ if callable(signing_fns):
193
+ return signing_fns
194
+ normalized = _normalize_hex(request.signer)
195
+ signing_fn = (
196
+ signing_fns.get(normalized)
197
+ or signing_fns.get(f"0x{normalized}")
198
+ or signing_fns.get(request.signer)
199
+ )
200
+ if signing_fn is None:
201
+ raise ValueError(
202
+ f"No secp256r1 signing function registered for signer {request.signer}"
203
+ )
204
+ return signing_fn
205
+
206
+
207
+ def _resolve_secp256k1_signing_fn(
208
+ request: ClientSignatureRequest,
209
+ signing_fns: Secp256k1SigningFns,
210
+ ) -> Secp256k1SigningFn:
211
+ if callable(signing_fns):
212
+ return signing_fns
213
+ normalized = _normalize_hex(request.signer)
214
+ signing_fn = (
215
+ signing_fns.get(normalized)
216
+ or signing_fns.get(f"0x{normalized}")
217
+ or signing_fns.get(request.signer)
218
+ )
219
+ if signing_fn is None:
220
+ raise ValueError(
221
+ f"No secp256k1 signing function registered for signer {request.signer}"
222
+ )
223
+ return signing_fn
224
+
225
+
226
+ class _MutableTransaction:
227
+ def __init__(
228
+ self,
229
+ message: Message | MessageV0,
230
+ signatures: Sequence[Signature],
231
+ ) -> None:
232
+ self.message = message
233
+ self.signatures = list(signatures)
234
+ self.instructions = list(message.instructions)
235
+
236
+ @classmethod
237
+ def from_bytes(cls, value: bytes) -> _MutableTransaction:
238
+ transaction = VersionedTransaction.from_bytes(value)
239
+ return cls(transaction.message, transaction.signatures)
240
+
241
+ def program_id(self, index: int) -> Pubkey:
242
+ instruction = self.instructions[index]
243
+ account_keys = self.message.account_keys
244
+ if instruction.program_id_index >= len(account_keys):
245
+ raise ValueError(
246
+ "Versioned transaction program id is not statically available"
247
+ )
248
+ return account_keys[instruction.program_id_index]
249
+
250
+ def instruction_data(self, index: int) -> bytes:
251
+ return bytes(self.instructions[index].data)
252
+
253
+ def set_instruction_data(self, index: int, data: bytes) -> None:
254
+ instruction = self.instructions[index]
255
+ self.instructions[index] = CompiledInstruction(
256
+ instruction.program_id_index,
257
+ data,
258
+ bytes(instruction.accounts),
259
+ )
260
+
261
+ def serialize(self) -> bytes:
262
+ if isinstance(self.message, MessageV0):
263
+ message: Message | MessageV0 = MessageV0(
264
+ self.message.header,
265
+ self.message.account_keys,
266
+ self.message.recent_blockhash,
267
+ self.instructions,
268
+ self.message.address_table_lookups,
269
+ )
270
+ else:
271
+ header = self.message.header
272
+ message = Message.new_with_compiled_instructions(
273
+ header.num_required_signatures,
274
+ header.num_readonly_signed_accounts,
275
+ header.num_readonly_unsigned_accounts,
276
+ self.message.account_keys,
277
+ self.message.recent_blockhash,
278
+ self.instructions,
279
+ )
280
+ return bytes(VersionedTransaction.populate(message, self.signatures))
281
+
282
+
283
+ def _find_secp256r1_instruction(
284
+ transaction: _MutableTransaction,
285
+ public_key: bytes,
286
+ message_hash: bytes,
287
+ ) -> int:
288
+ for index in range(len(transaction.instructions)):
289
+ if transaction.program_id(index) != SECP256R1_PROGRAM_ID:
290
+ continue
291
+ parsed = _parse_secp256r1_instruction(transaction.instruction_data(index))
292
+ if parsed is not None and parsed == (public_key, message_hash):
293
+ return index
294
+ raise ValueError("Matching secp256r1 signature instruction not found")
295
+
296
+
297
+ def _find_following_sign_v2(
298
+ transaction: _MutableTransaction,
299
+ secp_instruction_index: int,
300
+ ) -> int:
301
+ for index in range(secp_instruction_index + 1, len(transaction.instructions)):
302
+ if (
303
+ transaction.program_id(index) == SWIG_PROGRAM_ID
304
+ and _read_u16(transaction.instruction_data(index), 0)
305
+ == SIGN_V2_DISCRIMINATOR
306
+ ):
307
+ return index
308
+ raise ValueError("Swig SignV2 instruction following secp256r1 precompile not found")
309
+
310
+
311
+ def _find_secp256k1_sign_v2(
312
+ transaction: _MutableTransaction,
313
+ request: ClientSignatureRequest,
314
+ ) -> int:
315
+ fallback: list[int] = []
316
+ for index in range(len(transaction.instructions)):
317
+ if transaction.program_id(index) != SWIG_PROGRAM_ID:
318
+ continue
319
+ data = transaction.instruction_data(index)
320
+ if _read_u16(data, 0) != SIGN_V2_DISCRIMINATOR:
321
+ continue
322
+ offset = _authority_payload_offset(data)
323
+ if len(data) < offset + SECP256K1_AUTHORITY_PAYLOAD_SIZE:
324
+ continue
325
+ fallback.append(index)
326
+ if (
327
+ _read_u64(data, offset) == request.slot
328
+ and _read_u32(data, offset + 8) == request.counter
329
+ ):
330
+ return index
331
+ if len(fallback) == 1:
332
+ return fallback[0]
333
+ raise ValueError("Matching secp256k1 Swig SignV2 instruction not found")
334
+
335
+
336
+ def _encode_secp256r1_instruction(
337
+ public_key: bytes,
338
+ signature: bytes,
339
+ message: bytes,
340
+ ) -> bytes:
341
+ message_offset = (
342
+ SECP256R1_HEADER_SIZE + SECP256R1_PUBLIC_KEY_SIZE + SECP256R1_SIGNATURE_SIZE
343
+ )
344
+ data = bytearray(message_offset + len(message))
345
+ data[0] = 1
346
+ struct.pack_into(
347
+ "<7H",
348
+ data,
349
+ 2,
350
+ SECP256R1_HEADER_SIZE + SECP256R1_PUBLIC_KEY_SIZE,
351
+ U16_MAX,
352
+ SECP256R1_HEADER_SIZE,
353
+ U16_MAX,
354
+ message_offset,
355
+ len(message),
356
+ U16_MAX,
357
+ )
358
+ data[SECP256R1_HEADER_SIZE : SECP256R1_HEADER_SIZE + 33] = public_key
359
+ signature_start = SECP256R1_HEADER_SIZE + SECP256R1_PUBLIC_KEY_SIZE
360
+ data[signature_start : signature_start + 64] = signature
361
+ data[message_offset:] = message
362
+ return bytes(data)
363
+
364
+
365
+ def _parse_secp256r1_instruction(data: bytes) -> tuple[bytes, bytes] | None:
366
+ if len(data) < SECP256R1_HEADER_SIZE or data[0] != 1:
367
+ return None
368
+ public_key_offset = _read_u16(data, 6)
369
+ message_offset = _read_u16(data, 10)
370
+ message_size = _read_u16(data, 12)
371
+ if (
372
+ len(data) < public_key_offset + SECP256R1_PUBLIC_KEY_SIZE
373
+ or len(data) < message_offset + message_size
374
+ ):
375
+ return None
376
+ return (
377
+ data[public_key_offset : public_key_offset + SECP256R1_PUBLIC_KEY_SIZE],
378
+ data[message_offset : message_offset + message_size],
379
+ )
380
+
381
+
382
+ def _patch_authority_prefix(
383
+ instruction_data: bytes,
384
+ prefix: bytes,
385
+ fixed_payload_size: int,
386
+ scheme: str,
387
+ ) -> bytes:
388
+ offset = _authority_payload_offset(instruction_data)
389
+ if len(instruction_data) < offset + fixed_payload_size:
390
+ raise ValueError(
391
+ f"Swig SignV2 instruction is missing {scheme} authority payload"
392
+ )
393
+ return instruction_data[: offset + fixed_payload_size] + prefix
394
+
395
+
396
+ def _patch_secp256k1_payload(
397
+ instruction_data: bytes,
398
+ request: ClientSignatureRequest,
399
+ result: Secp256k1SigningResult,
400
+ ) -> bytes:
401
+ offset = _authority_payload_offset(instruction_data)
402
+ if len(instruction_data) < offset + SECP256K1_AUTHORITY_PAYLOAD_SIZE:
403
+ raise ValueError(
404
+ "Swig SignV2 instruction is missing secp256k1 authority payload"
405
+ )
406
+ if request.slot < 0 or request.slot > 0xFFFFFFFFFFFFFFFF:
407
+ raise ValueError("Secp256k1 signature request slot must fit in u64")
408
+ if request.counter < 0 or request.counter > 0xFFFFFFFF:
409
+ raise ValueError("Secp256k1 signature request counter must fit in u32")
410
+ end = offset + SECP256K1_AUTHORITY_PAYLOAD_SIZE
411
+ patched = bytearray(instruction_data[:end])
412
+ struct.pack_into("<QI", patched, offset, request.slot, request.counter)
413
+ start = offset + SECP256K1_SIGNATURE_OFFSET
414
+ patched[start : start + SECP256K1_SIGNATURE_SIZE] = result.signature
415
+ if result.prefix is not None:
416
+ patched.extend(result.prefix)
417
+ return bytes(patched)
418
+
419
+
420
+ def _authority_payload_offset(data: bytes) -> int:
421
+ return 8 + _read_u16(data, 2)
422
+
423
+
424
+ def _normalize_hex(value: str) -> str:
425
+ normalized = value[2:] if value.startswith("0x") else value
426
+ if not normalized or any(
427
+ char not in "0123456789abcdefABCDEF" for char in normalized
428
+ ):
429
+ raise ValueError("Invalid hex string")
430
+ return normalized.lower()
431
+
432
+
433
+ def _hex_to_bytes(value: str) -> bytes:
434
+ normalized = _normalize_hex(value)
435
+ if len(normalized) % 2:
436
+ raise ValueError("Hex strings must contain an even number of characters")
437
+ return bytes.fromhex(normalized)
438
+
439
+
440
+ def _read_u16(data: bytes, offset: int) -> int:
441
+ if len(data) < offset + 2:
442
+ raise ValueError("Instruction data is too short")
443
+ return int(struct.unpack_from("<H", data, offset)[0])
444
+
445
+
446
+ def _read_u32(data: bytes, offset: int) -> int:
447
+ if len(data) < offset + 4:
448
+ raise ValueError("Instruction data is too short")
449
+ return int(struct.unpack_from("<I", data, offset)[0])
450
+
451
+
452
+ def _read_u64(data: bytes, offset: int) -> int:
453
+ if len(data) < offset + 8:
454
+ raise ValueError("Instruction data is too short")
455
+ return int(struct.unpack_from("<Q", data, offset)[0])
456
+
457
+
458
+ async def _resolve(value: MaybeAwaitable[T]) -> T:
459
+ if inspect.isawaitable(value):
460
+ return await cast(Awaitable[T], value)
461
+ return value