aplanesdk 0.20.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.
- aplanesdk-0.20.0/LICENSE +21 -0
- aplanesdk-0.20.0/MANIFEST.in +17 -0
- aplanesdk-0.20.0/PKG-INFO +557 -0
- aplanesdk-0.20.0/README.md +526 -0
- aplanesdk-0.20.0/aplanesdk/__init__.py +111 -0
- aplanesdk-0.20.0/aplanesdk/_version.py +4 -0
- aplanesdk-0.20.0/aplanesdk/algokit.py +177 -0
- aplanesdk-0.20.0/aplanesdk/signer.py +2151 -0
- aplanesdk-0.20.0/aplanesdk.egg-info/SOURCES.txt +18 -0
- aplanesdk-0.20.0/examples/algokit_self_send.py +34 -0
- aplanesdk-0.20.0/examples/asa_transfer.py +67 -0
- aplanesdk-0.20.0/examples/atomic_swap.py +85 -0
- aplanesdk-0.20.0/examples/config.yaml.example +12 -0
- aplanesdk-0.20.0/examples/htlc_claim.py +84 -0
- aplanesdk-0.20.0/examples/send.py +59 -0
- aplanesdk-0.20.0/pyproject.toml +59 -0
- aplanesdk-0.20.0/setup.cfg +4 -0
- aplanesdk-0.20.0/tests/test_algokit.py +230 -0
- aplanesdk-0.20.0/tests/test_client.py +919 -0
- aplanesdk-0.20.0/tests/test_contracts.py +216 -0
- aplanesdk-0.20.0/tests/test_integration.py +170 -0
aplanesdk-0.20.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 APlane Project LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Include documentation
|
|
2
|
+
include README.md
|
|
3
|
+
include LICENSE
|
|
4
|
+
|
|
5
|
+
# Include examples
|
|
6
|
+
recursive-include examples *.py *.yaml.example
|
|
7
|
+
|
|
8
|
+
# Exclude build artifacts
|
|
9
|
+
global-exclude __pycache__
|
|
10
|
+
global-exclude *.py[cod]
|
|
11
|
+
global-exclude *.so
|
|
12
|
+
global-exclude .DS_Store
|
|
13
|
+
prune *.egg-info
|
|
14
|
+
prune build
|
|
15
|
+
prune dist
|
|
16
|
+
prune .pytest_cache
|
|
17
|
+
prune .mypy_cache
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aplanesdk
|
|
3
|
+
Version: 0.20.0
|
|
4
|
+
Summary: Python SDK for APlane transaction signing
|
|
5
|
+
Author: APlane Project LLC
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aplane-algo/aplanesdk
|
|
8
|
+
Project-URL: Documentation, https://github.com/aplane-algo/aplanesdk/tree/main/python
|
|
9
|
+
Project-URL: Repository, https://github.com/aplane-algo/aplanesdk
|
|
10
|
+
Keywords: algorand,blockchain,signing,post-quantum,falcon
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security :: Cryptography
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: py-algorand-sdk>=2.0.0
|
|
22
|
+
Requires-Dist: paramiko>=3.0.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: requests>=2.25.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# APlane Python SDK
|
|
33
|
+
|
|
34
|
+
Python SDK for signing Algorand transactions via apsigner.
|
|
35
|
+
|
|
36
|
+
## Versioning
|
|
37
|
+
|
|
38
|
+
SDK packages are published only when the SDK changes. SDK versions track
|
|
39
|
+
compatible APlane release tags and may skip product release numbers.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install aplanesdk
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The published package is `aplanesdk` on PyPI.
|
|
48
|
+
|
|
49
|
+
For the AlgoKit adapter, install AlgoKit Utils in the same environment:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install 'algokit-utils>=5.0.0b1'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or install from source:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd python
|
|
59
|
+
pip install -e .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from aplanesdk import SignerClient, send_raw_transaction
|
|
66
|
+
from algosdk import transaction
|
|
67
|
+
from algosdk.v2client import algod
|
|
68
|
+
|
|
69
|
+
# Connect to signer (reads config.yaml and token from data dir)
|
|
70
|
+
client = SignerClient.from_env()
|
|
71
|
+
|
|
72
|
+
# Build transaction with algosdk
|
|
73
|
+
algod_client = algod.AlgodClient("", "https://testnet-api.4160.nodely.dev")
|
|
74
|
+
params = algod_client.suggested_params()
|
|
75
|
+
|
|
76
|
+
txn = transaction.PaymentTxn(
|
|
77
|
+
sender="SENDER_ADDRESS",
|
|
78
|
+
sp=params,
|
|
79
|
+
receiver="RECEIVER_ADDRESS",
|
|
80
|
+
amt=1000000 # 1 ALGO
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Sign via apsigner (waits for operator approval)
|
|
84
|
+
signed = client.sign_transaction(txn)
|
|
85
|
+
|
|
86
|
+
# Submit to network (signed is ready to use, no processing needed)
|
|
87
|
+
txid = send_raw_transaction(algod_client, signed)
|
|
88
|
+
print(f"Submitted: {txid}")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Connection Methods
|
|
92
|
+
|
|
93
|
+
All SDK connections use the configured SSH-backed signer path. Direct local HTTP connection is not a supported SDK mode.
|
|
94
|
+
|
|
95
|
+
### Environment-Based Connection (Recommended)
|
|
96
|
+
|
|
97
|
+
Load configuration from a data directory. The directory is required — pass
|
|
98
|
+
`data_dir` or set the `APCLIENT_DATA` environment variable:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Set environment variable
|
|
102
|
+
# export APCLIENT_DATA=~/aplane/apclient
|
|
103
|
+
|
|
104
|
+
client = SignerClient.from_env()
|
|
105
|
+
|
|
106
|
+
# Or pass directly
|
|
107
|
+
client = SignerClient.from_env(data_dir="~/aplane/apclient")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Data directory structure (installer default: `~/aplane/apclient`):
|
|
111
|
+
```
|
|
112
|
+
<data_dir>/
|
|
113
|
+
config.yaml # Connection settings
|
|
114
|
+
aplane.token # Authentication token
|
|
115
|
+
.ssh/
|
|
116
|
+
id_ed25519 # SSH private key for authentication
|
|
117
|
+
known_hosts # Trusted server host keys
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Example `config.yaml`:
|
|
121
|
+
```yaml
|
|
122
|
+
signer_port: 11270
|
|
123
|
+
ssh:
|
|
124
|
+
host: localhost # Change to remote host if signer is on another machine
|
|
125
|
+
port: 1127
|
|
126
|
+
identity_file: .ssh/id_ed25519
|
|
127
|
+
known_hosts_path: .ssh/known_hosts
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Direct SSH Connection
|
|
131
|
+
|
|
132
|
+
Connect explicitly via SSH tunnel with 2FA:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
client = SignerClient.connect_ssh(
|
|
136
|
+
host="signer.example.com",
|
|
137
|
+
token="your-token", # used for both SSH auth and HTTP API
|
|
138
|
+
ssh_key_path="~/.ssh/id_ed25519",
|
|
139
|
+
ssh_port=1127, # default: 1127
|
|
140
|
+
signer_port=11270, # default: 11270
|
|
141
|
+
timeout=30 # optional explicit shorter request timeout
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Note**: SSH uses 2FA (token + public key). The token is passed as the SSH
|
|
146
|
+
username. Keys are enrolled via the `request-token` operator-approved flow.
|
|
147
|
+
|
|
148
|
+
The SSH tunnel is established automatically. Remember to close when done:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
client.close()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or use as a context manager:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
with SignerClient.connect_ssh(host="...", token="...", ssh_key_path="...") as client:
|
|
158
|
+
signed = client.sign_transaction(txn)
|
|
159
|
+
# Tunnel closed automatically
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Authentication
|
|
163
|
+
|
|
164
|
+
The recommended way to obtain a token is via the `request-token` flow, which enrolls your SSH key and provisions a token in a single operator-approved step. The token is saved automatically to `$APCLIENT_DATA/aplane.token`.
|
|
165
|
+
|
|
166
|
+
If your token was provisioned separately (e.g. copied by the operator), you can load it explicitly:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from aplanesdk import load_token
|
|
170
|
+
|
|
171
|
+
token = load_token("/path/to/apclient/aplane.token")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### SignerClient
|
|
177
|
+
|
|
178
|
+
#### `health() -> bool`
|
|
179
|
+
|
|
180
|
+
Check if signer is reachable.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
if client.health():
|
|
184
|
+
print("Signer is online")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### `get_status() -> StatusResponse`
|
|
188
|
+
|
|
189
|
+
Fetch authenticated signer status. This works while the signer is locked.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
identity = client.get_status()
|
|
193
|
+
print(identity.state, identity.keyset_revision)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`keyset_revision` is process-local and useful for deciding when to refresh
|
|
197
|
+
`list_keys(refresh=True)`; it is not durable across apsigner restarts.
|
|
198
|
+
`approval_wait_seconds` is used by the SDK to size `/sign` deadlines.
|
|
199
|
+
|
|
200
|
+
#### `list_keys() -> List[KeyInfo]`
|
|
201
|
+
|
|
202
|
+
List available signing keys.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
keys = client.list_keys()
|
|
206
|
+
for key in keys:
|
|
207
|
+
print(f"{key.address} [{key.key_type}]")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Returns list of `KeyInfo`:
|
|
211
|
+
- `address`: Algorand address
|
|
212
|
+
- `key_type`: "ed25519", "aplane.falcon1024.v1", "aplane.timelock.v1", etc.
|
|
213
|
+
- `lsig_size`: LogicSig size (for budget calculation)
|
|
214
|
+
- `is_generic_lsig`: True if no cryptographic signature needed
|
|
215
|
+
- `signing_args`: List of `SigningArg` for LogicSigs (name, arg_type, description)
|
|
216
|
+
|
|
217
|
+
**Discovering required arguments for generic LogicSigs:**
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
key_info = client.get_key_info(hashlock_address)
|
|
221
|
+
if key_info.signing_args:
|
|
222
|
+
for arg in key_info.signing_args:
|
|
223
|
+
print(f"{arg.name}: {arg.arg_type} - {arg.description}")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `sign_transaction(txn, auth_address=None, lsig_args=None) -> str`
|
|
227
|
+
|
|
228
|
+
Sign a single transaction. Returns a base64-encoded string ready for submission.
|
|
229
|
+
|
|
230
|
+
The server automatically handles fee pooling for large LogicSigs (e.g., Falcon-1024) by adding dummy transactions as needed.
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# Basic signing (uses txn.sender as auth_address)
|
|
234
|
+
signed = client.sign_transaction(txn)
|
|
235
|
+
|
|
236
|
+
# Rekeyed account (different auth key)
|
|
237
|
+
signed = client.sign_transaction(txn, auth_address="SIGNER_KEY_ADDRESS")
|
|
238
|
+
|
|
239
|
+
# Generic LogicSig with runtime args (e.g., HTLC)
|
|
240
|
+
signed = client.sign_transaction(
|
|
241
|
+
txn,
|
|
242
|
+
auth_address="HASHLOCK_ADDRESS",
|
|
243
|
+
lsig_args={"preimage": b"secret_value"}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Submit directly (no processing needed)
|
|
247
|
+
txid = send_raw_transaction(algod_client, signed)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `sign_transactions(txns, auth_addresses=None, lsig_args_map=None) -> str`
|
|
251
|
+
|
|
252
|
+
Sign multiple transactions as a group. Returns a base64-encoded string of concatenated signed transactions, ready for submission.
|
|
253
|
+
|
|
254
|
+
**Important**: Do NOT pre-assign group IDs. The server computes the group ID after adding any required dummy transactions for large LogicSigs.
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
# Build transactions (do NOT call assign_group_id)
|
|
258
|
+
txn1 = transaction.PaymentTxn(sender=addr1, sp=params, receiver=addr2, amt=100000)
|
|
259
|
+
txn2 = transaction.PaymentTxn(sender=addr2, sp=params, receiver=addr1, amt=100000)
|
|
260
|
+
|
|
261
|
+
# Sign group (server handles grouping and dummies)
|
|
262
|
+
signed = client.sign_transactions([txn1, txn2])
|
|
263
|
+
|
|
264
|
+
# Submit directly (no processing needed)
|
|
265
|
+
txid = algod_client.send_raw_transaction(signed)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### `sign_transactions_list(txns, auth_addresses=None, lsig_args_map=None) -> List[str]`
|
|
269
|
+
|
|
270
|
+
Like `sign_transactions()` but returns individual base64-encoded transactions instead of concatenated. Useful when you need to inspect transactions individually.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
signed_list = client.sign_transactions_list([txn1, txn2])
|
|
274
|
+
# signed_list is List[str], each element is a base64-encoded signed transaction
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### `sign_requests(sign_entries, request_id=None) -> GroupSignResponse`
|
|
278
|
+
|
|
279
|
+
Send one or more raw `/sign` request entries. Use this when an integration
|
|
280
|
+
already owns transaction encoding and wants APlane's native response shape.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
response = client.sign_requests(
|
|
284
|
+
[{
|
|
285
|
+
"txn_bytes_hex": "5458...",
|
|
286
|
+
"auth_address": "SIGNER_KEY_ADDRESS",
|
|
287
|
+
"txn_sender": "SENDER_ADDRESS", # advisory display hint only
|
|
288
|
+
}],
|
|
289
|
+
request_id="app-owned-request-id",
|
|
290
|
+
)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### AlgoKit Utils Adapter
|
|
294
|
+
|
|
295
|
+
For AlgoKit Utils 4 (utils-py v5) transaction composers, use the adapter
|
|
296
|
+
account. It connects AlgoKit clients to APlane's transaction signing functions
|
|
297
|
+
and presents the `addr` + `signer(txn_group, indexes_to_sign)` shape.
|
|
298
|
+
|
|
299
|
+
The minimal repository example is `examples/algokit_self_send.py`. From a
|
|
300
|
+
checkout, run it as a module so it imports the local SDK source:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
cd ~/aplanesdk/python
|
|
304
|
+
export APCLIENT_DATA=~/aplane/apclient
|
|
305
|
+
export APLANE_ADDRESS=SENDER_ADDRESS
|
|
306
|
+
python -m examples.algokit_self_send
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The example builds a transaction with AlgoKit, signs it through the APlane
|
|
310
|
+
adapter, then submits the signed blobs with AlgoKit's algod client:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from algokit_utils import AlgoAmount, AlgorandClient, PaymentParams
|
|
314
|
+
from aplanesdk import SignerClient
|
|
315
|
+
from aplanesdk.algokit import create_apsigner_account
|
|
316
|
+
|
|
317
|
+
sender = "SENDER_ADDRESS"
|
|
318
|
+
algorand = AlgorandClient.testnet()
|
|
319
|
+
|
|
320
|
+
with SignerClient.from_env() as signer:
|
|
321
|
+
auth = algorand.client.algod.account_information(sender).auth_addr or sender
|
|
322
|
+
account = create_apsigner_account(signer, sender, auth_address=auth)
|
|
323
|
+
txn = algorand.create_transaction.payment(
|
|
324
|
+
PaymentParams(
|
|
325
|
+
sender=sender,
|
|
326
|
+
signer=account,
|
|
327
|
+
receiver=sender,
|
|
328
|
+
amount=AlgoAmount(micro_algo=0),
|
|
329
|
+
validity_window=1000,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
signed = account.signer([txn], [0])
|
|
333
|
+
tx_id = algorand.client.algod.send_raw_transaction(signed).tx_id
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Use `create_transaction.*` when APlane must own final signing and any
|
|
337
|
+
APlane-managed group expansion. `algorand.send.*` owns the composer send path
|
|
338
|
+
and signs inside that path.
|
|
339
|
+
|
|
340
|
+
The Python AlgoKit signer is synchronous. `ApsignerAccount` tracks one active
|
|
341
|
+
signing request at a time; overlapping calls on the same account raise
|
|
342
|
+
`RuntimeError`. Use separate account objects for concurrent signing. For
|
|
343
|
+
asyncio applications, run the AlgoKit call site in a worker thread, for example
|
|
344
|
+
with `asyncio.to_thread(...)`. If an application needs to own request IDs, pass
|
|
345
|
+
`new_request_id`, a callable that returns a fresh ID for each sign call.
|
|
346
|
+
|
|
347
|
+
Signing calls discover `/status.approval_wait_seconds` and use that value
|
|
348
|
+
plus 30 seconds of slack for the request timeout. If discovery fails or an older
|
|
349
|
+
signer omits the field, signing falls back to 6 minutes. An explicit shorter
|
|
350
|
+
timeout still wins; SDK `/sign` calls include a `request_id` and send a
|
|
351
|
+
best-effort `/sign/cancel` when the HTTP request times out or disconnects.
|
|
352
|
+
High-level signing methods accept an optional keyword-only `request_id`.
|
|
353
|
+
AlgoKit adapter callers can call `account.cancel()` from another thread to
|
|
354
|
+
cancel the in-flight adapter request.
|
|
355
|
+
|
|
356
|
+
#### `cancel_sign_request(request_id) -> CancelSignResponse`
|
|
357
|
+
|
|
358
|
+
Ask apsigner to cancel a live synchronous `/sign` request by request ID.
|
|
359
|
+
Successful responses are idempotent for client behavior and return state
|
|
360
|
+
`"canceled"` or `"not_found"`.
|
|
361
|
+
|
|
362
|
+
Python high-level signing generates a request ID by default. Interactive
|
|
363
|
+
applications can pass an application-owned ID, then call
|
|
364
|
+
`cancel_sign_request()` with the same value from another thread:
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
request_id = "wallet-ui-approval-123"
|
|
368
|
+
signed = client.sign_transaction(txn, request_id=request_id)
|
|
369
|
+
# elsewhere, if the user aborts while approval is pending:
|
|
370
|
+
client.cancel_sign_request(request_id)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
For AlgoKit adapter signing, call `cancel()` on the account:
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
account.cancel()
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### `close()`
|
|
380
|
+
|
|
381
|
+
Close the client and SSH tunnel (if any).
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
client.close()
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Supported Key Types
|
|
388
|
+
|
|
389
|
+
| Key Type | Description | Notes |
|
|
390
|
+
|----------|-------------|-------|
|
|
391
|
+
| `ed25519` | Native Algorand keys | Standard signing |
|
|
392
|
+
| `aplane.falcon1024.v*` | Post-quantum LogicSig | Signature in LogicSig.Args[0] |
|
|
393
|
+
| `aplane.timelock.v*` | Time-locked funds | No signature, TEAL-only |
|
|
394
|
+
| `aplane.htlc.v*` | Hash-locked funds | Requires `preimage` arg (check `signing_args`) |
|
|
395
|
+
|
|
396
|
+
The server assembles the complete signed transaction - the SDK returns a base64 string ready for submission.
|
|
397
|
+
|
|
398
|
+
## Error Handling
|
|
399
|
+
|
|
400
|
+
### Signing Exceptions
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
from aplanesdk import (
|
|
404
|
+
SignerError,
|
|
405
|
+
AuthenticationError,
|
|
406
|
+
SigningRejectedError,
|
|
407
|
+
SignerUnavailableError,
|
|
408
|
+
KeyNotFoundError
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
signed = client.sign_transaction(txn)
|
|
413
|
+
except AuthenticationError:
|
|
414
|
+
print("Invalid token")
|
|
415
|
+
except SigningRejectedError:
|
|
416
|
+
print("Operator rejected the request")
|
|
417
|
+
except SignerUnavailableError:
|
|
418
|
+
print("Signer not reachable or locked")
|
|
419
|
+
except KeyNotFoundError:
|
|
420
|
+
print("Key not found in signer")
|
|
421
|
+
except SignerError as e:
|
|
422
|
+
print(f"Signing failed: {e}")
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Submission Exceptions
|
|
426
|
+
|
|
427
|
+
`send_raw_transaction()` wraps verbose algod errors into clean exceptions:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
from aplanesdk import (
|
|
431
|
+
send_raw_transaction,
|
|
432
|
+
TransactionRejectedError,
|
|
433
|
+
LogicSigRejectedError,
|
|
434
|
+
InsufficientFundsError,
|
|
435
|
+
InvalidTransactionError
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
txid = send_raw_transaction(algod_client, signed)
|
|
440
|
+
except LogicSigRejectedError as e:
|
|
441
|
+
print(f"LogicSig failed: {e.reason}") # e.txid also available
|
|
442
|
+
except InsufficientFundsError as e:
|
|
443
|
+
print(f"Not enough funds: {e.reason}")
|
|
444
|
+
except InvalidTransactionError as e:
|
|
445
|
+
print(f"Invalid transaction: {e.reason}")
|
|
446
|
+
except TransactionRejectedError as e:
|
|
447
|
+
print(f"Rejected: {e.reason}")
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Example: Complete Workflow
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
#!/usr/bin/env python3
|
|
454
|
+
from aplanesdk import SignerClient, load_token, SignerError, send_raw_transaction
|
|
455
|
+
from algosdk import transaction
|
|
456
|
+
from algosdk.v2client import algod
|
|
457
|
+
|
|
458
|
+
def main():
|
|
459
|
+
# Load token
|
|
460
|
+
token = load_token("~/aplane/apclient/aplane.token")
|
|
461
|
+
|
|
462
|
+
# Connect via SSH (token is used as SSH username for 2FA)
|
|
463
|
+
with SignerClient.connect_ssh(
|
|
464
|
+
host="signer.example.com",
|
|
465
|
+
token=token,
|
|
466
|
+
ssh_key_path="~/.ssh/id_ed25519"
|
|
467
|
+
) as client:
|
|
468
|
+
|
|
469
|
+
# List keys
|
|
470
|
+
keys = client.list_keys()
|
|
471
|
+
sender = keys[0].address
|
|
472
|
+
print(f"Using: {sender}")
|
|
473
|
+
|
|
474
|
+
# Build transaction
|
|
475
|
+
algod_client = algod.AlgodClient("", "https://testnet-api.4160.nodely.dev")
|
|
476
|
+
params = algod_client.suggested_params()
|
|
477
|
+
|
|
478
|
+
txn = transaction.PaymentTxn(
|
|
479
|
+
sender=sender,
|
|
480
|
+
sp=params,
|
|
481
|
+
receiver=sender,
|
|
482
|
+
amt=0
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Sign (will wait for operator approval)
|
|
486
|
+
try:
|
|
487
|
+
signed = client.sign_transaction(txn)
|
|
488
|
+
print("Signed!")
|
|
489
|
+
|
|
490
|
+
# Submit directly (no processing needed)
|
|
491
|
+
txid = send_raw_transaction(algod_client, signed)
|
|
492
|
+
print(f"TxID: {txid}")
|
|
493
|
+
|
|
494
|
+
# Wait for confirmation
|
|
495
|
+
result = transaction.wait_for_confirmation(algod_client, txid, 4)
|
|
496
|
+
print(f"Confirmed in round {result['confirmed-round']}")
|
|
497
|
+
|
|
498
|
+
except SignerError as e:
|
|
499
|
+
print(f"Failed: {e}")
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
main()
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Fee Pooling (Large LogicSigs)
|
|
506
|
+
|
|
507
|
+
Algorand limits LogicSig size to 1000 bytes per transaction. Large signatures like Falcon-1024 (~3000 bytes) exceed this limit.
|
|
508
|
+
|
|
509
|
+
**Solution**: The server automatically creates dummy transactions to expand the LogicSig budget pool. Each transaction in a group contributes 1000 bytes to the shared pool.
|
|
510
|
+
|
|
511
|
+
### How It Works (Server-Side)
|
|
512
|
+
|
|
513
|
+
1. Server detects key's `lsig_size` exceeds available budget
|
|
514
|
+
2. Server calculates dummies needed: `ceil(total_lsig_bytes / 1000) - num_txns`
|
|
515
|
+
3. Server creates dummy self-payment transactions (0 amount, min fee)
|
|
516
|
+
4. Server distributes dummy fees across LogicSig transactions in the group
|
|
517
|
+
5. Server computes group ID and signs all transactions
|
|
518
|
+
6. SDK returns concatenated signed group ready for submission
|
|
519
|
+
|
|
520
|
+
### Example: Falcon-1024 Key
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
# Falcon-1024 has lsig_size ~3035 bytes, needs 3 dummies
|
|
524
|
+
# Total group: 1 main + 3 dummies = 4 transactions
|
|
525
|
+
# Pool budget: 4 x 1000 = 4000 bytes (enough for 3035)
|
|
526
|
+
|
|
527
|
+
params = algod_client.suggested_params()
|
|
528
|
+
txn = transaction.PaymentTxn(sender=falcon_addr, sp=params, receiver=receiver, amt=1000000)
|
|
529
|
+
|
|
530
|
+
# Server automatically adds dummies - just sign and submit
|
|
531
|
+
signed = client.sign_transaction(txn)
|
|
532
|
+
txid = send_raw_transaction(algod_client, signed)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Fee Impact
|
|
536
|
+
|
|
537
|
+
| Key Type | LogicSig Size | Dummies Needed | Extra Fee |
|
|
538
|
+
|----------|---------------|----------------|-----------|
|
|
539
|
+
| Ed25519 | 0 | 0 | 0 |
|
|
540
|
+
| Falcon-1024 | ~3035 | 3 | ~3000 uA |
|
|
541
|
+
|
|
542
|
+
The extra fee covers the dummy transactions required for post-quantum security.
|
|
543
|
+
|
|
544
|
+
## License
|
|
545
|
+
|
|
546
|
+
MIT
|
|
547
|
+
|
|
548
|
+
## Project
|
|
549
|
+
|
|
550
|
+
This SDK is part of the APlane project:
|
|
551
|
+
|
|
552
|
+
- Repository: https://github.com/aplane-algo/aplanesdk
|
|
553
|
+
- SDK path: `python`
|
|
554
|
+
|
|
555
|
+
APlane is an open-source project stewarded by the APlane Project.
|
|
556
|
+
|
|
557
|
+
See the repository [README](https://github.com/aplane-algo/aplanesdk/blob/main/README.md) for project overview and alpha-status guidance, and [DISCLAIMER.md](https://github.com/aplane-algo/aplanesdk/blob/main/DISCLAIMER.md) for risk, liability, and usage information.
|