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...
}