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 +111 -0
- aplanesdk/_version.py +4 -0
- aplanesdk/algokit.py +177 -0
- aplanesdk/signer.py +2151 -0
- aplanesdk-0.20.0.dist-info/METADATA +557 -0
- aplanesdk-0.20.0.dist-info/RECORD +9 -0
- aplanesdk-0.20.0.dist-info/WHEEL +5 -0
- aplanesdk-0.20.0.dist-info/licenses/LICENSE +21 -0
- aplanesdk-0.20.0.dist-info/top_level.txt +1 -0
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
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
|
+
]
|