4. Transaction Building
Complete guide to building and signing transactions with the Amadeus Protocol SDK.
Overview
Transactions in Amadeus Protocol are structured data that represent actions on the blockchain. They must be properly built, signed, and serialized before submission.
TransactionBuilder Class
The TransactionBuilder class provides a convenient API for building and signing transactions.
Creating a Builder Instance
import { TransactionBuilder } from '@amadeus-protocol/sdk'
// Create builder with private key (convenient for multiple transactions)
const builder = new TransactionBuilder('5Kd3N...') // Base58 encoded seed
// Or create without private key (must provide keys in each method call)
const builder = new TransactionBuilder()
Building Transfer Transactions
Option 1: Build and Sign in One Step (Recommended)
const builder = new TransactionBuilder(privateKey)
const { txHash, txPacked } = builder.transfer({
recipient: '5Kd3N...', // Base58 encoded recipient address
amount: 10.5, // Amount in human-readable format
symbol: 'AMA' // Token symbol
})
// Submit the transaction
const result = await sdk.transaction.submit(txPacked)
Option 2: Build Unsigned, Then Sign (More Control)
const builder = new TransactionBuilder(privateKey)
// Build unsigned transaction
const unsignedTx = builder.buildTransfer({
recipient: '5Kd3N...',
amount: 10.5,
symbol: 'AMA'
})
// Can inspect or modify unsignedTx before signing
console.log('Nonce:', unsignedTx.tx.nonce)
console.log('Action:', unsignedTx.tx.action)
// Sign the transaction
const { txHash, txPacked } = builder.sign(unsignedTx)
// Submit the transaction
const result = await sdk.transaction.submit(txPacked)
Building Custom Transactions
Option 1: Build and Sign in One Step
import { TransactionBuilder, fromBase58, toAtomicAma } from '@amadeus-protocol/sdk'
const builder = new TransactionBuilder(privateKey)
const { txHash, txPacked } = builder.buildAndSign('Coin', 'transfer', [
fromBase58('5Kd3N...'), // Recipient bytes
toAtomicAma(10.5).toString(), // Amount in atomic units
'AMA' // Token symbol
])
Option 2: Build Unsigned, Then Sign
const builder = new TransactionBuilder(privateKey)
// Build unsigned transaction
const unsignedTx = builder.build('Coin', 'transfer', [
fromBase58('5Kd3N...'),
toAtomicAma(10.5).toString(),
'AMA'
])
// Inspect or modify if needed
console.log('Transaction:', unsignedTx.tx)
// Sign the transaction
const { txHash, txPacked } = builder.sign(unsignedTx)
Using Static Methods
You can also use static methods without creating an instance:
Build and Sign Transfer
import { TransactionBuilder } from '@amadeus-protocol/sdk'
const { txHash, txPacked } = TransactionBuilder.buildSignedTransfer({
senderPrivkey: '5Kd3N...', // Base58 encoded seed
recipient: '5Kd3N...', // Base58 encoded recipient address
amount: 10.5, // Amount in human-readable format
symbol: 'AMA' // Token symbol
})
Build Unsigned, Then Sign
import {
TransactionBuilder,
getPublicKey,
deriveSkAndSeed64FromBase58Seed
} from '@amadeus-protocol/sdk'
// Derive keys from seed
const { seed64 } = deriveSkAndSeed64FromBase58Seed('5Kd3N...')
const signerPubKey = getPublicKey(seed64)
// Build unsigned transfer
const unsignedTx = TransactionBuilder.buildTransfer(
{ recipient: '5Kd3N...', amount: 10.5, symbol: 'AMA' },
signerPubKey
)
// Sign the transaction
const { txHash, txPacked } = TransactionBuilder.sign(unsignedTx, '5Kd3N...')
Transaction Structure
Unsigned Transaction
interface UnsignedTransaction {
signer: Uint8Array // Sender's public key (48 bytes)
nonce: bigint // Transaction nonce
action: TransactionAction
}
interface TransactionAction {
op: 'call' // Operation type
contract: string // Contract name or address
function: string // Function to call
args: SerializableValue[] // Function arguments
}
Signed Transaction Result
interface BuildTransactionResult {
txHash: string // Transaction hash (Base58 encoded)
txPacked: Uint8Array // Packed transaction ready for submission
}
Transaction Nonces
Nonces are automatically generated using timestamps:
// Nonce = BigInt(Date.now()) * 1_000_000n
// This ensures uniqueness for transactions
Important: For high-frequency transactions, ensure sufficient time between transactions to avoid nonce collisions.
Amount Handling
Always use atomic units for transaction amounts:
import { toAtomicAma } from '@amadeus-protocol/sdk'
// ✅ Good - use conversion function
const amount = toAtomicAma(10.5) // Returns 10500000000
// ❌ Bad - may lose precision
const amount = 10.5 * 1000000000
Submitting Transactions
Submit Without Waiting
const result = await sdk.transaction.submit(txPacked)
if (result.error === 'ok') {
console.log('Transaction hash:', result.hash)
} else {
console.error('Transaction error:', result.error)
}
Submit and Wait for Confirmation
try {
const result = await sdk.transaction.submitAndWait(txPacked)
if (result.error === 'ok') {
console.log('Transaction confirmed!')
console.log('Hash:', result.hash)
console.log('Entry hash:', result.metadata?.entry_hash)
console.log('Receipt:', result.receipt)
} else {
console.error('Transaction error:', result.error)
}
} catch (error) {
console.error('Transaction failed or timed out:', error)
}
Common Transaction Types
Token Transfer
const builder = new TransactionBuilder(privateKey)
const { txHash, txPacked } = builder.transfer({
recipient: recipientAddress,
amount: amount,
symbol: 'AMA'
})
Contract Call
const builder = new TransactionBuilder(privateKey)
const { txHash, txPacked } = builder.buildAndSign('ContractName', 'functionName', [
arg1,
arg2,
arg3
])
Error Handling
import { AmadeusSDKError } from '@amadeus-protocol/sdk'
try {
const { txHash, txPacked } = builder.transfer({
recipient: address,
amount: 10.5,
symbol: 'AMA'
})
const result = await sdk.transaction.submit(txPacked)
if (result.error === 'ok') {
console.log('Success:', result.hash)
} else {
console.error('Transaction error:', result.error)
}
} catch (error) {
if (error instanceof AmadeusSDKError) {
console.error('SDK Error:', error.message)
} else {
console.error('Unexpected error:', error)
}
}
Best Practices
1. Always Validate Inputs
function validateAddress(address: string): boolean {
try {
const bytes = fromBase58(address)
return bytes.length === 48
} catch {
return false
}
}
if (!validateAddress(recipient)) {
throw new Error('Invalid recipient address')
}
2. Check Balance Before Transferring
const balance = await sdk.wallet.getBalance(senderAddress, 'AMA')
if (balance.balance.float < amount + fee) {
throw new Error('Insufficient balance')
}
3. Use Appropriate Nonce Timing
For high-frequency transactions, add delays:
async function submitMultipleTransactions(txs: Transaction[]) {
for (const tx of txs) {
await sdk.transaction.submit(tx)
await new Promise((resolve) => setTimeout(resolve, 100)) // 100ms delay
}
}
4. Handle Transaction Errors
const result = await sdk.transaction.submit(txPacked)
switch (result.error) {
case 'ok':
console.log('Success')
break
case 'insufficient_funds':
console.error('Not enough balance')
break
case 'invalid_signature':
console.error('Invalid signature')
break
default:
console.error('Unknown error:', result.error)
}
Advanced Usage
Building Transactions Without Private Key
const builder = new TransactionBuilder()
// Must provide public key for building
const { seed64 } = deriveSkAndSeed64FromBase58Seed(privateKey)
const signerPubKey = getPublicKey(seed64)
// Build unsigned transaction
const unsignedTx = builder.build('Coin', 'transfer', args, signerPubKey)
// Must provide private key for signing
const { txHash, txPacked } = builder.sign(unsignedTx, privateKey)
Inspecting Transactions
const unsignedTx = builder.buildTransfer({
recipient: address,
amount: 10.5,
symbol: 'AMA'
})
// Inspect transaction details
console.log('Signer:', toBase58(unsignedTx.tx.signer))
console.log('Nonce:', unsignedTx.tx.nonce.toString())
console.log('Contract:', unsignedTx.tx.action.contract)
console.log('Function:', unsignedTx.tx.action.function)
console.log('Args:', unsignedTx.tx.action.args)
console.log('Hash:', toBase58(unsignedTx.hash))