Skip to main content

Offline Signing with Atto Commons

In this tutorial, we demonstrate how to integrate with the Atto network at a low level using the Atto Commons libraries (available for JavaScript/TypeScript, Kotlin, and Java). This approach allows you to generate and manage keys, construct transactions offline, and interact directly with an Atto node – all without relying on the Wallet Server component. This is ideal for advanced integrators (such as exchanges or custodians) who require full control over key management and offline signing.

Note: Atto uses Ed25519 elliptic curve cryptography for its account keys and signatures. This means you can leverage third-party tools or HSMs for key derivation and signing if desired. In this guide, however, we will use the Atto Commons library for all key and transaction operations for consistency and simplicity.

Before proceeding, ensure you have access to the following components in your environment:

  • An Atto Historical Node (with its REST API endpoint) connected to the main network.
  • An Atto Work Server for generating the Proof-of-Work required by transactions.

Typically, integrators will run their own historical node and work server. For testing, you may use Atto's public endpoints (available upon request). In the code examples below, we will use the main network configuration.

Installation

Make sure the Atto Commons libraries are available in your project:

  npm i @attocash/commons-js

Key Generation and Derivation

The first step is to generate a private key for your Atto account. The Atto Commons library provides utilities for secure key generation. In JavaScript, you can use AttoPrivateKey.generate() to create a new random key. In Kotlin, a similar AttoPrivateKey.generate() function (via the companion object) is available. You can also import an existing private key from a hex string if you have one (for example, from a mnemonic or another source).

import {
AttoMnemonic,
toSeedAsync,
toPrivateKey,
toPublicKey,
toAttoIndex,
toHex,
} from '@attocash/commons-js';

// Generate a new mnemonic and derive a private key (index 0)
(async () => {
const mnemonic = AttoMnemonic.generate();
console.log('Mnemonic:', mnemonic.words.join(' '));

const seed = await toSeedAsync(mnemonic);
const index0 = toAttoIndex(0);
const privateKey0 = toPrivateKey(seed, index0);
console.log('Derived Private Key (hex):', toHex(privateKey0.value));

// Derive the public key if needed
const publicKey0 = toPublicKey(privateKey0);
console.log('Public Key (hex):', toHex(publicKey0.value));
})();

About Key Security: The private key is a 32-byte value. Keep this safe! With Ed25519, the public key can be derived from the private key, and the Atto address can be derived from the public key. If you prefer to derive keys from a mnemonic seed, ensure you use a secure, Atto-compatible method. (Atto Commons supports seed generation as well, though it’s not shown here.)

Address Generation

Once you have a private key (and its corresponding public key), you can create an Atto Address. An Atto address is analogous to an account identifier on the network. It encodes the public key (and a checksum), along with a prefix that indicates the algorithm. Atto Commons makes address creation straightforward: you provide the algorithm ( currently AttoAlgorithm.V1 for Ed25519-based addresses) and the public key, and it returns the address object.

import { AttoAddress, AttoAlgorithm, toPublicKey } from '@attocash/commons-js';

// Assume we have a private key from the previous section (e.g., privateKey0)
const publicKey = toPublicKey(privateKey0); // derive Ed25519 public key
const address = new AttoAddress(AttoAlgorithm.V1, publicKey); // build the Atto address
console.log('Address:', address.toString());

The resulting address (when printed) will be a string starting with the atto:// prefix. This address is what you share to receive funds.

Choosing a Representative: Every Atto account can designate a representative. In most cases, you can use your own voting node as its representative or use a default Atto representative.

Setting Up Network Client and Work Server

To interact with the network, you need a client to communicate with an Atto node’s REST API, and a worker to compute Proof-of-Work (PoW). The Atto Commons library can create these for you:

import { AttoNodeClientAsyncBuilder, AttoWorkerAsyncBuilder } from '@attocash/commons-js';

// Configure endpoints (using mainnet infra in this example)
const nodeClient = new AttoNodeClientAsyncBuilder('https://my-historical-node.mydomain.cash').build();
const worker = new AttoWorkerAsyncBuilder('https://my-work-server.mydomain.cash').build();

Make sure the URLs point to your own infrastructure or trusted public endpoints. The node client will be used for querying the blockchain state (accounts, transactions) and broadcasting signed transactions. The work server will be used to compute the small PoW required for each transaction block (Atto uses a micro-hash for spam prevention instead of fees).

Reading Account State

