The Crypto Shop Backend implements a multi-stage payment flow that integrates traditional e-commerce order management with TRON blockchain transactions.
Payment Flow Overview
Stage 1: Order Creation
The first stage creates an order record with pending status.
Order Creation Endpoint
// POST /api/orders
export const createOrder = async (req, res) => {
const { products } = req.body;
const userId = req.user.id;
// 1. Prevent multiple pending orders per user
const pendingOrder = await Order.findOne({ userId, status: 'pending' });
if (pendingOrder) {
return res.status(409).json({
error: 'You have a pending order. Please complete or cancel it first.',
code: 'PENDING_ORDER_EXISTS',
pendingOrderId: pendingOrder._id
});
}
// 2. Validate user wallet
const user = await User.findById(userId);
if (!user || !user.wallet.address) {
return res.status(400).json({ error: 'User wallet not found' });
}
// 3. Calculate order total
let subtotal = 0;
const orderProducts = [];
for (const item of products) {
const product = await Product.findById(item.productId);
if (!product) {
return res.status(404).json({ error: `Product not found` });
}
if (product.stock < item.quantity) {
return res.status(400).json({ error: `Insufficient stock` });
}
subtotal += product.price * item.quantity;
orderProducts.push({
productId: product._id,
name: product.name,
price: product.price,
quantity: item.quantity,
color: item.color || null
});
}
const networkFee = -0.01;
const total = subtotal + networkFee;
// 4. Get merchant wallet address
const admin = await User.findOne({ role: 'admin' });
const merchantAddress = admin?.wallet?.address ||
process.env.MERCHANT_WALLET_ADDRESS;
// 5. Create order
const order = new Order({
userId,
products: orderProducts,
subtotal,
networkFee,
total,
walletAddress: user.wallet.address,
merchantAddress
});
await order.save();
res.status(201).json({ success: true, order });
};
Reference: src/api/orders/createOrder.js:5-85
Important: Users can only have one pending order at a time. This prevents inventory reservation conflicts and simplifies payment tracking.
Order Pricing Calculation
const networkFee = -0.01; // TRX network fee estimate
const total = subtotal + networkFee;
Reference: src/api/orders/createOrder.js:48-49
The network fee is a negative value (-0.01 TRX) representing the estimated TRON blockchain transaction cost. The actual fee is paid from the user’s wallet balance separately.
Stage 2: Payment Execution
Once an order is created, the user initiates payment by submitting the transaction to the blockchain.
Payment Endpoint
// POST /api/orders/:id/pay
export const payOrder = async (req, res) => {
const { id } = req.params;
const userId = req.user.id;
// 1. Verify order ownership and status
const order = await Order.findById(id);
if (order.userId.toString() !== userId) {
return res.status(403).json({ error: 'Unauthorized' });
}
if (order.status !== 'pending') {
return res.status(400).json({ error: 'Order is not pending' });
}
// 2. Verify wallet and balance
const user = await User.findById(userId);
const balance = await getBalance(user.wallet.address);
if (balance < Math.abs(order.total)) {
return res.status(400).json({
error: 'Insufficient balance',
code: 'INSUFFICIENT_BALANCE',
userBalance: balance,
requiredAmount: Math.abs(order.total)
});
}
// 3. Send TRX transaction
try {
const tx = await sendTRX(
user.wallet.privateKey,
order.merchantAddress,
Math.abs(order.total)
);
const txHash = tx.txid || tx.txID || tx.transaction?.txID;
// 4. Create transaction record
const transaction = new Transaction({
userId,
orderId: order._id,
type: 'purchase',
amount: Math.abs(order.total),
currency: 'TRX',
network: 'TRC-20',
transactionHash: txHash,
fromAddress: order.walletAddress,
toAddress: order.merchantAddress,
status: 'pending',
confirmations: 0
});
await transaction.save();
// 5. Link transaction to order
order.transactionHash = txHash;
await order.save();
res.json({
success: true,
message: 'Payment sent. Waiting for blockchain confirmation.',
transaction: {
hash: txHash,
status: 'pending'
}
});
} catch (txError) {
// Mark order as failed if transaction fails
order.status = 'failed';
await order.save();
throw txError;
}
};
Reference: src/api/orders/payOrder.js:6-88
Balance Verification
const balance = await getBalance(user.wallet.address);
if (balance < Math.abs(order.total)) {
return res.status(400).json({
error: 'Insufficient balance',
code: 'INSUFFICIENT_BALANCE',
userBalance: balance,
requiredAmount: Math.abs(order.total)
});
}
Reference: src/api/orders/payOrder.js:30-39
The payment endpoint checks the user’s TRX balance before attempting the transaction to provide immediate feedback and prevent failed blockchain submissions.
Stage 3: Blockchain Confirmation
After payment submission, the transaction listener monitors the blockchain for confirmation.
Transaction Listener Service
// src/services/transactionListener.js
const CONFIRMATIONS_REQUIRED = 21;
const CHECK_INTERVAL = 15000; // 15 seconds
export const startTransactionListener = () => {
syncPendingTransactions();
setInterval(() => {
syncPendingTransactions();
}, CHECK_INTERVAL);
};
Reference: src/services/transactionListener.js:16-107
Transaction Synchronization
export const syncPendingTransactions = async () => {
// 1. Find all pending transactions
const pendingTransactions = await Transaction.find({
status: 'pending',
transactionHash: { $ne: null }
});
for (const transaction of pendingTransactions) {
// 2. Check blockchain status
const status = await getTransactionStatus(transaction.transactionHash);
if (status && status.confirmed) {
// 3. Update transaction
transaction.status = 'confirmed';
transaction.confirmations = CONFIRMATIONS_REQUIRED;
transaction.updatedAt = Date.now();
await transaction.save();
// 4. Process order
if (transaction.orderId) {
const order = await Order.findById(transaction.orderId);
if (transaction.type === 'purchase' && order.status === 'pending') {
// 5. Complete order
order.status = 'completed';
order.updatedAt = Date.now();
await order.save();
// 6. Update inventory
for (const item of order.products) {
await Product.findByIdAndUpdate(
item.productId,
{ $inc: { stock: -item.quantity } }
);
}
// 7. Notify user
emitTransactionConfirmed(
transaction.userId.toString(),
transaction.orderId.toString(),
transaction.transactionHash
);
}
}
}
}
};
Reference: src/services/transactionListener.js:36-100
Stock is only decremented after blockchain confirmation, not at order creation. This prevents inventory locking for unpaid orders.
Blockchain Status Check
export const getTransactionStatus = async (txHash) => {
try {
const tx = await tronWeb.trx.getTransactionInfo(txHash);
if (!tx || !tx.blockNumber) {
return null;
}
return {
confirmed: true,
confirmations: 21
};
} catch (error) {
return null;
}
};
Reference: src/services/transactionListener.js:19-34
Stage 4: Real-Time Notification
When confirmation occurs, the user receives an instant notification via WebSocket.
Socket Event Emission
export const emitTransactionConfirmed = (userId, orderId, txHash) => {
const io = getIO();
io.to(`user:${userId}`).emit('transaction:confirmed', {
orderId,
txHash,
message: 'Your purchase has been confirmed. You can now place new orders.',
timestamp: new Date()
});
};
Reference: src/config/socket.js:37-45
Client-Side Connection
Clients must join their user room to receive notifications:
// Client-side example
socket.emit('join-user', userId);
socket.on('transaction:confirmed', (data) => {
console.log('Order confirmed:', data.orderId);
console.log('Transaction:', data.txHash);
});
Order Schema
The Order model tracks all payment-related information:
{
orderId: String, // Auto-generated: #TRX-1, #TRX-2, etc.
userId: ObjectId,
products: [{
productId: ObjectId,
name: String,
price: Number,
quantity: Number,
color: String
}],
subtotal: Number,
networkFee: Number, // -0.01 TRX
total: Number,
status: String, // pending, completed, failed, refunded, cancelled
paymentMethod: String, // TRC-20
transactionHash: String,
walletAddress: String, // User's wallet
merchantAddress: String // Admin's wallet
}
Reference: src/models/Order.js:3-74
Order ID Generation
orderSchema.pre('save', async function(next) {
if (!this.isNew) return next();
const lastOrder = await mongoose.model('Order')
.findOne()
.sort({ createdAt: -1 });
const lastNumber = lastOrder ?
parseInt(lastOrder.orderId.replace('#TRX-', '')) : 0;
this.orderId = `#TRX-${lastNumber + 1}`;
next();
});
Reference: src/models/Order.js:76-87
Error Handling
Insufficient Balance
{
"error": "Insufficient balance",
"code": "INSUFFICIENT_BALANCE",
"userBalance": 5.2,
"requiredAmount": 10.5
}
Pending Order Exists
{
"error": "You have a pending order. Please complete or cancel it first.",
"code": "PENDING_ORDER_EXISTS",
"pendingOrderId": "507f1f77bcf86cd799439011"
}
Transaction Failure
If the blockchain transaction fails, the order status is set to failed:
catch (txError) {
order.status = 'failed';
await order.save();
throw txError;
}
Reference: src/api/orders/payOrder.js:80-84
Refund Flow
The transaction listener also handles refunds:
if (transaction.type === 'refund' && order.status !== 'refunded') {
order.status = 'refunded';
order.updatedAt = Date.now();
await order.save();
// Restore product stock
for (const item of order.products) {
await Product.findByIdAndUpdate(
item.productId,
{ $inc: { stock: item.quantity } } // Add back to inventory
);
}
}
Reference: src/services/transactionListener.js:79-91
Refunds restore product inventory by incrementing stock quantities. The refund transaction must also be confirmed on the blockchain before the order status changes.
Payment Timeline
| Stage | Duration | Status |
|---|
| Order creation | Instant | pending |
| Payment submission | 3-5 seconds | pending |
| Blockchain broadcast | 5-10 seconds | pending |
| Block confirmation | 1-3 minutes | pending |
| 21 confirmations | 1-3 minutes | confirmed |
| Order completion | Instant | completed |
Total payment flow typically completes in 2-6 minutes on the TRON network. Nile testnet may be faster, while mainnet during high congestion could take longer.