Build / Integration Guide / Deposit Monitoring
5. Deposit Monitoring
💡 Concept: Users send funds to your exchange’s one-sided address with a payment ID. Your system monitors for these transactions and credits user accounts.
1. Generate Deposit Address
Pseudocode
FUNCTION generateDepositAddress(userId):
// Step 1: Input validation
IF (userId IS empty OR userId IS invalid):
THROW "Invalid user ID"
// Step 2: Generate unique payment identifier
timestamp = getCurrentTimestamp()
randomBytes = generateSecureRandom(8) // 8 bytes = 64 bits
paymentId = "deposit-" + userId + "-" + timestamp + "-" + randomBytes
// Step 3: Convert payment ID to bytes for gRPC
paymentIdBytes = convertToBytes(paymentId, "UTF-8")
// Step 4: Call wallet gRPC to get address with payment ID
request = {
payment_id: paymentIdBytes
}
TRY:
response = walletClient.GetPaymentIdAddress(request)
// Step 5: Extract address information
address = response.one_sided_address_base58
emojiAddress = response.one_sided_address_emoji
// Step 6: Store in database for tracking
depositRecord = {
userId: userId,
paymentId: paymentId,
address: address,
status: "pending",
createdAt: timestamp
}
database.save(depositRecord)
// Step 7: Return deposit instructions to user
RETURN {
address: address,
paymentId: paymentId,
emojiAddress: emojiAddress,
instructions: "Send XTM to this address with payment ID: " + paymentId,
qrCode: generateQRCode(address, paymentId)
}
CATCH (grpcError):
LOG "Failed to generate address: " + grpcError.message
THROW "Address generation failed"
grpcurl
PAYMENT_ID_B64=$(echo -n "deposit-user123-$(date +%s)" | base64)
grpcurl -plaintext
-d "{"payment_id": "$PAYMENT_ID_B64"}"
localhost:18143
tari.rpc.Wallet/GetPaymentIdAddress
Node.js
async function generateDepositAddress(userId, walletClient, db) {
if (!userId) throw new Error('Invalid user ID');
const timestamp = Date.now();
const randomBytes = crypto.randomBytes(8).toString('hex');
const paymentId = `deposit-${userId}-${timestamp}-${randomBytes}`;
const paymentIdBytes = Buffer.from(paymentId, 'utf-8');
try {
const response = await walletClient.getPaymentIdAddress({ paymentId: paymentIdBytes });
await db.deposits.create({
userId,
paymentId,
address: response.oneSidedAddressBase58,
status: 'pending',
createdAt: timestamp,
});
return {
address: response.oneSidedAddressBase58,
emojiAddress: response.oneSidedAddressEmoji,
paymentId,
instructions: `Send XTM to this address with payment ID: ${paymentId}`,
};
} catch (grpcError) {
throw new Error(`Address generation failed: ${grpcError.message}`);
}
}
Python
def generate_deposit_address(user_id, wallet_client, db):
if not user_id:
raise ValueError("Invalid user ID")
timestamp = int(time.time())
random_bytes = secrets.token_hex(8)
payment_id = f"deposit-{user_id}-{timestamp}-{random_bytes}"
payment_id_bytes = payment_id.encode("utf-8")
try:
response = wallet_client.GetPaymentIdAddress(payment_id=payment_id_bytes)
db.deposits.save({
"user_id": user_id,
"payment_id": payment_id,
"address": response.one_sided_address_base58,
"status": "pending",
"created_at": timestamp,
})
return {
"address": response.one_sided_address_base58,
"emoji_address": response.one_sided_address_emoji,
"payment_id": payment_id,
"instructions": f"Send XTM to this address with payment ID: {payment_id}",
}
except grpc.RpcError as e:
raise RuntimeError(f"Address generation failed: {e.details()}")
Rust
async fn generate_deposit_address(
user_id: &str,
wallet_client: &mut WalletClient,
) -> Result> {
if user_id.is_empty() {
return Err("Invalid user ID".into());
}
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let random_bytes = hex::encode(rand::random::<[u8; 8]>());
let payment_id = format!("deposit-{}-{}-{}", user_id, timestamp, random_bytes);
let request = GetPaymentIdAddressRequest {
payment_id: payment_id.clone().into_bytes(),
};
let response = wallet_client.get_payment_id_address(request).await?;
Ok(DepositInfo {
address: response.one_sided_address_base58,
payment_id,
instructions: format!("Send XTM to this address with payment ID: {}", payment_id),
})
}
PHP
function generateDepositAddress(string $userId, WalletClient $walletClient, PDO $db): array {
if (empty($userId)) {
throw new InvalidArgumentException('Invalid user ID');
}
$timestamp = time();
$randomBytes = bin2hex(random_bytes(8));
$paymentId = "deposit-{$userId}-{$timestamp}-{$randomBytes}";
try {
$response = $walletClient->GetPaymentIdAddress(['payment_id' => $paymentId])->wait();
$stmt = $db->prepare(
'INSERT INTO deposits (user_id, payment_id, address, status, created_at) VALUES (?, ?, ?, ?, ?)'
);
$stmt->execute([$userId, $paymentId, $response->getOneSidedAddressBase58(), 'pending', $timestamp]);
return [
'address' => $response->getOneSidedAddressBase58(),
'payment_id' => $paymentId,
'instructions' => "Send XTM to this address with payment ID: {$paymentId}",
];
} catch (Exception $e) {
throw new RuntimeException('Address generation failed: ' . $e->getMessage());
}
}
2. Monitor for Incoming Transactions
Pseudocode
FUNCTION monitorIncomingTransactions():
processedTransactions = loadProcessedTransactionsList()
WHILE (service_is_running):
TRY:
// Method 1: Real-time streaming (preferred)
IF (streaming_available):
transactionStream = walletClient.StreamTransactionEvents()
FOR EACH event IN transactionStream:
IF (event.type == "Mined" AND event.direction == "Inbound"):
processTransactionEvent(event)
// Method 2: Polling (backup)
ELSE:
completedTransactions = walletClient.GetCompletedTransactions()
FOR EACH transaction IN completedTransactions:
IF (shouldProcessTransaction(transaction)):
processTransaction(transaction)
CATCH (connection_error):
LOG "Connection lost, retrying in 5 seconds..."
SLEEP(5)
CONTINUE
SLEEP(30) // Poll every 30 seconds if streaming not available
grpcurl
# Poll for completed transactions (streaming requires a persistent client)
grpcurl -plaintext localhost:18143 tari.rpc.Wallet/GetCompletedTransactions
# Check a specific transaction's confirmation status
grpcurl -plaintext -d '{"transaction_ids": ["PLACEHOLDER_tx_id"]}'
localhost:18143 tari.rpc.Wallet/GetTransactionInfo
Node.js
async function monitorIncomingTransactions(walletClient, db) {
const processed = await db.processedTx.getIds();
while (serviceIsRunning) {
try {
const stream = walletClient.streamTransactionEvents({});
for await (const event of stream) {
if (event.type === 'Mined' && event.direction === 'Inbound') {
await processTransaction(event.transaction, db, processed);
}
}
} catch (err) {
console.error('Connection lost, retrying in 5 seconds...', err);
await sleep(5000);
continue;
}
await sleep(30000);
}
}
async function processTransaction(tx, db, processed) {
const paymentId = tx.paymentId?.toString('utf-8');
if (!paymentId) return;
const deposit = await db.deposits.findByPaymentId(paymentId);
if (!deposit) return;
const amountXTM = tx.amount / 1_000_000;
await db.transaction(async (trx) => {
await trx.users.incrementBalance(deposit.userId, amountXTM);
await trx.deposits.markProcessed(deposit.id, tx.txId);
});
processed.add(tx.txId);
}
Python
def monitor_incoming_transactions(wallet_client, db):
processed = db.processed_tx.load_ids()
while service_is_running:
try:
for event in wallet_client.StreamTransactionEvents():
if event.type == "Mined" and event.direction == "Inbound":
process_transaction(event.transaction, db, processed)
except grpc.RpcError:
logging.warning("Connection lost, retrying in 5 seconds...")
time.sleep(5)
continue
time.sleep(30)
def process_transaction(tx, db, processed):
payment_id = tx.payment_id.decode("utf-8") if tx.payment_id else None
if not payment_id:
return
deposit = db.deposits.find_by_payment_id(payment_id)
if not deposit:
return
amount_xtm = tx.amount / 1_000_000
with db.transaction():
db.users.increment_balance(deposit.user_id, amount_xtm)
db.deposits.mark_processed(deposit.id, tx.tx_id)
processed.add(tx.tx_id)
Rust
async fn monitor_incoming_transactions(
wallet_client: &mut WalletClient,
db: &Db,
) -> Result<(), Box> {
let mut processed = db.processed_tx_ids().await?;
loop {
let mut stream = wallet_client.stream_transaction_events(()).await?.into_inner();
while let Some(event) = stream.message().await? {
if event.event_type == "Mined" && event.direction == "Inbound" {
process_transaction(&event.transaction, db, &mut processed).await?;
}
}
tokio::time::sleep(Duration::from_secs(30)).await;
}
}
async fn process_transaction(
tx: &Transaction,
db: &Db,
processed: &mut HashSet,
) -> Result<(), Box> {
let payment_id = String::from_utf8(tx.payment_id.clone())?;
let deposit = match db.find_deposit_by_payment_id(&payment_id).await? {
Some(d) => d,
None => return Ok(()),
};
let amount_xtm = tx.amount as f64 / 1_000_000.0;
db.credit_user_and_mark_processed(&deposit.user_id, amount_xtm, &tx.tx_id).await?;
processed.insert(tx.tx_id.clone());
Ok(())
}
PHP
function monitorIncomingTransactions(WalletClient $walletClient, PDO $db): void {
$processed = loadProcessedTransactionIds($db);
while (serviceIsRunning()) {
try {
$completed = $walletClient->GetCompletedTransactions(new Empty())->wait();
foreach ($completed->getTransactions() as $tx) {
if ($tx->getDirection() === 'INBOUND' && !in_array($tx->getTxId(), $processed)) {
processTransaction($tx, $db, $processed);
}
}
} catch (Exception $e) {
error_log('Connection lost, retrying in 5 seconds: ' . $e->getMessage());
sleep(5);
continue;
}
sleep(30);
}
}
function processTransaction($tx, PDO $db, array &$processed): void {
$paymentId = $tx->getPaymentId();
if (empty($paymentId)) {
return;
}
$stmt = $db->prepare('SELECT * FROM deposits WHERE payment_id = ?');
$stmt->execute([$paymentId]);
$deposit = $stmt->fetch();
if (!$deposit) {
return;
}
$amountXTM = $tx->getAmount() / 1_000_000;
$db->beginTransaction();
$db->prepare('UPDATE users SET balance = balance + ? WHERE id = ?')
->execute([$amountXTM, $deposit['user_id']]);
$db->prepare('UPDATE deposits SET status = ?, tx_id = ? WHERE id = ?')
->execute(['completed', $tx->getTxId(), $deposit['id']]);
$db->commit();
$processed[] = $tx->getTxId();
}