aplanesdk
v0.37.0
Published
TypeScript SDK for signing Algorand transactions via apsigner
Maintainers
Readme
APlane TypeScript SDK
TypeScript SDK for signing Algorand transactions via apsigner.
Versioning
SDK packages are published only when the SDK changes. SDK versions track compatible APlane release tags and may skip product release numbers.
The native-Falcon SDK release line requires an APlane signer release after
v0.35.0; see docs/COMPATIBILITY.md.
Installation
npm install aplanesdk algosdkFor the optional AlgoKit adapter example/client helpers, install the current AlgoKit Utils TypeScript v10 beta. The adapter is tested against 10.0.0-beta.4:
npm install "@algorandfoundation/algokit-utils@^10.0.0-beta.4"Or with yarn/pnpm:
yarn add aplanesdk algosdk
pnpm add aplanesdk algosdkInstalling from Local Tarball
When installing from a local .tgz file, build and pack the SDK first, then install the generated tarball into your project:
# From typescript/
npm install
npm pack
# In your consuming project
npm init -y # if needed
npm install ../path/to/aplanesdk-<version>.tgz algosdkTroubleshooting
Peer dependency conflicts: If you see peer dependency errors, try:
npm install aplanesdk algosdk --legacy-peer-depsQuick Start
import { SignerClient, sendRawTransaction } from "aplanesdk";
import algosdk from "algosdk";
// Connect to signer
const client = await SignerClient.fromEnv();
// Build transaction with algosdk
const algodClient = new algosdk.Algodv2("", "https://testnet-api.4160.nodely.dev", "");
const params = await algodClient.getTransactionParams().do();
const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
sender: "SENDER_ADDRESS",
receiver: "RECEIVER_ADDRESS",
amount: 1000000, // 1 ALGO
suggestedParams: params,
});
// Sign via apsigner (waits for operator approval)
const signed = await client.signTransaction(txn);
// Submit to network (signed is ready to use, no processing needed)
const txid = await sendRawTransaction(algodClient, signed);
console.log(`Submitted: ${txid}`);Connection Methods
All SDK connections use the configured SSH-backed signer path. Direct local HTTP connection is not a supported SDK mode.
Remote Connection via SSH
Connect to apsigner on a remote machine through an SSH tunnel with 2FA:
const client = await SignerClient.connectSsh(
"signer.example.com",
"your-token", // used for both SSH auth and HTTP API
"~/aplane/apclient/.ssh/id_ed25519",
{
sshPort: 1127, // default: 1127
signerPort: 11270, // default: 11270
timeout: 30000, // optional explicit shorter request timeout
knownHostsPath: "~/aplane/apclient/.ssh/known_hosts",
}
);Note: SSH verifies the enrolled public key, then performs a programmatic
mutual proof of the token bound to the fixed username, accepted host key, and fresh nonces. The
SSH uses the fixed non-secret username aplane; the bearer token is never sent
as SSH metadata. Remember to close when done:
await client.close();Environment-Based Connection
Load configuration from a data directory. The directory is required — pass
dataDir or set the APCLIENT_DATA environment variable:
// Set environment variable
// export APCLIENT_DATA=~/aplane/apclient
const client = await SignerClient.fromEnv();
// Or pass directly
const client = await SignerClient.fromEnv({ dataDir: "~/aplane/apclient" });Data directory structure (installer default: ~/aplane/apclient):
<data_dir>/
config.yaml # Non-routing client settings
endpoints.yaml # Signer and sentry routing
aplane.token # Authentication token
.ssh/
id_ed25519 # SSH key
known_hosts # Trusted signer host keysExample endpoints.yaml (remote via SSH):
schema_version: 2
default: primary
endpoints:
primary:
role: signer
url: ssh://signer.example.com:1127
signer_port: 11270
identity_file: .ssh/id_ed25519
known_hosts_path: .ssh/known_hostsPass trustOnFirstUse: true to SignerClient.fromEnv only when the call may
trust and save an unknown host key. Use endpoint: "sentry.qa" to select a
named endpoint.
Authentication
The token is the contents of the aplane.token file from your apsigner data directory.
import { loadToken } from "aplanesdk";
// Load from file
const token = loadToken("~/aplane/apclient/aplane.token");
// Or from environment
const token = process.env.APSIGNER_TOKEN;API Reference
SignerClient
health(): Promise<boolean>
Check if signer is reachable.
if (await client.health()) {
console.log("Signer is online");
}getStatus(): Promise<StatusResponse>
Fetch authenticated signer status. This works while the signer is locked.
const status = await client.getStatus();
console.log(status.state, status.keysetRevision, status.warnings);keysetRevision is process-local and useful for deciding when to refresh
listKeys(true); it is not durable across apsigner restarts.
approvalWaitSeconds is used by the SDK to size /sign deadlines. Display
non-empty warnings to operators; they report persistent health conditions
that require attention.
listKeys(refresh?: boolean): Promise<KeyInfo[]>
List available signing keys.
const keys = await client.listKeys();
for (const key of keys) {
console.log(`${key.address} [${key.keyType}]`);
}Returns list of KeyInfo:
address: Algorand addresskeyType: "ed25519", "aplane.falcon1024.v1", "aplane.htlc.v1", etc.logicSigResources: independent program-byte, argument-byte, and maximum-opcode-cost demand by authorization path.isGenericLsig: True if no cryptographic signature neededsigningArgs: List ofSigningArgfor LogicSigs
The SDK exposes bounded inventory and ordinary spend signing only. It does not
build, partially sign, or complete contract-admin rekey transactions; use the
APlane aprekey workflow for those operations.
Discovering required arguments for generic LogicSigs:
const keyInfo = await client.getKeyInfo(hashlockAddress);
if (keyInfo?.signingArgs) {
for (const arg of keyInfo.signingArgs) {
console.log(`${arg.name}: ${arg.type} - ${arg.description}`);
}
}signTransaction(txn, authAddress?, lsigArgs?): Promise<string>
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 authAddress)
const signed = await client.signTransaction(txn);
// Rekeyed account (different auth key)
const signed = await client.signTransaction(txn, "SIGNER_KEY_ADDRESS");
// Generic LogicSig with runtime args (e.g., HTLC)
const signed = await client.signTransaction(
txn,
"HASHLOCK_ADDRESS",
{ preimage: new Uint8Array([/* secret value */]) }
);
// Submit directly (no processing needed)
const txid = await sendRawTransaction(algodClient, signed);signTransactions(txns, authAddresses?, lsigArgsMap?): Promise<string>
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 assignGroupId)
const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({...});
const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({...});
// Sign group (server handles grouping and dummies)
const signed = await client.signTransactions([txn1, txn2]);
// Submit directly (no processing needed)
const response = await algodClient.sendRawTransaction(Buffer.from(signed, "base64")).do();signTransactionsList(txns, authAddresses?, lsigArgsMap?): Promise<string[]>
Like signTransactions() but returns individual base64-encoded transactions instead of concatenated. Useful when you need to inspect transactions individually.
const signedList = await client.signTransactionsList([txn1, txn2]);
// signedList is string[], each element is a base64-encoded signed transactionsignRequests(requests, options?): Promise<GroupSignResponse>
Send one or more raw /sign request entries. Use this when an integration
already owns transaction encoding and wants APlane's native response shape.
const response = await client.signRequests(
[{
txn_bytes_hex: "5458...",
auth_address: "SIGNER_KEY_ADDRESS",
txn_sender: "SENDER_ADDRESS", // advisory display hint only
}],
{ requestId: "app-owned-request-id" },
);AlgoKit Utils Adapter
For AlgoKit Utils TypeScript v10 beta transaction signing, use the adapter
account. It is tested against 10.0.0-beta.4, connects AlgoKit clients to
APlane's transaction signing functions, and presents the addr +
signer(txnGroup, indexesToSign) shape.
The minimal repository example is examples/algokit_self_send.ts. From a
checkout with dependencies installed and the SDK built:
cd ~/aplanesdk/typescript
npm install
npm install --no-save "@algorandfoundation/algokit-utils@^10.0.0-beta.4"
npm run build
export APCLIENT_DATA=~/aplane/apclient
export APLANE_ADDRESS=SENDER_ADDRESS
node --import tsx examples/algokit_self_send.tsThe example builds a transaction with AlgoKit, signs it through the APlane adapter, then submits the signed blobs with AlgoKit's algod client:
import { AlgorandClient, microAlgo } from "@algorandfoundation/algokit-utils";
import { SignerClient, createApsignerAccount } from "aplanesdk";
const sender = "SENDER_ADDRESS";
const algorand = AlgorandClient.testNet();
const signer = await SignerClient.fromEnv();
const info = await algorand.account.getInformation(sender);
const account = createApsignerAccount({
client: signer,
address: sender,
authAddress: info.authAddr?.toString() ?? sender,
});
const txn = await algorand.createTransaction.payment({
sender,
signer: account,
receiver: sender,
amount: microAlgo(0),
validityWindow: 1000,
});
const signed = await account.signer([txn], [0]);
const txId = (await algorand.client.algod.sendRawTransaction(signed)).txId;Use createTransaction.* when APlane must own final signing and any
APlane-managed group expansion. algorand.send.* owns the composer send path
and signs inside that path.
Signing calls discover /status.approval_wait_seconds and use that value
plus 30 seconds of slack for the request timeout. If discovery fails or an older
signer omits the field, signing falls back to 6 minutes. An explicit shorter
timeout still wins; SDK /sign calls include a request_id and send a
best-effort /sign/cancel when the HTTP request times out or disconnects.
High-level signing methods accept requestId and signal options for
applications that need user-initiated cancellation.
cancelSignRequest(requestId): Promise<CancelSignResponse>
Ask apsigner to cancel a live synchronous /sign request by request ID.
Successful responses are idempotent for client behavior and return state
"canceled" or "not_found".
TypeScript high-level signing generates a request ID by default. Interactive
applications can pass an application-owned ID and an AbortSignal; aborting the
signal aborts the HTTP request and sends best-effort /sign/cancel with the
same ID:
const controller = new AbortController();
const requestId = "wallet-ui-approval-123";
const signed = await client.signTransaction(txn, undefined, undefined, {
requestId,
signal: controller.signal,
});
// elsewhere, if the user aborts while approval is pending:
controller.abort();Supported Key Types
| Key Type | Description | Notes |
|----------|-------------|-------|
| ed25519 | Native Algorand keys | Standard signing |
| aplane.falcon1024.v1 | Post-quantum LogicSig | Signature in LogicSig.Args[0] |
| aplane.ed25519.v1 | Ed25519 DSA LogicSig | Library-visible plain DSA account |
| aplane.witness-falcon1024.v1 | Witness key | Sentry-custodied policy signature key; not a spending account |
| aplane.falcon1024-sentry1024.v1 | Guarded account | Requires user and sentry component signatures |
| aplane.corridor.v1 | Bounded Corridor account | bounded1 contract; bounded-sentry1 spend flow |
| aplane.falcon1024-allowlist.v1 | Bounded allowlist | Inline allowlist; bounded1 signing flow |
| aplane.falcon1024-allowlist.v2 | Bounded allowlist | Merkle allowlist; bounded1 signing flow |
| aplane.falcon1024-timelock.v1 | Bounded timelock | Round-gated bounded1 signing flow |
| aplane.falcon1024-allowlist-alock.v1 | Rekey-locked bounded allowlist | Ordinary spending uses bounded1; admin rekey is outside SDK scope |
| aplane.htlc.v1 | Hash-locked funds | Requires preimage arg (check signingArgs) |
The server assembles the complete signed transaction - the SDK returns a base64 string ready for submission.
Sentry And Guarded Accounts
Witness keys enrolled as sentries are public policy-signature selectors, not Algorand spending accounts. Do not use them as senders, receivers, auth addresses, or rekey targets. Guarded account keys must be signed through the guarded flow.
Low-level endpoint wrappers are available:
const userPart = await userClient.requestComponents({
group_bytes_hex: ["5458..."],
targets: [{
target_index: 0,
kind: "user",
auth_address: "GUARDED_ACCOUNT_ADDRESS",
}],
});
const sentryPart = await sentryClient.requestComponents({
group_bytes_hex: ["5458..."],
targets: [{
target_index: 0,
kind: "sentry",
component_key: "SENTRY_COMPONENT_SELECTOR",
}],
});
const assembled = await userClient.requestAssemble({
group_bytes_hex: ["5458..."],
targets: [{
target_index: 0,
kind: "guarded",
auth_address: "GUARDED_ACCOUNT_ADDRESS",
user_signature: userPart.components[0].signature!,
sentry_signature: sentryPart.components[0].signature!,
}],
});For the common explicit two-client flow, use signGuardedGroup. The direct
helper does not perform inventory discovery, so pass the reviewed spend-path
resource profile returned by listKeys():
const result = await signGuardedGroup({
userClient,
sentryClient,
sentryComponentKey: "SENTRY_COMPONENT_SELECTOR",
groupBytesHex: ["5458..."],
guardedTargets: [
{
targetIndex: 0,
guardedAccount: "GUARDED_ACCOUNT_ADDRESS",
logicSigResources: reviewedSpendResources,
},
],
});
const signedGroup = result.signedGroup;assembleGroup() remains the local multi-party concatenation helper; it is not
the same operation as server-side guarded assembly.
Bounded Sentry Accounts
Corridor uses the bounded contract bounded1 with the distinct
bounded-sentry1 online signing flow. The contract identifies the LogicSig
rules; the flow identifies the user-first multi-endpoint choreography. The
prepared helper detects that flow from signer inventory and routes it
automatically:
const result = await signPreparedGuardedGroup({
userClient,
sentryResolver,
preparedGroup,
});
const signedGroup = result.signedGroup;The SDK first freezes the complete canonical group through /plan; the user
signer then approves those bytes through requestComponents() with
kind: "bounded-base". Only then does the SDK request sentry signatures over
the same bytes, sign ordinary positions, and call requestAssemble(). Before
signing anything, the SDK compares the
signer-produced plan with the caller's prepared group: only reported fee
pooling and group-ID assignment are accepted, and appended positions must be
canonical budget dummies. The returned group must use canonical transaction
encoding and a group ID recomputed from the presented membership. The SDK also
verifies ordinary signed positions and every assembled transaction against the
frozen transaction bytes.
requestComponents() sends best-effort /sign/cancel when its
approval-bearing request is aborted, times out, or disconnects.
requestAssemble() does not open an approval request and is not a
cancellation handle.
The signer planner owns fee selection, authorization-resource sizing, and any reported group mutations for both guarded flows.
Applications that own orchestration can call requestComponents() and
requestAssemble() directly. Sentry authorization is spend-only in this
contract; bounded contract-admin rekeys remain an external aprekey ceremony
and are not completed by the SDK.
Error Handling
Signing Exceptions
import {
SignerError,
## Project
This SDK is part of the APlane project:
- Repository: https://github.com/aplane-algo/aplanesdk
- SDK path: `typescript`
APlane is an open-source project stewarded by the APlane Project.
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.
AuthenticationError,
SigningRejectedError,
SignerUnavailableError,
KeyNotFoundError,
} from "aplanesdk";
try {
const signed = await client.signTransaction(txn);
} catch (error) {
if (error instanceof AuthenticationError) {
console.log("Invalid token");
} else if (error instanceof SigningRejectedError) {
console.log("Operator rejected the request");
} else if (error instanceof SignerUnavailableError) {
console.log("Signer not reachable or locked");
} else if (error instanceof KeyNotFoundError) {
console.log("Key not found in signer");
} else if (error instanceof SignerError) {
console.log(`Signing failed: ${error.message}`);
}
}Submission Exceptions
sendRawTransaction() wraps verbose algod errors into clean exceptions:
import {
sendRawTransaction,
TransactionRejectedError,
LogicSigRejectedError,
InsufficientFundsError,
InvalidTransactionError,
} from "aplanesdk";
try {
const txid = await sendRawTransaction(algodClient, signed);
} catch (error) {
if (error instanceof LogicSigRejectedError) {
console.log(`LogicSig failed: ${error.reason}`); // error.txid also available
} else if (error instanceof InsufficientFundsError) {
console.log(`Not enough funds: ${error.reason}`);
} else if (error instanceof InvalidTransactionError) {
console.log(`Invalid transaction: ${error.reason}`);
} else if (error instanceof TransactionRejectedError) {
console.log(`Rejected: ${error.reason}`);
}
}Example: Complete Workflow
import { SignerClient, loadToken, SignerError, sendRawTransaction } from "aplanesdk";
import algosdk from "algosdk";
async function main() {
// Load token
const token = loadToken("~/aplane/apclient/aplane.token");
// Connect to local signer
const client = await SignerClient.fromEnv();
// List keys
const keys = await client.listKeys();
const sender = keys[0].address;
console.log(`Using: ${sender}`);
// Build transaction
const algodClient = new algosdk.Algodv2("", "https://testnet-api.4160.nodely.dev", "");
const params = await algodClient.getTransactionParams().do();
const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
sender: sender,
receiver: sender,
amount: 0,
suggestedParams: params,
});
// Sign (will wait for operator approval)
try {
const signed = await client.signTransaction(txn);
console.log("Signed!");
// Submit directly (no processing needed)
const txid = await sendRawTransaction(algodClient, signed);
console.log(`TxID: ${txid}`);
// Wait for confirmation
const result = await algosdk.waitForConfirmation(algodClient, txid, 4);
console.log(`Confirmed in round ${result.confirmedRound}`);
} catch (error) {
if (error instanceof SignerError) {
console.log(`Failed: ${error.message}`);
} else {
throw error;
}
}
}
main().catch(console.error);LogicSig Resource Planning
The signer plans LogicSig program bytes, argument bytes, and opcode cost as independent consensus resources. Under v42, excess program bytes are paid by the group fee; dummies are added only when argument or opcode capacity requires them.
How It Works (Server-Side)
- Server selects the authorization-path resource profile.
- Server solves argument and opcode capacity, adding canonical dummies only when required.
- Server adds the v42 program-byte fee contribution.
- Server computes the final group ID and signs all transactions.
- SDK returns the complete signed group ready for submission.
Example: Falcon-1024 Key
const params = await algodClient.getTransactionParams().do();
const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
sender: falconAddr,
receiver: receiverAddr,
amount: 1000000,
suggestedParams: params,
});
// Server automatically adds dummies - just sign and submit
const signed = await client.signTransaction(txn);
const txid = await sendRawTransaction(algodClient, 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
MIT
Project
This SDK is part of the APlane project:
- Repository: https://github.com/aplane-algo/aplanesdk
- SDK path:
typescript
APlane is an open-source project stewarded by the APlane Project.
See the repository README for project overview and alpha-status guidance, and DISCLAIMER.md for risk, liability, and usage information.