Before crafting transactions, it’s important to retrieve the current state of your accounts (balance and height). The account height is used to sequence transactions correctly (each new block on an account increments the height).

Using the node client, you can fetch account information for one or multiple addresses via client.account([...addresses]). The result will include each account’s current balance, frontier (last block hash), and height, among other data. We can store this in a map for quick lookup.

// Prepare a list of addresses to monitor (in this example, our hot wallet and another account)
const addressesToMonitor = [hotWalletAddress, anotherAddress];
const accountsInfo = await nodeClient.account(addressesToMonitor);

// Create a map of address -> account info for easy reference
const accountMap = new Map(accountsInfo.map(acc => [acc.address.toString(), acc]));

// Example: print the current balance and height of the hot wallet
const hotWalletInfo = accountMap.get(hotWalletAddress.toString());
if (hotWalletInfo) {
console.log("Hot Wallet Balance:", hotWalletInfo.balance.toString());
console.log("Hot Wallet Height:", hotWalletInfo.height.toString());
}

If an address has never received any funds (and thus has no account entry on the ledger yet), the client.account call will return no entry for it. In our example, anotherAddress might be a brand new address (no funds yet), so it would not appear in accountsInfo. We handle this by checking existingAccount == null later when receiving funds.

Receiving Incoming Transactions (Opening / Receiving)

One crucial aspect of integration is handling incoming funds to your addresses. In Atto, incoming funds arrive as receivable blocks (similar to pending transactions). To actually claim the funds, the receiving account must publish a corresponding block: an open block if it’s the account’s first transaction, or a receive block for subsequent ones.

The Commons wallet provides an auto-receiver built on top of the account monitor. It subscribes to receivables for you and handles opening/receiving, signing, PoW, publishing, and state updates automatically.

Below, we enable the wallet’s auto‑receiver. It listens for receivables emitted by the account monitor and, when detected, automatically constructs the correct block (open for the first receive or receive for subsequent ones), timestamps it, requests PoW from the worker, signs, publishes, and updates the account state. You control the minimum amount to auto‑receive, the retry cadence, and the default representative used for new accounts.

import {
AttoAccountMonitorAsyncBuilder,
AttoWalletAsyncBuilder,
AttoAmount,
AttoUnit,
AttoAddress,
AttoAlgorithm,
AttoPublicKey,
toAttoIndex,
} from '@attocash/commons-js';

// Assumes you already created `nodeClient`, `worker`, and have a `seed` from the key section.
const accountMonitor = new AttoAccountMonitorAsyncBuilder(nodeClient).build();

// Optional: default representative provider (used when opening an account)
const defaultRepresentativeProvider = () => {
const dummyBytes = new Uint8Array(32);
return new AttoAddress(AttoAlgorithm.V1, new AttoPublicKey(dummyBytes));
};

// Build a wallet with auto-receiver enabled. It will automatically open/receive
// for any receivables found for accounts derived from your seed.
const wallet = new AttoWalletAsyncBuilder(nodeClient, worker)
.signerProviderSeed(seed)
.enableAutoReceiver(
accountMonitor,
AttoAmount.from(AttoUnit.ATTO, '1'), // minimum receivable to auto-receive
10, // retry interval (seconds)
defaultRepresentativeProvider // representative provider for opens
)
.build();

// Example: open first account (index 0) so it’s tracked by the wallet
const index0 = toAttoIndex(0);
await wallet.openAccount(index0).asPromise();

// You can derive/show its address
const address0 = await wallet.getAddress(index0).asPromise();
console.log('Account[0] address:', address0.toString());

The wallet handles block timestamps and PoW internally when auto-receiving. You provide a default representative to use for new accounts (opens).

What’s Happening: When someone sends funds to our address, the node’s receivable stream triggers. We create an open block if this is the first time the address is receiving funds (which sets up the account with an initial representative), or a receive block if the account already exists. We then sign it and solve PoW, and publish it to incorporate the funds into our account. After publishing, the funds are confirmed in our account’s balance.

Sending a Transaction

Sending Atto (e.g., processing a withdrawal) involves creating a send block on the sender’s account. The Atto Commons wallet send will build the new send block and handle signing, PoW, and publishing for you.

Important considerations before sending:

  • The sending account must have a sufficient balance.
  • You must use the correct current height of the account (the library handles this if you pass the latest existingAccount object).
  • Each send reduces the account’s balance and increases its height.

