Skip to main content

8. Best Practices

Security and development best practices for using the Amadeus Protocol SDK.

Security

Private Key Management

Never:

  • ❌ Commit private keys to version control
  • ❌ Log private keys in console or files
  • ❌ Share private keys with anyone
  • ❌ Store private keys in plain text
  • ❌ Send private keys over unencrypted channels

Always:

  • ✅ Use environment variables for private keys
  • ✅ Encrypt private keys at rest
  • ✅ Use secure key storage solutions
  • ✅ Use separate keys for development and production
  • ✅ Implement proper access controls
// ✅ Good: Use environment variables
const privateKey = process.env.PRIVATE_KEY
if (!privateKey) {
throw new Error('PRIVATE_KEY not set')
}

// ❌ Bad: Hardcoded private key
const privateKey = '5Kd3N...' // DON'T DO THIS!

Password-Based Encryption

Always encrypt sensitive data before storage:

import { encryptWithPassword, decryptWithPassword } from '@amadeus-protocol/sdk'

// Encrypt before storage
const encrypted = await encryptWithPassword(privateKey, userPassword)

// Store encrypted data securely
await secureStorage.save({
encryptedData: encrypted.encryptedData,
iv: encrypted.iv,
salt: encrypted.salt
})

// Decrypt only when needed
const decrypted = await decryptWithPassword(encrypted, userPassword)

Address Validation

Always validate addresses before using them:

import { fromBase58, AMADEUS_PUBLIC_KEY_BYTE_LENGTH } from '@amadeus-protocol/sdk'

function isValidAddress(address: string): boolean {
try {
const bytes = fromBase58(address)
return bytes.length === AMADEUS_PUBLIC_KEY_BYTE_LENGTH
} catch {
return false
}
}

// Use validation
if (!isValidAddress(recipient)) {
throw new Error('Invalid recipient address')
}

Transaction Verification

Always verify transaction details before signing:

// Build unsigned transaction first
const unsignedTx = builder.buildTransfer({
recipient: address,
amount: amount,
symbol: 'AMA'
})

// Verify details
console.log('Recipient:', toBase58(unsignedTx.tx.action.args[0]))
console.log('Amount:', unsignedTx.tx.action.args[1])
console.log('Symbol:', unsignedTx.tx.action.args[2])

// Only sign after verification
const { txHash, txPacked } = builder.sign(unsignedTx)

Error Handling

Comprehensive Error Handling

Always handle errors appropriately:

import { AmadeusSDKError } from '@amadeus-protocol/sdk'

try {
const result = await sdk.transaction.submit(txPacked)

if (result.error === 'ok') {
console.log('Success:', result.hash)
} else {
// Handle specific transaction errors
switch (result.error) {
case 'insufficient_funds':
throw new Error('Not enough balance')
case 'invalid_signature':
throw new Error('Invalid signature')
default:
throw new Error(`Transaction error: ${result.error}`)
}
}
} catch (error) {
if (error instanceof AmadeusSDKError) {
// Handle SDK-specific errors
if (error.status === 404) {
console.log('Resource not found')
} else if (error.status === 400) {
console.error('Invalid request:', error.message)
} else {
console.error('SDK Error:', error.message)
}
} else {
// Handle unexpected errors
console.error('Unexpected error:', error)
}
}

Retry Logic

Implement retry logic for network requests:

async function submitWithRetry(
txPacked: Uint8Array,
maxRetries = 3,
delay = 1000
): Promise<SubmitTransactionResponse> {
for (let i = 0; i < maxRetries; i++) {
try {
return await sdk.transaction.submit(txPacked)
} catch (error) {
if (i === maxRetries - 1) throw error

// Exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay * Math.pow(2, i)))
}
}
throw new Error('Max retries exceeded')
}

Transaction Management

Nonce Management

For high-frequency transactions, ensure sufficient time between transactions:

async function submitMultipleTransactions(txs: Uint8Array[]) {
for (const tx of txs) {
await sdk.transaction.submit(tx)

// Add delay to avoid nonce collisions
await new Promise((resolve) => setTimeout(resolve, 100))
}
}

Balance Checking

Always check balance before transferring:

async function safeTransfer(recipient: string, amount: number, symbol: string) {
// Check balance first
const balance = await sdk.wallet.getBalance(senderAddress, symbol)
const transferAmount = toAtomicAma(amount)

if (balance.balance.flat < transferAmount) {
throw new Error('Insufficient balance')
}

// Proceed with transfer
const { txHash, txPacked } = builder.transfer({
recipient,
amount,
symbol
})

return await sdk.transaction.submit(txPacked)
}

Amount Precision

