aplanesdk 0.20.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.
aplanesdk/__init__.py ADDED
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2026 APlane Project LLC
3
+
4
+ """
5
+ APlane Python SDK - Transaction signing via apsigner
6
+
7
+ Data directory: required via data_dir parameter or APCLIENT_DATA env var
8
+
9
+ Token provisioning:
10
+ from aplanesdk import request_token_to_file
11
+ request_token_to_file() # operator must approve in apadmin
12
+
13
+ Usage:
14
+ from aplanesdk import SignerClient, send_raw_transaction
15
+
16
+ client = SignerClient.from_env()
17
+ signed_txn = client.sign_transaction(txn)
18
+ txid = send_raw_transaction(algod_client, signed_txn)
19
+ client.close()
20
+ """
21
+ from ._version import __version__
22
+
23
+ from .signer import (
24
+ # Main client
25
+ SignerClient,
26
+
27
+ # Submission helpers
28
+ send_raw_transaction,
29
+ assemble_group,
30
+
31
+ # Token provisioning
32
+ request_token,
33
+ request_token_to_file,
34
+
35
+ # Utility
36
+ load_token,
37
+ load_config,
38
+
39
+ # Exceptions
40
+ SignerError,
41
+ AuthenticationError,
42
+ SigningRejectedError,
43
+ SignerUnavailableError,
44
+ KeyNotFoundError,
45
+ KeyDeletionError,
46
+ TokenProvisioningError,
47
+ TransactionRejectedError,
48
+ LogicSigRejectedError,
49
+ InsufficientFundsError,
50
+ InvalidTransactionError,
51
+
52
+ # Types
53
+ RuntimeArg,
54
+ SigningArg,
55
+ InputModeInfo,
56
+ KeyInfo,
57
+ SSHConfig,
58
+ ClientConfig,
59
+ CreationParam,
60
+ KeyTypeInfo,
61
+ StatusResponse,
62
+ CancelSignResponse,
63
+ GroupSignResponse,
64
+ ErrorResponse,
65
+ GenerateResult,
66
+ )
67
+
68
+ __all__ = [
69
+ # Main client
70
+ "SignerClient",
71
+
72
+ # Submission helpers
73
+ "send_raw_transaction",
74
+ "assemble_group",
75
+
76
+ # Token provisioning
77
+ "request_token",
78
+ "request_token_to_file",
79
+
80
+ # Utility
81
+ "load_token",
82
+ "load_config",
83
+
84
+ # Exceptions
85
+ "SignerError",
86
+ "AuthenticationError",
87
+ "SigningRejectedError",
88
+ "SignerUnavailableError",
89
+ "KeyNotFoundError",
90
+ "KeyDeletionError",
91
+ "TokenProvisioningError",
92
+ "TransactionRejectedError",
93
+ "LogicSigRejectedError",
94
+ "InsufficientFundsError",
95
+ "InvalidTransactionError",
96
+
97
+ # Types
98
+ "RuntimeArg",
99
+ "SigningArg",
100
+ "InputModeInfo",
101
+ "KeyInfo",
102
+ "SSHConfig",
103
+ "ClientConfig",
104
+ "CreationParam",
105
+ "KeyTypeInfo",
106
+ "StatusResponse",
107
+ "CancelSignResponse",
108
+ "GroupSignResponse",
109
+ "ErrorResponse",
110
+ "GenerateResult",
111
+ ]
aplanesdk/_version.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2026 APlane Project LLC
3
+
4
+ __version__ = "0.20.0"
aplanesdk/algokit.py ADDED
@@ -0,0 +1,177 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2026 APlane Project LLC
3
+
4
+ """AlgoKit Utils adapter for apsigner-backed transaction signing."""
5
+
6
+ from collections.abc import Callable, Sequence
7
+ from secrets import token_hex
8
+ from threading import Lock
9
+ from typing import Any, Optional, Protocol
10
+
11
+ from .signer import CancelSignResponse, GroupSignResponse, KeyInfo, SignerError
12
+
13
+
14
+ class _GroupSignerClient(Protocol):
15
+ def sign_requests(
16
+ self,
17
+ sign_entries: list[dict[str, Any]],
18
+ *,
19
+ request_id: Optional[str] = None,
20
+ ) -> GroupSignResponse: ...
21
+
22
+ def list_keys(self, refresh: bool = False) -> list[KeyInfo]: ...
23
+
24
+ def cancel_sign_request(self, request_id: str) -> CancelSignResponse: ...
25
+
26
+
27
+ TransactionEncoder = Callable[[Any], bytes]
28
+ RequestIDFactory = Callable[[], str]
29
+
30
+
31
+ def _default_request_id() -> str:
32
+ return f"sdk-{token_hex(16)}"
33
+
34
+
35
+ def _default_encode_transaction(txn: Any) -> bytes:
36
+ try:
37
+ from algokit_transact.codec.transaction import encode_transaction
38
+ except ImportError as exc:
39
+ raise SignerError(
40
+ "algokit_transact is not importable; install algokit-utils >= 5.0.0b1 "
41
+ "or pass an explicit encode_transaction"
42
+ ) from exc
43
+ return encode_transaction(txn)
44
+
45
+
46
+ def _txn_sender(txn: Any) -> str:
47
+ sender = getattr(txn, "sender", None)
48
+ if sender is None:
49
+ raise SignerError("AlgoKit transaction is missing sender")
50
+ return str(sender)
51
+
52
+
53
+ class ApsignerAccount:
54
+ """
55
+ AlgoKit AddressWithTransactionSigner adapter backed by apsigner.
56
+
57
+ The adapter connects AlgoKit clients to APlane's transaction signing
58
+ functions.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ client: _GroupSignerClient,
64
+ address: str,
65
+ *,
66
+ auth_address: Optional[str] = None,
67
+ lsig_args: Optional[dict[str, bytes]] = None,
68
+ new_request_id: Optional[RequestIDFactory] = None,
69
+ encode_transaction: Optional[TransactionEncoder] = None,
70
+ ) -> None:
71
+ self._client = client
72
+ self._addr = address
73
+ self.auth_address = auth_address or address
74
+ self._lsig_args = lsig_args
75
+ self._new_request_id = new_request_id or _default_request_id
76
+ self._encode_transaction = encode_transaction or _default_encode_transaction
77
+ self._current_request_id: Optional[str] = None
78
+ self._lock = Lock()
79
+
80
+ @property
81
+ def addr(self) -> str:
82
+ return self._addr
83
+
84
+ @property
85
+ def signer(self) -> Callable[[Sequence[Any], Sequence[int]], list[bytes]]:
86
+ return self._sign
87
+
88
+ def _begin_sign(self) -> str:
89
+ request_id = self._new_request_id()
90
+ with self._lock:
91
+ if self._current_request_id is not None:
92
+ raise RuntimeError("ApsignerAccount already has an in-flight signing request")
93
+ self._current_request_id = request_id
94
+ return request_id
95
+
96
+ def _end_sign(self, request_id: str) -> None:
97
+ with self._lock:
98
+ if self._current_request_id == request_id:
99
+ self._current_request_id = None
100
+
101
+ def cancel(self) -> None:
102
+ """
103
+ Best-effort cancellation for the current synchronous AlgoKit sign call.
104
+
105
+ AlgoKit's Python signer callback has no cancellation parameter, so
106
+ applications that need user cancellation can call this from another
107
+ thread while the signer is waiting for operator approval.
108
+ """
109
+ with self._lock:
110
+ request_id = self._current_request_id
111
+ if request_id is None:
112
+ return
113
+ try:
114
+ self._client.cancel_sign_request(request_id)
115
+ except (SignerError, OSError):
116
+ # Best-effort: backend errors and transport failures are tolerated,
117
+ # but programming errors (TypeError, AttributeError, ...) propagate.
118
+ pass
119
+
120
+ def _sign(self, txn_group: Sequence[Any], indexes_to_sign: Sequence[int]) -> list[bytes]:
121
+ if not indexes_to_sign:
122
+ return []
123
+
124
+ request_id = self._begin_sign()
125
+ try:
126
+ requests: list[dict[str, Any]] = []
127
+ for index in indexes_to_sign:
128
+ if index < 0 or index >= len(txn_group):
129
+ raise SignerError(
130
+ f"index {index} out of range for {len(txn_group)} transactions"
131
+ )
132
+
133
+ txn = txn_group[index]
134
+ if txn is None:
135
+ raise SignerError(f"transaction is required at index {index}")
136
+
137
+ req = {
138
+ "txn_bytes_hex": self._encode_transaction(txn).hex(),
139
+ "txn_sender": _txn_sender(txn),
140
+ "auth_address": self.auth_address,
141
+ }
142
+ if self._lsig_args:
143
+ req["lsig_args"] = {
144
+ name: value.hex()
145
+ for name, value in self._lsig_args.items()
146
+ }
147
+ requests.append(req)
148
+
149
+ result = self._client.sign_requests(requests, request_id=request_id)
150
+
151
+ if len(result.signed) < len(indexes_to_sign):
152
+ raise SignerError(
153
+ "apsigner returned fewer signed transactions than AlgoKit requested"
154
+ )
155
+
156
+ return [bytes.fromhex(item) for item in result.signed]
157
+ finally:
158
+ self._end_sign(request_id)
159
+
160
+
161
+ def create_apsigner_account(
162
+ client: _GroupSignerClient,
163
+ address: str,
164
+ **kwargs: Any,
165
+ ) -> ApsignerAccount:
166
+ return ApsignerAccount(client, address, **kwargs)
167
+
168
+
169
+ def list_apsigner_accounts(
170
+ client: _GroupSignerClient,
171
+ *,
172
+ refresh: bool = False,
173
+ ) -> list[ApsignerAccount]:
174
+ return [
175
+ ApsignerAccount(client, key.address, auth_address=key.address)
176
+ for key in client.list_keys(refresh=refresh)
177
+ ]