Below we craft a send from our hotWalletAddress to anotherAddress for an example amount of 100.1 ATTO. We assume accountMap has been kept up-to-date with the latest state of hotWalletAddress (including any receives done above).

import { AttoAmount, AttoUnit } from '@attocash/commons-js';

// Define the amount to send (100.1 ATTO in this example)
const amount = AttoAmount.from(AttoUnit.ATTO, '100.1');

// Assume you built `wallet` earlier and opened index 0
const index0 = toAttoIndex(0);

// Derive the recipient address from account index 2 via the wallet (mirrors Kotlin/Java)
const recipientIndex = toAttoIndex(2);
const recipientAddress = await wallet.getAddress(recipientIndex).asPromise();
const tx = await wallet.sendByIndex(index0, recipientAddress, amount, null).asPromise();
console.log(`Sent ${amount} from index 0 to ${recipientAddress}. Tx hash: ${tx.hash}`);

After publishing the send, the funds will be deducted from the sender’s account on the ledger, and a corresponding receivable will be waiting for the recipient (which, if it's one of our addresses and we have the receivable subscription running as above, will trigger an automatic receive).

Monitoring Account Activity (Transactions and Entries)

To facilitate real-time updates (e.g., for showing confirmations or new incoming transactions), the Atto node provides streaming endpoints. We already used receivable for incoming pending funds. There are two other useful streams for integrators:

  • Transaction Stream – streams every new block (transaction) involving the specified accounts, starting from given heights.
  • Account Entry Stream – streams every new account entry (state update) for the specified accounts, starting from given heights.

The difference between these streams is subtle but important. The transaction stream will emit on any new send or receive block for your accounts as they are confirmed on the network. The account entry stream will emit an event when an account’s state changes (for example, a new open block or any new block on the account). In practice, if you track all your addresses from their latest heights, both streams will notify you of new blocks; you can choose which to use based on the level of detail you need.

First, we build a HeightSearch object that tells the node from which height to start streaming for each account. Typically, you start at the next height after the current one (or at height 0 for unopened accounts).

import {
AttoTransactionMonitorAsyncBuilder,
AttoAccountEntryMonitorAsyncBuilder,
AttoAccountMonitorAsyncBuilder,
AttoAddress,
AttoAlgorithm,
toAttoHeight,
AttoHeight,
} from '@attocash/commons-js';

// Build monitors
const accountMonitor = new AttoAccountMonitorAsyncBuilder(nodeClient).build();

// We will start streaming from AttoHeight.MIN for all addresses.
// In local mocks you may want to skip the genesis block (start from height 2 for the genesis address).
// Provide a heightProvider callback that can decide per-address.
const transactionMonitor = new AttoTransactionMonitorAsyncBuilder(nodeClient, accountMonitor)
.heightProvider((address) => {
// example: start at height 1 for everyone
return Promise.resolve(AttoHeight.MIN);
})
.build();

const accountEntryMonitor = new AttoAccountEntryMonitorAsyncBuilder(nodeClient, accountMonitor)
.heightProvider((address) => Promise.resolve(AttoHeight.MIN))
.build();

// Register listeners
const txJob = transactionMonitor.onTransaction(
(tx) => console.log('Transaction event:', tx.hash),
(err) => { if (err) console.warn('Transaction monitor error:', err.message); }
);

const entryJob = accountEntryMonitor.onAccountEntry(
(entry) => console.log('Account entry event:', entry.hash),
(err) => { if (err) console.warn('Account entry monitor error:', err.message); }
);

This can be useful to update balances, show confirmations, or trigger internal accounting logic in real time. Remember to handle the termination or errors of these streams (for example, re-establish them if needed, or clean up on application shutdown).

Conclusion

By using the Atto Commons library for JS, Kotlin, and Java, you can fully manage the lifecycle of Atto transactions in your own code: from key creation and offline signing to publishing transactions on the network and reacting to incoming funds. This advanced integration approach gives you maximum flexibility and security (you control your private keys at all times) at the cost of a bit more implementation work compared to using a high-level Wallet Server.

Integrators should ensure they maintain the accountMap state accurately and securely store private keys. Additionally, for production use, always run your own Atto node and work server for reliability and security. With the patterns shown in this tutorial, you can build a robust integration that handles sending and receiving Atto programmatically, all while keeping sensitive operations offline.

Footer background
Ready to Experience Atto?Enjoy instant, feeless, and eco-friendly transactions with just a tap.
Copyright © 2025 Atto B.V.
X.comRedditLinkedinGithubDiscord