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:
- JavaScript
- Kotlin
- Java
npm i @attocash/commons-js
implementation("cash.atto:commons-node-remote:$commonsVersion")
implementation("cash.atto:commons-worker-remote:$commonsVersion")
<!-- Maven -->
<dependency>
<groupId>cash.atto</groupId>
<artifactId>commons-node-remote-jvm</artifactId>
<version>${commonsVersion}</version>
</dependency>
<dependency>
<groupId>cash.atto</groupId>
<artifactId>commons-worker-remote-jvm</artifactId>
<version>${commonsVersion}</version>
</dependency>
// Gradle (Groovy DSL)
implementation "cash.atto:commons-node-remote:${commonsVersion}"
implementation "cash.atto:commons-worker-remote:${commonsVersion}"
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).
- JavaScript
- Kotlin
- Java
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));
})();
import cash.atto.commons.*
// Generate a mnemonic and derive keys via seed/index
val mnemonic = AttoMnemonic.generate()
println(mnemonic.words.joinToString(" "))
val seed = mnemonic.toSeed()
val index0 = 0u.toAttoIndex()
val privateKey0 = seed.toPrivateKey(index0)
println("Derived Private Key (hex): ${privateKey0.value.toHex()}")
// Derive the public key if needed
val publicKey0 = privateKey0.toSigner().publicKey
println("Public Key (hex): ${publicKey0.value.toHex()}")
import cash.atto.commons.*;
import static cash.atto.commons.AttoKeyIndexes.toAttoIndex;
// Generate a mnemonic and derive keys via seed/index
AttoMnemonic mnemonic = AttoMnemonic.generate();
System.out.println(String.join(" ", mnemonic.getWords()));
AttoSeed seed = AttoSeeds.toSeedBlocking(mnemonic);
AttoKeyIndex index0 = toAttoIndex(0);
AttoPrivateKey privateKey0 = AttoPrivateKeys.toPrivateKey(seed, index0);
System.out.println("Derived Private Key (hex): " + AttoHex.toHex(privateKey0.getValue()));
// Derive the public key if needed
AttoPublicKey publicKey0 = AttoPublicKeys.toPublicKey(privateKey0);
System.out.println("Public Key (hex): " + AttoHex.toHex(publicKey0.getValue()));
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.
- JavaScript
- Kotlin
- Java
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());
import cash.atto.commons.*
// Use the key from the previous section
val publicKey = privateKey0.toSigner().publicKey
val address = AttoAddress(AttoAlgorithm.V1, publicKey)
println("Address: $address")
import cash.atto.commons.*;
// Use the key from the previous section
AttoPublicKey publicKey = AttoPublicKeys.toPublicKey(privateKey0);
AttoAddress address = new AttoAddress(AttoAlgorithm.V1, publicKey);
System.out.println("Address: " + address);
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:
- JavaScript
- Kotlin
- Java
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();
import cash.atto.commons.*
import kotlin.time.Duration.Companion.seconds
// Remote clients (v6)
val client = AttoNodeClient.remote("https://my-historical-node.mydomain.cash")
val worker = AttoWorker.remote("https://my-work-server.mydomain.cash").retry(1.seconds).cached()
import cash.atto.commons.node.*;
import cash.atto.commons.worker.*;
// Configure endpoints (using mainnet infra in this example)
AttoNodeClientAsync nodeClient = new AttoNodeClientAsyncBuilder("https://my-historical-node.mydomain.cash").build();
AttoWorkerAsync 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.
- JavaScript
- Kotlin
- Java
// 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());
}
val addressesToMonitor = listOf(hotWalletAddress, anotherAddress)
val accounts = client.account(addressesToMonitor)
// Create a map from address to account info
val accountMap = accounts.associateBy { it.address }.toMutableMap()
// Example: print current balance and height for the hot wallet
accountMap[hotWalletAddress]?.let { account ->
println("Hot Wallet Balance: ${account.balance}")
println("Hot Wallet Height: ${account.height}")
}
import cash.atto.commons.*;
import cash.atto.commons.node.*;
import java.util.*;
// Prepare a list of addresses to monitor (in this example, our hot wallet and another account)
List<AttoAddress> addressesToMonitor = Arrays.asList(hotWalletAddress, anotherAddress);
List<AttoAccount> accountsInfo = nodeClient.account(addressesToMonitor).get();
// Create a map of address -> account info for easy reference
Map<AttoAddress, AttoAccount> accountMap = new HashMap<>();
for (AttoAccount acc : accountsInfo) {
accountMap.put(acc.getAddress(), acc);
}
// Example: print the current balance and height of the hot wallet
AttoAccount hotWalletInfo = accountMap.get(hotWalletAddress);
if (hotWalletInfo != null) {
System.out.println("Hot Wallet Balance: " + hotWalletInfo.getBalance());
System.out.println("Hot Wallet Height: " + hotWalletInfo.getHeight());
}
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.
- JavaScript
- Kotlin
- Java
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());
import cash.atto.commons.*
import kotlin.time.Duration.Companion.seconds
// Build account monitor and wallet
val accountMonitor = client.createAccountMonitor()
val wallet = AttoWallet.create(client, worker, seed)
// Start auto-receiver with a default representative for opens
val receiverJob = wallet.startAutoReceiver(accountMonitor) {
AttoAddress(AttoAlgorithm.V1, AttoPublicKey(ByteArray(32)))
}
// Open accounts you want to manage via the wallet
val genesisIndex = 0u.toAttoIndex()
wallet.openAccount(genesisIndex)
import cash.atto.commons.*;
import cash.atto.commons.node.monitor.*;
import cash.atto.commons.wallet.*;
import cash.atto.commons.worker.*;
import java.util.concurrent.*;
// Build account monitor and wallet (assumes nodeClient, worker, and seed from previous sections)
AttoAccountMonitorAsync accountMonitor = new AttoAccountMonitorAsyncBuilder(nodeClient).build();
// Default representative provider for opens
java.util.function.Supplier<AttoAddress> defaultRepresentativeProvider = () -> {
byte[] dummyBytes = new byte[32];
return new AttoAddress(AttoAlgorithm.V1, new AttoPublicKey(dummyBytes));
};
AttoWalletAsync wallet = new AttoWalletAsyncBuilder(nodeClient, worker)
.signerProvider(seed)
.enableAutoReceiver(
accountMonitor,
AttoAmount.from(AttoUnit.ATTO, "1"), // minimum receivable to auto-receive
java.time.Duration.ofSeconds(10), // retry interval
defaultRepresentativeProvider // representative for opens
)
.build();
// Open account index 0 so it is tracked by the wallet
AttoKeyIndex index0 = cash.atto.commons.AttoKeyIndexes.toAttoIndex(0);
wallet.openAccount(index0).get();
// Show its address
AttoAddress address0 = wallet.getAddress(index0).get();
System.out.println("Account[0] address: " + address0);
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
existingAccountobject). - 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).
- JavaScript
- Kotlin
- Java
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}`);
import cash.atto.commons.*
// Using the wallet created earlier
val senderIndex = 0u.toAttoIndex()
val recipientIndex = 2u.toAttoIndex()
val amount = AttoAmount.from(AttoUnit.ATTO, "100.1")
val recipientAddress = wallet.getAddress(recipientIndex)
val sendTx = wallet.send(senderIndex, recipientAddress, amount)
println("Sent $amount from index $senderIndex to $recipientAddress. Tx hash: ${sendTx.hash}")
import cash.atto.commons.*;
import static cash.atto.commons.AttoKeyIndexes.toAttoIndex;
// Using the wallet created earlier
AttoKeyIndex senderIndex = toAttoIndex(0);
AttoKeyIndex recipientIndex = toAttoIndex(2);
AttoAmount amount = AttoAmount.from(AttoUnit.ATTO, "100.1");
AttoAddress recipientAddress = wallet.getAddress(recipientIndex).get();
AttoTransaction sendTx = wallet.send(senderIndex, recipientAddress, amount, null).get();
System.out.println("Sent " + amount + " from index " + senderIndex + " to " + recipientAddress + ". Tx hash: " + sendTx.getHash());
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).
- JavaScript
- Kotlin
- Java
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); }
);
import cash.atto.commons.*
import cash.atto.commons.node.monitor.*
import kotlinx.coroutines.flow.first
// Build monitors from the account monitor (assumes you already have `client`)
val accountMonitor = client.createAccountMonitor()
val transactionMonitor = accountMonitor.toTransactionMonitor { AttoHeight.MIN }
val accountEntryMonitor = accountMonitor.toAccountEntryMonitor { AttoHeight.MIN }
// Example: consume one item from each stream and acknowledge it
val transactionMessage = transactionMonitor.stream().first()
transactionMessage.acknowledge()
println("Transaction: ${transactionMessage.value.hash}")
val accountEntryMessage = accountEntryMonitor.stream().first()
accountEntryMessage.acknowledge()
println("Account entry: ${accountEntryMessage.value.hash}")
import cash.atto.commons.*;
import cash.atto.commons.node.*;
import cash.atto.commons.node.monitor.*;
import java.util.concurrent.*;
// Build monitors
AttoAccountMonitorAsync accountMonitor = new AttoAccountMonitorAsyncBuilder(nodeClient).build();
// Start streaming from height 1 (MIN) for everyone in this example
AttoTransactionMonitorAsync transactionMonitor = new AttoTransactionMonitorAsyncBuilder(nodeClient, accountMonitor)
.heightProvider(address -> CompletableFuture.completedFuture(AttoHeight.MIN))
.build();
AttoAccountEntryMonitorAsync accountEntryMonitor = new AttoAccountEntryMonitorAsyncBuilder(nodeClient, accountMonitor)
.heightProvider(address -> CompletableFuture.completedFuture(AttoHeight.MIN))
.build();
// Register listeners
AttoJob txJob = transactionMonitor.onTransaction(
tx -> System.out.println("Transaction event: " + tx.getHash()),
err -> { if (err != null) System.out.println("Transaction monitor error: " + err.getMessage()); }
);
AttoJob entryJob = accountEntryMonitor.onAccountEntry(
entry -> System.out.println("Account entry event: " + entry.getHash()),
err -> { if (err != null) System.out.println("Account entry monitor error: " + err.getMessage()); }
);
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.
