aplanesdk

中文文档
0.47.0发布于 2026/11/03MITTypeScriptESM + CJS构建来源已验证

TypeScript SDK for signing Algorand transactions via apsigner

原始使用文档
文档目录25 节
  1. Versioning
  2. Installation
  3. Installing from Local Tarball
  4. Troubleshooting
  5. Quick Start
  6. Connection Methods
  7. Remote Connection via SSH
  8. Environment-Based Connection
  9. Authentication
  10. API Reference
  11. SignerClient
  12. AlgoKit Utils Adapter
  13. Supported Key Types
  14. Cosigner And Guarded Accounts
  15. Bounded Cosigner Accounts
  16. Error Handling
  17. Signing Exceptions
  18. Submission Exceptions
  19. Example: Complete Workflow
  20. LogicSig Resource Planning
  21. How It Works (Server-Side)
  22. Example: Falcon-1024 Key
  23. Fee Impact
  24. License
  25. Project

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 algosdk

For 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 algosdk

Installing 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 algosdk

Troubleshooting

Peer dependency conflicts: If you see peer dependency errors, try:

npm install aplanesdk algosdk --legacy-peer-deps

Quick 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, authenticated by the enrolled client key:

const client = await SignerClient.connectSsh(
  "signer.example.com",
  "~/aplane/apclient/.ssh/id_ed25519",
  {
    sshPort: 1127,           // default: 1127
    timeout: 30000,          // optional explicit shorter request timeout
    sshSetupTimeout: 60000,  // TCP dial + SSH authentication only
    knownHostsPath: "~/aplane/apclient/.ssh/known_hosts",
  }
);

SSH setup uses its own 60-second deadline, independent of HTTP request and operator approval timeouts. Pass signal to cancel setup. Enrollment offers the same controls as setupTimeout and signal.

Note: SSH uses the fixed non-secret username aplane and public-key authentication only; the enrolled key is the client's only credential. A key the signer has not enrolled fails the handshake. 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 cosigner routing
  .ssh/
    id_ed25519         # SSH key: the client's credential
    known_hosts        # Trusted signer host keys

Example endpoints.yaml (remote via SSH):

schema_version: 2
default: primary
endpoints:
  primary:
    role: signer
    url: ssh://signer.example.com:1127
    identity_file: .ssh/id_ed25519
    known_hosts_path: .ssh/known_hosts

Pass trustOnFirstUse: true to SignerClient.fromEnv only when the call may trust and save an unknown host key. Use endpoint: "cosigner.qa" to select a named endpoint.

Authentication

The client's SSH key is its credential. A new key is enrolled once: the request is queued for the operator to approve later in apadmin (pending: true, the normal outcome) or the key is already enrolled, and nothing is stored on the client. Connect once the operator has approved:

import { requestEnrollmentFromEnv } from "aplanesdk";

const { fingerprint, pending } = await requestEnrollmentFromEnv({ label: "ci-runner" });
if (pending) console.log(`key ${fingerprint} queued; have the operator approve it in apadmin`);

requestEnrollment(host, sshKeyPath, { knownHostsPath, label }) is the explicit form. The same enrollment can be done from apshell with enroll.

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 address
  • keyType: "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 needed
  • signingArgs: List of SigningArg for 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 transaction

signRequests(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.ts

The 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 TypeDescriptionNotes
ed25519Native Algorand keysStandard signing
aplane.falcon1024.v1Post-quantum LogicSigSignature in LogicSig.Args[0]
aplane.ed25519.v1Ed25519 DSA LogicSigLibrary-visible plain DSA account
aplane.witness-falcon1024.v1Witness keyCosigner-custodied policy signature key; not a spending account
aplane.falcon1024-cosigner1024.v1Guarded accountRequires user and cosigner component signatures
aplane.corridor.v1Bounded Corridor accountbounded1 contract; bounded-cosigner1 spend flow
aplane.falcon1024-allowlist.v1Bounded allowlistInline allowlist; bounded1 signing flow
aplane.falcon1024-allowlist.v2Bounded allowlistMerkle allowlist; bounded1 signing flow
aplane.falcon1024-timelock.v1Bounded timelockRound-gated bounded1 signing flow
aplane.falcon1024-allowlist-alock.v1Rekey-locked bounded allowlistOrdinary spending uses bounded1; admin rekey is outside SDK scope
aplane.htlc.v1Hash-locked fundsRequires preimage arg (check signingArgs)

The server assembles the complete signed transaction - the SDK returns a base64 string ready for submission.

Cosigner And Guarded Accounts

Witness keys enrolled as cosigners 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 cosignerPart = await cosignerClient.requestComponents({
  group_bytes_hex: ["5458..."],
  targets: [{
    target_index: 0,
    kind: "cosigner",
    component_key: "COSIGNER_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!,
    cosigner_signature: cosignerPart.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,
  cosignerClient,
  cosignerComponentKey: "COSIGNER_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 Cosigner Accounts

Corridor uses the bounded contract bounded1 with the distinct bounded-cosigner1 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,
  cosignerResolver,
  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". It then signs ordinary positions, and only after the user side is complete does it request cosigner signatures over the same bytes 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. Cosigner 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("Connection is not authenticated by an enrolled SSH key");
  } 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, SignerError, sendRawTransaction } from "aplanesdk";
import algosdk from "algosdk";

async function main() {
  // Connect to the signer with the enrolled client key
  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)

  1. Server selects the authorization-path resource profile.
  2. Server solves argument and opcode capacity, adding canonical dummies only when required.
  3. Server adds the v42 program-byte fee contribution.
  4. Server computes the final group ID and signs all transactions.
  5. 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 TypeLogicSig SizeDummies NeededExtra Fee
Ed25519000
Falcon-1024~30353~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:

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.

版本与发布记录

aplanesdk 共发布 22 个版本,首次发布于 2026/05/26;过去 12 个月发布了 22 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.47.0latest2026/11/03今天
0.46.02026/11/021 天前
0.45.02026/10/304 天前
0.44.02026/10/2113 天前
0.43.02026/10/2014 天前
0.42.02026/10/1518 天前

关于 aplanesdk 的常见问题

aplanesdk 是什么?

通过apsigner签署Algorand事务的TypeScript SDK

如何安装 aplanesdk?

在项目目录运行 npm install aplanesdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:algosdk(^3.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

aplanesdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

aplanesdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

aplanesdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

aplanesdk 可以商用吗?

aplanesdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

aplanesdk 还在维护吗?

最新版本 0.47.0 发布于 2026/11/03(今天);至今共发布 22 个版本,其中过去 12 个月发布了 22 个;首次发布于 2026/05/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。