Skip to main content

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

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))

Next Steps