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​