Python SDK for APlane transaction signing
Project description
APlane Python SDK
Python SDK for signing Algorand transactions via apsignerd.
Installation
pip install aplane
Or install from source:
cd sdk/python
pip install -e .
Quick Start
from aplane import SignerClient, send_raw_transaction
from algosdk import transaction
from algosdk.v2client import algod
# Connect to signer (reads config.yaml and token from data dir)
client = SignerClient.from_env()
# Build transaction with algosdk
algod_client = algod.AlgodClient("", "https://testnet-api.4160.nodely.dev")
params = algod_client.suggested_params()
txn = transaction.PaymentTxn(
sender="SENDER_ADDRESS",
sp=params,
receiver="RECEIVER_ADDRESS",
amt=1000000 # 1 ALGO
)
# Sign via apsignerd (waits for operator approval)
signed = client.sign_transaction(txn)
# Submit to network (signed is ready to use, no processing needed)
txid = send_raw_transaction(algod_client, signed)
print(f"Submitted: {txid}")
Connection Methods
All SDK connections use the configured SSH-backed signer path. Direct local HTTP connection is not a supported SDK mode.
Environment-Based Connection (Recommended)
Load configuration from a data directory:
# Set environment variable
# export APCLIENT_DATA=~/apclient
client = SignerClient.from_env()
# Or pass directly
client = SignerClient.from_env(data_dir="~/apclient")
Data directory structure:
~/apclient/
config.yaml # Connection settings
aplane.token # Authentication token
.ssh/
id_ed25519 # SSH private key for authentication
known_hosts # Trusted server host keys
Example config.yaml:
signer_port: 11270
ssh:
host: localhost # Change to remote host if signer is on another machine
port: 1127
identity_file: .ssh/id_ed25519
known_hosts_path: .ssh/known_hosts
Direct SSH Connection
Connect explicitly via SSH tunnel with 2FA:
client = SignerClient.connect_ssh(
host="signer.example.com",
token="your-token", # used for both SSH auth and HTTP API
ssh_key_path="~/.ssh/id_ed25519",
ssh_port=1127, # default: 1127
signer_port=11270, # default: 11270
timeout=90 # seconds, default
)
Note: SSH uses 2FA (token + public key). The token is passed as the SSH
username. Keys are enrolled via the request-token operator-approved flow.
The SSH tunnel is established automatically. Remember to close when done:
client.close()
Or use as a context manager:
with SignerClient.connect_ssh(host="...", token="...", ssh_key_path="...") as client:
signed = client.sign_transaction(txn)
# Tunnel closed automatically
Authentication
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.
If your token was provisioned separately (e.g. copied by the operator), you can load it explicitly:
from aplane import load_token
token = load_token("/path/to/apclient/aplane.token")
API Reference
SignerClient
health() -> bool
Check if signer is reachable.
if client.health():
print("Signer is online")
list_keys() -> List[KeyInfo]
List available signing keys.
keys = client.list_keys()
for key in keys:
print(f"{key.address} [{key.key_type}]")
Returns list of KeyInfo:
address: Algorand addresskey_type: "ed25519", "falcon1024-v1", "timelock-v1", etc.lsig_size: LogicSig size (for budget calculation)is_generic_lsig: True if no cryptographic signature neededruntime_args: List ofRuntimeArgfor generic LogicSigs (name, arg_type, description)
Discovering required arguments for generic LogicSigs:
key_info = client.get_key_info(hashlock_address)
if key_info.runtime_args:
for arg in key_info.runtime_args:
print(f"{arg.name}: {arg.arg_type} - {arg.description}")
sign_transaction(txn, auth_address=None, lsig_args=None) -> str
Sign a single transaction. Returns a base64-encoded string ready for submission.
The server automatically handles fee pooling for large LogicSigs (e.g., Falcon-1024) by adding dummy transactions as needed.
# Basic signing (uses txn.sender as auth_address)
signed = client.sign_transaction(txn)
# Rekeyed account (different auth key)
signed = client.sign_transaction(txn, auth_address="SIGNER_KEY_ADDRESS")
# Generic LogicSig with runtime args (e.g., HTLC)
signed = client.sign_transaction(
txn,
auth_address="HASHLOCK_ADDRESS",
lsig_args={"preimage": b"secret_value"}
)
# Submit directly (no processing needed)
txid = send_raw_transaction(algod_client, signed)
sign_transactions(txns, auth_addresses=None, lsig_args_map=None) -> str
Sign multiple transactions as a group. Returns a base64-encoded string of concatenated signed transactions, ready for submission.
Important: Do NOT pre-assign group IDs. The server computes the group ID after adding any required dummy transactions for large LogicSigs.
# Build transactions (do NOT call assign_group_id)
txn1 = transaction.PaymentTxn(sender=addr1, sp=params, receiver=addr2, amt=100000)
txn2 = transaction.PaymentTxn(sender=addr2, sp=params, receiver=addr1, amt=100000)
# Sign group (server handles grouping and dummies)
signed = client.sign_transactions([txn1, txn2])
# Submit directly (no processing needed)
txid = algod_client.send_raw_transaction(signed)
sign_transactions_list(txns, auth_addresses=None, lsig_args_map=None) -> List[str]
Like sign_transactions() but returns individual base64-encoded transactions instead of concatenated. Useful when you need to inspect transactions individually.
signed_list = client.sign_transactions_list([txn1, txn2])
# signed_list is List[str], each element is a base64-encoded signed transaction
close()
Close the client and SSH tunnel (if any).
client.close()
Supported Key Types
| Key Type | Description | Notes |
|---|---|---|
ed25519 |
Native Algorand keys | Standard signing |
falcon1024-v* |
Post-quantum LogicSig | Signature in LogicSig.Args[0] |
timelock-v* |
Time-locked funds | No signature, TEAL-only |
htlc-v* |
Hash-locked funds | Requires preimage arg (check runtime_args) |
The server assembles the complete signed transaction - the SDK returns a base64 string ready for submission.
Error Handling
Signing Exceptions
from aplane import (
SignerError,
AuthenticationError,
SigningRejectedError,
SignerUnavailableError,
KeyNotFoundError
)
try:
signed = client.sign_transaction(txn)
except AuthenticationError:
print("Invalid token")
except SigningRejectedError:
print("Operator rejected the request")
except SignerUnavailableError:
print("Signer not reachable or locked")
except KeyNotFoundError:
print("Key not found in signer")
except SignerError as e:
print(f"Signing failed: {e}")
Submission Exceptions
send_raw_transaction() wraps verbose algod errors into clean exceptions:
from aplane import (
send_raw_transaction,
TransactionRejectedError,
LogicSigRejectedError,
InsufficientFundsError,
InvalidTransactionError
)
try:
txid = send_raw_transaction(algod_client, signed)
except LogicSigRejectedError as e:
print(f"LogicSig failed: {e.reason}") # e.txid also available
except InsufficientFundsError as e:
print(f"Not enough funds: {e.reason}")
except InvalidTransactionError as e:
print(f"Invalid transaction: {e.reason}")
except TransactionRejectedError as e:
print(f"Rejected: {e.reason}")
Example: Complete Workflow
#!/usr/bin/env python3
from aplane import SignerClient, load_token, SignerError, send_raw_transaction
from algosdk import transaction
from algosdk.v2client import algod
def main():
# Load token
token = load_token("~/apclient/aplane.token")
# Connect via SSH (token is used as SSH username for 2FA)
with SignerClient.connect_ssh(
host="signer.example.com",
token=token,
ssh_key_path="~/.ssh/id_ed25519"
) as client:
# List keys
keys = client.list_keys()
sender = keys[0].address
print(f"Using: {sender}")
# Build transaction
algod_client = algod.AlgodClient("", "https://testnet-api.4160.nodely.dev")
params = algod_client.suggested_params()
txn = transaction.PaymentTxn(
sender=sender,
sp=params,
receiver=sender,
amt=0
)
# Sign (will wait for operator approval)
try:
signed = client.sign_transaction(txn)
print("Signed!")
# Submit directly (no processing needed)
txid = send_raw_transaction(algod_client, signed)
print(f"TxID: {txid}")
# Wait for confirmation
result = transaction.wait_for_confirmation(algod_client, txid, 4)
print(f"Confirmed in round {result['confirmed-round']}")
except SignerError as e:
print(f"Failed: {e}")
if __name__ == "__main__":
main()
Fee Pooling (Large LogicSigs)
Algorand limits LogicSig size to 1000 bytes per transaction. Large signatures like Falcon-1024 (~3000 bytes) exceed this limit.
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.
How It Works (Server-Side)
- Server detects key's
lsig_sizeexceeds available budget - Server calculates dummies needed:
ceil(total_lsig_bytes / 1000) - num_txns - Server creates dummy self-payment transactions (0 amount, min fee)
- Server distributes dummy fees across LogicSig transactions in the group
- Server computes group ID and signs all transactions
- SDK returns concatenated signed group ready for submission
Example: Falcon-1024 Key
# Falcon-1024 has lsig_size ~3035 bytes, needs 3 dummies
# Total group: 1 main + 3 dummies = 4 transactions
# Pool budget: 4 x 1000 = 4000 bytes (enough for 3035)
params = algod_client.suggested_params()
txn = transaction.PaymentTxn(sender=falcon_addr, sp=params, receiver=receiver, amt=1000000)
# Server automatically adds dummies - just sign and submit
signed = client.sign_transaction(txn)
txid = send_raw_transaction(algod_client, signed)
Fee Impact
| Key Type | LogicSig Size | Dummies Needed | Extra Fee |
|---|---|---|---|
| Ed25519 | 0 | 0 | 0 |
| Falcon-1024 | ~3035 | 3 | ~3000 uA |
The extra fee covers the dummy transactions required for post-quantum security.
License
AGPL-3.0-or-later
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aplane-0.2.1.tar.gz.
File metadata
- Download URL: aplane-0.2.1.tar.gz
- Upload date:
- Size: 38.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38e6ac300e06eca37a3ad5e0dd5c45103388a0c987b6eec5fa06db286a39d9ea
|
|
| MD5 |
3a11950d91edc457b3c96bf900177494
|
|
| BLAKE2b-256 |
3473f7063073292a45061c11d3a4127d5c07fb8ef726e23a962ffc8081d61a9e
|
File details
Details for the file aplane-0.2.1-py3-none-any.whl.
File metadata
- Download URL: aplane-0.2.1-py3-none-any.whl
- Upload date:
- Size: 32.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abd43901f70deca2e8134ca0eb807a108fa7d1fe69e828ca89e6bcf068e7b23a
|
|
| MD5 |
ca67fc104218d62a1081c36480e6cde7
|
|
| BLAKE2b-256 |
c6b684cd8e0e1598f512bbfa1631adea02275e48e310bb9b9f8b3eae18ee2a40
|