Skip to content
Algorand Developer Portal

Single Signature

← Back to Transactions

This example demonstrates how to create an ed25519 keypair and sign transactions using the low-level transact package APIs. Key concepts:

  • Creating a keypair using tweetnacl (ed25519 signature scheme)
  • Using generateAddressWithSigners() to derive an Algorand address from the public key
  • Understanding the relationship between ed25519 public key and Algorand address
  • Signing transactions with a raw ed25519 signer function
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example transact/09-single-sig.ts

View source on GitHub

09-single-sig.ts
/**
* Example: Single Signature
*
* This example demonstrates how to create an ed25519 keypair and sign transactions
* using the low-level transact package APIs.
*
* Key concepts:
* - Creating a keypair using tweetnacl (ed25519 signature scheme)
* - Using generateAddressWithSigners() to derive an Algorand address from the public key
* - Understanding the relationship between ed25519 public key and Algorand address
* - Signing transactions with a raw ed25519 signer function
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { AlgorandClient } from '@algorandfoundation/algokit-utils';
import {
Transaction,
TransactionType,
assignFee,
generateAddressWithSigners,
type PaymentTransactionFields,
} from '@algorandfoundation/algokit-utils/transact';
import nacl from 'tweetnacl';
import {
createAlgodClient,
formatAlgo,
getAccountBalance,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
waitForConfirmation,
} from '../shared/utils.js';
/**
* Gets a funded account from LocalNet's KMD wallet
*/
async function getLocalNetFundedAccount(algorand: AlgorandClient) {
return await algorand.account.kmd.getLocalNetDispenserAccount();
}
async function main() {
printHeader('Single Signature Example');
// Step 1: Initialize clients
printStep(1, 'Initialize Algod Client');
const algod = createAlgodClient();
const algorand = AlgorandClient.defaultLocalNet();
printInfo('Connected to LocalNet Algod');
// Step 2: Create a keypair using tweetnacl
printStep(2, 'Create Keypair Using tweetnacl');
// The keypair generation uses a cryptographically secure random number generator
const keypair = nacl.sign.keyPair();
printInfo(
`Public key (32 bytes): ${Buffer.from(keypair.publicKey).toString('hex').slice(0, 32)}...`,
);
printInfo(
`Secret key (64 bytes): ${Buffer.from(keypair.secretKey).toString('hex').slice(0, 32)}... (truncated for security)`,
);
printInfo('');
printInfo('Note: The secret key in ed25519 is 64 bytes because it contains');
printInfo(' both the 32-byte private seed AND the 32-byte public key.');
// Step 3: Derive Algorand address using generateAddressWithSigners
printStep(3, 'Derive Algorand Address with generateAddressWithSigners()');
// generateAddressWithSigners() does the following internally:
// 1. Takes the 32-byte ed25519 public key
// 2. Computes a 4-byte checksum using SHA-512/256
// 3. Concatenates: public_key (32 bytes) + checksum (4 bytes) = 36 bytes
// 4. Base32 encodes the 36 bytes to get the 58-character Algorand address
const account = generateAddressWithSigners({
ed25519Pubkey: keypair.publicKey,
rawEd25519Signer: async (bytesToSign: Uint8Array) => {
return nacl.sign.detached(bytesToSign, keypair.secretKey);
},
});
printInfo(`Ed25519 public key (hex): ${Buffer.from(keypair.publicKey).toString('hex')}`);
printInfo(`Algorand address (base32): ${account.addr.toString()}`);
printInfo('');
printInfo('The Algorand address is derived from the public key by:');
printInfo(' 1. Computing SHA-512/256 checksum of the public key');
printInfo(' 2. Appending last 4 bytes of checksum to public key');
printInfo(' 3. Base32 encoding the result (36 bytes -> 58 characters)');
// Step 4: Fund the account from dispenser
printStep(4, 'Fund Account from Dispenser');
const dispenser = await getLocalNetFundedAccount(algorand);
const fundingAmount = 2_000_000n; // 2 ALGO
const suggestedParams = await algod.suggestedParams();
const fundTx = new Transaction({
type: TransactionType.Payment,
sender: dispenser.addr,
firstValid: suggestedParams.firstValid,
lastValid: suggestedParams.lastValid,
genesisHash: suggestedParams.genesisHash,
genesisId: suggestedParams.genesisId,
payment: {
receiver: account.addr,
amount: fundingAmount,
},
});
const fundTxWithFee = assignFee(fundTx, {
feePerByte: suggestedParams.fee,
minFee: suggestedParams.minFee,
});
const signedFundTx = await dispenser.signer([fundTxWithFee], [0]);
await algod.sendRawTransaction(signedFundTx);
await waitForConfirmation(algod, fundTxWithFee.txId());
const balance = await getAccountBalance(algorand, account.addr.toString());
printInfo(`Funded account with ${formatAlgo(fundingAmount)}`);
printInfo(`Account balance: ${formatAlgo(balance.microAlgo)}`);
// Step 5: Create a payment transaction to demonstrate signing
printStep(5, 'Create Payment Transaction');
const paymentAmount = 100_000n; // 0.1 ALGO
// Use AlgorandClient helper for the receiver (this example focuses on sender signing)
const receiver = algorand.account.random();
const payParams = await algod.suggestedParams();
const paymentFields: PaymentTransactionFields = {
receiver: receiver.addr,
amount: paymentAmount,
};
const paymentTx = new Transaction({
type: TransactionType.Payment,
sender: account.addr,
firstValid: payParams.firstValid,
lastValid: payParams.lastValid,
genesisHash: payParams.genesisHash,
genesisId: payParams.genesisId,
payment: paymentFields,
});
const paymentTxWithFee = assignFee(paymentTx, {
feePerByte: payParams.fee,
minFee: payParams.minFee,
});
printInfo(`Payment amount: ${formatAlgo(paymentAmount)}`);
printInfo(`Sender: ${shortenAddress(account.addr.toString())}`);
printInfo(`Receiver: ${shortenAddress(receiver.addr.toString())}`);
printInfo(`Transaction ID: ${paymentTxWithFee.txId()}`);
// Step 6: Sign the transaction (explaining the process)
printStep(6, 'Sign Transaction with ed25519 Signature');
printInfo('Signing process:');
printInfo(' 1. Transaction is encoded to msgpack bytes');
printInfo(' 2. Bytes are prefixed with "TX" (to prevent cross-protocol attacks)');
printInfo(' 3. The prefixed bytes are signed using ed25519 with the secret key');
printInfo(' 4. Signature (64 bytes) is attached to create a SignedTransaction');
printInfo('');
// The signer function handles all of this internally
// account.signer([transaction], [indices]) signs the specified transactions
const signedTxns = await account.signer([paymentTxWithFee], [0]);
printInfo(`Signed transaction size: ${signedTxns[0].length} bytes`);
printInfo('Transaction signed successfully!');
// Step 7: Submit and verify
printStep(7, 'Submit Transaction');
await algod.sendRawTransaction(signedTxns);
printInfo('Transaction submitted to network');
const pendingInfo = await waitForConfirmation(algod, paymentTxWithFee.txId());
printInfo(`Transaction confirmed in round: ${pendingInfo.confirmedRound}`);
// Step 8: Verify balances
printStep(8, 'Verify Balances');
const senderBalanceAfter = await getAccountBalance(algorand, account.addr.toString());
let receiverBalanceAfter: bigint;
try {
const info = await getAccountBalance(algorand, receiver.addr.toString());
receiverBalanceAfter = info.microAlgo;
} catch {
receiverBalanceAfter = 0n;
}
printInfo(`Sender balance after: ${formatAlgo(senderBalanceAfter.microAlgo)}`);
printInfo(`Receiver balance after: ${formatAlgo(receiverBalanceAfter)}`);
const expectedSenderBalance = fundingAmount - paymentAmount - (paymentTxWithFee.fee ?? 0n);
if (senderBalanceAfter.microAlgo === expectedSenderBalance) {
printSuccess('Sender balance verified!');
}
if (receiverBalanceAfter === paymentAmount) {
printSuccess('Receiver received the payment!');
}
// Summary
printInfo('');
printInfo('Summary - Single Signature Key Points:');
printInfo(' - ed25519 is the signature algorithm used by Algorand');
printInfo(' - Public key (32 bytes) -> Algorand address (58 chars base32)');
printInfo(' - generateAddressWithSigners() bridges raw crypto to Algorand');
printInfo(' - The signer function signs transaction bytes with ed25519');
printInfo(' - Each transaction requires a valid signature from the sender');
printSuccess('Single signature example completed!');
}
main().catch(error => {
console.error('Error:', error);
process.exit(1);
});