Always use conversion functions for amounts:

import { toAtomicAma, fromAtomicAma } from '@amadeus-protocol/sdk'

// ✅ Good - use conversion function
const amount = toAtomicAma(1.5)

// ❌ Bad - may lose precision
const amount = 1.5 * 1000000000

Configuration

Environment-Based Configuration

Use environment variables for configuration:

const sdk = new AmadeusSDK({
baseUrl: process.env.NODE_API_URL || 'https://nodes.amadeus.bot/api',
timeout: parseInt(process.env.REQUEST_TIMEOUT || '30000')
})

Request Timeouts

Set appropriate timeouts for your use case:

// Short timeout for quick queries
const quickSDK = new AmadeusSDK({
baseUrl: 'https://nodes.amadeus.bot/api',
timeout: 5000 // 5 seconds
})

// Longer timeout for transactions
const txSDK = new AmadeusSDK({
baseUrl: 'https://nodes.amadeus.bot/api',
timeout: 60000 // 60 seconds
})

Code Organization

Separate Concerns

Organize your code into logical modules:

// wallet.ts
export class WalletManager {
constructor(
private sdk: AmadeusSDK,
private builder: TransactionBuilder
) {}

async getBalance(address: string, symbol: string) {
return this.sdk.wallet.getBalance(address, symbol)
}

async transfer(recipient: string, amount: number, symbol: string) {
const { txHash, txPacked } = this.builder.transfer({
recipient,
amount,
symbol
})
return this.sdk.transaction.submit(txPacked)
}
}

// chain.ts
export class ChainQuerier {
constructor(private sdk: AmadeusSDK) {}

async getCurrentHeight() {
const tip = await this.sdk.chain.getTip()
return tip.entry.height
}

async getStats() {
return this.sdk.chain.getStats()
}
}

Type Safety

Use TypeScript types for better safety:

import type { AmadeusSDKConfig, Transaction, WalletBalance } from '@amadeus-protocol/sdk'

function processTransaction(tx: Transaction) {
// TypeScript will catch type errors
console.log(tx.hash)
console.log(tx.tx.action)
}

Testing

Testnet Usage

Always test on Testnet first:

// Testnet configuration
const testnetSDK = new AmadeusSDK({
baseUrl: 'https://testnet-rpc.ama.one/api'
})

// Test transactions on testnet
const testResult = await testnetSDK.transaction.submit(testTxPacked)

Mock Implementations

Create mocks for testing:

class MockAmadeusSDK {
async wallet = {
getBalance: async () => ({
balance: { float: 100, flat: 100000000000, symbol: 'AMA' }
})
}

async transaction = {
submit: async () => ({ error: 'ok', hash: 'mock-hash' })
}
}

Performance

Batch Operations

Batch operations when possible:

// ✅ Good: Batch balance queries
const addresses = ['addr1', 'addr2', 'addr3']
const balances = await Promise.all(addresses.map((addr) => sdk.wallet.getBalance(addr, 'AMA')))

// ❌ Bad: Sequential queries
for (const addr of addresses) {
await sdk.wallet.getBalance(addr, 'AMA')
}

Caching

Cache frequently accessed data:

class CachedChainQuerier {
private tipCache: ChainEntry | null = null
private cacheTime = 0
private cacheTTL = 5000 // 5 seconds

async getTip(): Promise<ChainEntry> {
const now = Date.now()
if (this.tipCache && now - this.cacheTime < this.cacheTTL) {
return this.tipCache
}

const { entry } = await this.sdk.chain.getTip()
this.tipCache = entry
this.cacheTime = now
return entry
}
}

Logging

Structured Logging

Use structured logging:

function logTransaction(txHash: string, result: SubmitTransactionResponse) {
console.log(
JSON.stringify({
type: 'transaction',
hash: txHash,
error: result.error,
timestamp: new Date().toISOString()
})
)
}

Sensitive Data

Never log sensitive data:

// ✅ Good: Log only public information
console.log('Transaction hash:', txHash)
console.log('Recipient:', recipientAddress)

// ❌ Bad: Log private keys
console.log('Private key:', privateKey) // NEVER DO THIS!

Documentation

Code Comments

Document complex logic:

/**
* Builds and submits a transfer transaction with balance verification
*
* @param recipient - Base58 encoded recipient address
* @param amount - Amount in AMA (human-readable)
* @param symbol - Token symbol (default: 'AMA')
* @returns Transaction hash if successful
* @throws Error if balance is insufficient or transaction fails
*/
async function transferWithVerification(
recipient: string,
amount: number,
symbol: string = 'AMA'
): Promise<string> {
// Implementation...
}

Next Steps