Build / Integration Guide / Withdrawal Processing
6. Withdrawal Processing
🔐 Security Alert: Withdrawal processing involves the cold storage wallet. Implement proper authorization, rate limiting, and multi-signature approval processes.
1. Validate Withdrawal Request
Pseudocode
FUNCTION validateWithdrawalRequest(userId, destinationAddress, amount):
// Step 1: Validate user authentication and KYC status
user = database.getUser(userId)
IF (user IS null):
THROW "User not found"
IF (NOT user.isKYCVerified):
THROW "User not KYC verified"
IF (user.isBlocked OR user.withdrawalsSuspended):
THROW "User account restricted"
// Step 2: Validate withdrawal amount
IF (amount <= 0):
THROW "Invalid withdrawal amount"
minimumWithdrawal = getMinimumWithdrawalAmount()
IF (amount < minimumWithdrawal):
THROW "Amount below minimum withdrawal: " + minimumWithdrawal + " XTM"
// Step 3: Check user balance (including fees)
withdrawalFee = calculateWithdrawalFee(amount)
totalRequired = amount + withdrawalFee
IF (user.availableBalance < totalRequired):
THROW "Insufficient balance"
// Step 4: Validate destination address
IF (NOT isValidTariAddress(destinationAddress)):
THROW "Invalid Tari address format"
// Step 5: Rate limiting and daily limits
dailyWithdrawalLimit = getDailyWithdrawalLimit(user.kycLevel)
todayWithdrawals = getTodayWithdrawals(userId)
IF (todayWithdrawals + amount > dailyWithdrawalLimit):
THROW "Daily withdrawal limit exceeded"
// Step 6: Create withdrawal record
withdrawalId = generateUniqueId()
withdrawalRecord = {
id: withdrawalId,
userId: userId,
destinationAddress: destinationAddress,
amount: amount,
fee: withdrawalFee,
status: "pending_approval",
createdAt: getCurrentTimestamp()
}
database.saveWithdrawal(withdrawalRecord)
database.reserveUserBalance(userId, totalRequired)
RETURN {
withdrawalId: withdrawalId,
amount: amount,
fee: withdrawalFee,
estimatedProcessingTime: "15-30 minutes"
}
grpcurl
# Check the cold wallet's available balance before approving
grpcurl -plaintext localhost:18143 tari.rpc.Wallet/GetState
# Validate the destination address format via the base node
grpcurl -plaintext -d '{"address": "PLACEHOLDER_destination_address"}'
localhost:18142 tari.rpc.BaseNode/CheckAddress
Node.js
async function validateWithdrawalRequest(userId, destinationAddress, amount, db) {
const user = await db.users.get(userId);
if (!user) throw new Error('User not found');
if (!user.isKYCVerified) throw new Error('User not KYC verified');
if (user.isBlocked || user.withdrawalsSuspended) throw new Error('User account restricted');
if (amount <= 0) throw new Error('Invalid withdrawal amount');
const minimumWithdrawal = await getMinimumWithdrawalAmount();
if (amount < minimumWithdrawal) {
throw new Error(`Amount below minimum withdrawal: ${minimumWithdrawal} XTM`);
}
const fee = calculateWithdrawalFee(amount);
if (user.availableBalance < amount + fee) {
throw new Error('Insufficient balance');
}
if (!isValidTariAddress(destinationAddress)) {
throw new Error('Invalid Tari address format');
}
const dailyLimit = getDailyWithdrawalLimit(user.kycLevel);
const todayWithdrawals = await getTodayWithdrawals(userId);
if (todayWithdrawals + amount > dailyLimit) {
throw new Error('Daily withdrawal limit exceeded');
}
const withdrawalId = crypto.randomUUID();
await db.withdrawals.create({
id: withdrawalId,
userId,
destinationAddress,
amount,
fee,
status: 'pending_approval',
});
await db.users.reserveBalance(userId, amount + fee);
return { withdrawalId, amount, fee, estimatedProcessingTime: '15-30 minutes' };
}
Python
def validate_withdrawal_request(user_id, destination_address, amount, db):
user = db.users.get(user_id)
if user is None:
raise ValueError("User not found")
if not user.is_kyc_verified:
raise ValueError("User not KYC verified")
if user.is_blocked or user.withdrawals_suspended:
raise ValueError("User account restricted")
if amount <= 0:
raise ValueError("Invalid withdrawal amount")
minimum_withdrawal = get_minimum_withdrawal_amount()
if amount < minimum_withdrawal:
raise ValueError(f"Amount below minimum withdrawal: {minimum_withdrawal} XTM")
fee = calculate_withdrawal_fee(amount)
if user.available_balance < amount + fee:
raise ValueError("Insufficient balance")
if not is_valid_tari_address(destination_address):
raise ValueError("Invalid Tari address format")
daily_limit = get_daily_withdrawal_limit(user.kyc_level)
today_withdrawals = get_today_withdrawals(user_id)
if today_withdrawals + amount > daily_limit:
raise ValueError("Daily withdrawal limit exceeded")
withdrawal_id = generate_unique_id()
db.withdrawals.save({
"id": withdrawal_id,
"user_id": user_id,
"destination_address": destination_address,
"amount": amount,
"fee": fee,
"status": "pending_approval",
})
db.users.reserve_balance(user_id, amount + fee)
return {
"withdrawal_id": withdrawal_id,
"amount": amount,
"fee": fee,
"estimated_processing_time": "15-30 minutes",
}
Rust
fn validate_withdrawal_request(
user_id: &str,
destination_address: &str,
amount: u64,
db: &Db,
) -> Result {
let user = db.get_user(user_id).ok_or("User not found")?;
if !user.is_kyc_verified {
return Err("User not KYC verified".into());
}
if user.is_blocked || user.withdrawals_suspended {
return Err("User account restricted".into());
}
if amount == 0 {
return Err("Invalid withdrawal amount".into());
}
let fee = calculate_withdrawal_fee(amount);
if user.available_balance < amount + fee {
return Err("Insufficient balance".into());
}
if !is_valid_tari_address(destination_address) {
return Err("Invalid Tari address format".into());
}
let daily_limit = get_daily_withdrawal_limit(user.kyc_level);
let today_withdrawals = get_today_withdrawals(user_id);
if today_withdrawals + amount > daily_limit {
return Err("Daily withdrawal limit exceeded".into());
}
let withdrawal_id = generate_unique_id();
db.save_withdrawal(&withdrawal_id, user_id, destination_address, amount, fee)?;
db.reserve_balance(user_id, amount + fee)?;
Ok(WithdrawalReceipt { withdrawal_id, amount, fee })
}
PHP
function validateWithdrawalRequest(string $userId, string $destinationAddress, float $amount, PDO $db): array {
$user = getUser($db, $userId);
if (!$user) throw new RuntimeException('User not found');
if (!$user['is_kyc_verified']) throw new RuntimeException('User not KYC verified');
if ($user['is_blocked'] || $user['withdrawals_suspended']) throw new RuntimeException('User account restricted');
if ($amount <= 0) throw new InvalidArgumentException('Invalid withdrawal amount');
$minimumWithdrawal = getMinimumWithdrawalAmount();
if ($amount < $minimumWithdrawal) {
throw new RuntimeException("Amount below minimum withdrawal: {$minimumWithdrawal} XTM");
}
$fee = calculateWithdrawalFee($amount);
if ($user['available_balance'] < $amount + $fee) {
throw new RuntimeException('Insufficient balance');
}
if (!isValidTariAddress($destinationAddress)) {
throw new RuntimeException('Invalid Tari address format');
}
$dailyLimit = getDailyWithdrawalLimit($user['kyc_level']);
$todayWithdrawals = getTodayWithdrawals($db, $userId);
if ($todayWithdrawals + $amount > $dailyLimit) {
throw new RuntimeException('Daily withdrawal limit exceeded');
}
$withdrawalId = generateUniqueId();
saveWithdrawal($db, $withdrawalId, $userId, $destinationAddress, $amount, $fee);
reserveUserBalance($db, $userId, $amount + $fee);
return [
'withdrawal_id' => $withdrawalId,
'amount' => $amount,
'fee' => $fee,
'estimated_processing_time' => '15-30 minutes',
];
}
2. Process Approved Withdrawals
🔒 Cold Storage Operation: This step requires access to the cold storage wallet. Ensure proper security protocols are followed.
Pseudocode
FUNCTION processApprovedWithdrawal(withdrawalRecord):
// Step 1: Final validation checks
IF (withdrawalRecord.status != "approved"):
THROW "Withdrawal not approved for processing"
// Step 2: Connect to cold storage wallet
coldWallet = connectToColdWallet()
IF (NOT coldWallet.isOnline()):
THROW "Cold wallet not accessible"
// Step 3: Sync wallet and verify balance
coldWallet.sync()
currentBalance = coldWallet.getBalance()
totalRequired = withdrawalRecord.amount + withdrawalRecord.fee
IF (currentBalance.available < totalRequired):
THROW "Insufficient cold wallet balance"
// Step 4: Create and validate payment recipient
recipient = {
address: withdrawalRecord.destinationAddress,
amount: withdrawalRecord.amount,
fee_per_gram: 25,
payment_type: "ONE_SIDED",
payment_id: withdrawalRecord.id
}
// Step 5: Execute the transaction
updateWithdrawalStatus(withdrawalRecord.id, "processing")
response = coldWallet.transfer({ recipients: [recipient] })
result = response.results[0]
IF (NOT result.is_success):
THROW "Transaction failed: " + result.failure_message
// Step 6: Update database records
updateWithdrawalStatus(withdrawalRecord.id, "completed")
deductReservedBalance(withdrawalRecord.userId, totalRequired)
// Step 7: Notifications
notifyUserWithdrawalCompleted(withdrawalRecord.userId, result.transaction_id)
RETURN {
success: true,
transactionId: result.transaction_id
}
grpcurl
# Check cold wallet balance before transferring
grpcurl -plaintext localhost:18143 tari.rpc.Wallet/GetState
# Execute a one-sided transfer to the destination address
grpcurl -plaintext -d '{
"recipients": [{
"address": "PLACEHOLDER_destination_address",
"amount": "PLACEHOLDER_amount_in_microxtm",
"fee_per_gram": 25,
"payment_type": "ONE_SIDED"
}]
}' localhost:18143 tari.rpc.Wallet/Transfer
Node.js
async function processApprovedWithdrawal(withdrawal, coldWallet, db) {
if (withdrawal.status !== 'approved') {
throw new Error('Withdrawal not approved for processing');
}
await coldWallet.sync();
const balance = await coldWallet.getBalance();
const totalRequired = withdrawal.amount + withdrawal.fee;
if (balance.available < totalRequired) {
throw new Error('Insufficient cold wallet balance');
}
await db.withdrawals.updateStatus(withdrawal.id, 'processing');
const response = await coldWallet.transfer({
recipients: [{
address: withdrawal.destinationAddress,
amount: withdrawal.amount,
feePerGram: 25,
paymentType: 'ONE_SIDED',
paymentId: withdrawal.id,
}],
});
const result = response.results[0];
if (!result.isSuccess) {
throw new Error(`Transaction failed: ${result.failureMessage}`);
}
await db.withdrawals.updateStatus(withdrawal.id, 'completed');
await db.users.deductReservedBalance(withdrawal.userId, totalRequired);
return { success: true, transactionId: result.transactionId };
}
Python
def process_approved_withdrawal(withdrawal, cold_wallet, db):
if withdrawal.status != "approved":
raise RuntimeError("Withdrawal not approved for processing")
cold_wallet.sync()
balance = cold_wallet.get_balance()
total_required = withdrawal.amount + withdrawal.fee
if balance.available < total_required:
raise RuntimeError("Insufficient cold wallet balance")
db.withdrawals.update_status(withdrawal.id, "processing")
response = cold_wallet.transfer(recipients=[{
"address": withdrawal.destination_address,
"amount": withdrawal.amount,
"fee_per_gram": 25,
"payment_type": "ONE_SIDED",
"payment_id": withdrawal.id,
}])
result = response.results[0]
if not result.is_success:
raise RuntimeError(f"Transaction failed: {result.failure_message}")
db.withdrawals.update_status(withdrawal.id, "completed")
db.users.deduct_reserved_balance(withdrawal.user_id, total_required)
return {"success": True, "transaction_id": result.transaction_id}
Rust
async fn process_approved_withdrawal(
withdrawal: &WithdrawalRecord,
cold_wallet: &mut ColdWallet,
db: &Db,
) -> Result {
if withdrawal.status != "approved" {
return Err("Withdrawal not approved for processing".into());
}
cold_wallet.sync().await?;
let balance = cold_wallet.get_balance().await?;
let total_required = withdrawal.amount + withdrawal.fee;
if balance.available < total_required {
return Err("Insufficient cold wallet balance".into());
}
db.update_withdrawal_status(&withdrawal.id, "processing").await?;
let response = cold_wallet.transfer(vec![Recipient {
address: withdrawal.destination_address.clone(),
amount: withdrawal.amount,
fee_per_gram: 25,
payment_type: PaymentType::OneSided,
payment_id: withdrawal.id.clone(),
}]).await?;
let result = &response.results[0];
if !result.is_success {
return Err(format!("Transaction failed: {}", result.failure_message));
}
db.update_withdrawal_status(&withdrawal.id, "completed").await?;
db.deduct_reserved_balance(&withdrawal.user_id, total_required).await?;
Ok(TransferResult { success: true, transaction_id: result.transaction_id.clone() })
}
PHP
function processApprovedWithdrawal(array $withdrawal, ColdWallet $coldWallet, PDO $db): array {
if ($withdrawal['status'] !== 'approved') {
throw new RuntimeException('Withdrawal not approved for processing');
}
$coldWallet->sync();
$balance = $coldWallet->getBalance();
$totalRequired = $withdrawal['amount'] + $withdrawal['fee'];
if ($balance['available'] < $totalRequired) {
throw new RuntimeException('Insufficient cold wallet balance');
}
updateWithdrawalStatus($db, $withdrawal['id'], 'processing');
$response = $coldWallet->transfer([[
'address' => $withdrawal['destination_address'],
'amount' => $withdrawal['amount'],
'fee_per_gram' => 25,
'payment_type' => 'ONE_SIDED',
'payment_id' => $withdrawal['id'],
]]);
$result = $response['results'][0];
if (!$result['is_success']) {
throw new RuntimeException('Transaction failed: ' . $result['failure_message']);
}
updateWithdrawalStatus($db, $withdrawal['id'], 'completed');
deductReservedBalance($db, $withdrawal['user_id'], $totalRequired);
return ['success' => true, 'transaction_id' => $result['transaction_id']];
}