Makuhari Development Corporation
6 min read, 1168 words, last updated: 2025/12/10
TwitterLinkedInFacebookEmail

Building a Secure H5-to-Wallet Payment System: A Minimal Implementation Guide

When building H5 applications that need to integrate with third-party wallets for payments, many developers initially consider using plaintext deeplinks containing payment amounts and addresses. However, this approach introduces significant security risks. This tutorial will guide you through implementing a more secure, session-based approach that eliminates most vulnerabilities while remaining simple to implement.

Prerequisites

Before starting this tutorial, you should have:

  • Basic understanding of Web3 payment flows
  • Familiarity with REST API development
  • Knowledge of mobile app deeplink mechanisms
  • Understanding of basic cryptographic concepts (signatures, nonces)

Using plaintext deeplinks like wallet://pay?amount=10&to=0x123... creates several security risks:

  • URL hijacking: Malicious apps can intercept and modify payment parameters
  • Man-in-the-middle attacks: Payment data can be tampered with during transmission
  • Phishing attacks: Fake wallets can be launched with legitimate-looking URLs
  • Parameter tampering: Users might not notice if amounts or addresses are modified

The Solution: Session-Based Payment Flow

Instead of embedding sensitive payment data in deeplinks, we'll implement a system where:

  1. H5 app creates a payment session and gets a session ID
  2. Only the session ID is passed to the wallet via deeplink
  3. Wallet fetches actual payment data from your backend
  4. Payment confirmation goes through your backend

Step-by-Step Implementation

Step 1: Create Payment Session Endpoint

First, implement an endpoint for creating payment sessions:

// Backend API - Create Payment Session
app.post('/api/payments/create', async (req, res) => {
  const { amount, token, chainId, to } = req.body;
  
  // Generate unique session ID
  const sessionId = generateSecureId();
  
  // Store payment data with expiration
  await paymentSessions.set(sessionId, {
    amount,
    token,
    chainId,
    to,
    status: 'pending',
    createdAt: Date.now(),
    expiresAt: Date.now() + 120000 // 2 minutes
  });
  
  res.json({
    sessionId,
    expiresAt: Date.now() + 120000
  });
});
 
function generateSecureId() {
  return crypto.randomBytes(16).toString('hex');
}

Step 2: H5 Frontend Implementation

In your H5 app, create the payment session and launch the wallet:

// H5 Frontend - Initiate Payment
class PaymentManager {
  async initiatePayment(paymentData) {
    try {
      // Create payment session
      const response = await fetch('/api/payments/create', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(paymentData)
      });
      
      const { sessionId } = await response.json();
      
      // Launch wallet with session ID only
      const deeplink = `wallet://pay?sessionId=${sessionId}`;
      window.location.href = deeplink;
      
      return sessionId;
    } catch (error) {
      console.error('Payment initiation failed:', error);
      throw error;
    }
  }
}
 
// Usage
const paymentManager = new PaymentManager();
paymentManager.initiatePayment({
  amount: '10',
  token: 'USDC',
  chainId: 1,
  to: '0xYourReceiveAddress'
});

Step 3: Payment Info Retrieval Endpoint

Create an endpoint for wallets to fetch payment information:

// Backend API - Get Payment Info
app.get('/api/payments/info', async (req, res) => {
  const { sessionId } = req.query;
  
  if (!sessionId) {
    return res.status(400).json({ error: 'Session ID required' });
  }
  
  const paymentData = await paymentSessions.get(sessionId);
  
  if (!paymentData) {
    return res.status(404).json({ error: 'Session not found' });
  }
  
  // Check expiration
  if (Date.now() > paymentData.expiresAt) {
    await paymentSessions.delete(sessionId);
    return res.status(410).json({ error: 'Session expired' });
  }
  
  // Add nonce for replay protection
  const nonce = crypto.randomBytes(8).toString('hex');
  
  const responseData = {
    amount: paymentData.amount,
    token: paymentData.token,
    chainId: paymentData.chainId,
    to: paymentData.to,
    nonce,
    timestamp: Date.now()
  };
  
  // Optional: Add signature for data integrity
  const signature = signPaymentData(responseData);
  
  res.json({
    ...responseData,
    signature
  });
});

Step 4: Wallet Integration (Pseudocode)

Here's how a wallet app would integrate with this system:

// Wallet App - Handle Payment Request
class WalletPaymentHandler {
  async handlePaymentRequest(sessionId) {
    try {
      // Fetch payment info from backend
      const response = await fetch(
        `https://yourapi.com/api/payments/info?sessionId=${sessionId}`,
        {
          headers: {
            'X-Wallet-Client': 'YourWalletApp'
          }
        }
      );
      
      const paymentInfo = await response.json();
      
      // Verify signature (optional but recommended)
      if (!this.verifySignature(paymentInfo)) {
        throw new Error('Invalid payment data signature');
      }
      
      // Display payment confirmation to user
      const userConfirmed = await this.showPaymentConfirmation(paymentInfo);
      
      if (userConfirmed) {
        // Execute transaction
        const txHash = await this.executePayment(paymentInfo);
        
        // Confirm payment with backend
        await this.confirmPayment(sessionId, txHash);
      }
      
    } catch (error) {
      console.error('Payment failed:', error);
      throw error;
    }
  }
  
  async confirmPayment(sessionId, txHash) {
    await fetch('https://yourapi.com/api/payments/confirm', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Wallet-Client': 'YourWalletApp'
      },
      body: JSON.stringify({
        sessionId,
        txHash,
        timestamp: Date.now()
      })
    });
  }
}

Step 5: Payment Confirmation Endpoint

Implement the payment confirmation endpoint:

// Backend API - Confirm Payment
app.post('/api/payments/confirm', async (req, res) => {
  const { sessionId, txHash } = req.body;
  
  const paymentData = await paymentSessions.get(sessionId);
  
  if (!paymentData) {
    return res.status(404).json({ error: 'Session not found' });
  }
  
  if (paymentData.status !== 'pending') {
    return res.status(400).json({ error: 'Payment already processed' });
  }
  
  // Update session status
  await paymentSessions.set(sessionId, {
    ...paymentData,
    status: 'confirmed',
    txHash,
    confirmedAt: Date.now()
  });
  
  // Optional: Verify transaction on blockchain
  // const isValid = await verifyTransaction(txHash, paymentData);
  
  res.json({
    status: 'confirmed',
    txHash
  });
});

Step 6: Enhanced Security Features

Add these security enhancements to your implementation:

// EIP-712 Signature for Payment Data
function signPaymentData(paymentData) {
  const domain = {
    name: 'YourPaymentService',
    version: '1',
    chainId: paymentData.chainId
  };
  
  const types = {
    Payment: [
      { name: 'amount', type: 'string' },
      { name: 'token', type: 'string' },
      { name: 'to', type: 'address' },
      { name: 'nonce', type: 'string' },
      { name: 'timestamp', type: 'uint256' }
    ]
  };
  
  return ethers.utils._TypedDataEncoder.hash(domain, types, paymentData);
}
 
// Session cleanup utility
function cleanupExpiredSessions() {
  setInterval(async () => {
    const sessions = await paymentSessions.getAll();
    for (const [sessionId, data] of sessions) {
      if (Date.now() > data.expiresAt) {
        await paymentSessions.delete(sessionId);
      }
    }
  }, 60000); // Run every minute
}

Security Benefits

This implementation addresses the major security concerns:

Security Risk Solution
Plaintext amount tampering ✅ Amount stored securely on backend
URL parameter hijacking ✅ Only session ID in deeplink
Man-in-the-middle attacks ✅ Direct wallet-to-backend communication
Replay attacks ✅ Nonce and timestamp validation
Data integrity ✅ Optional cryptographic signatures

Complete Flow Summary

  1. H5 app creates payment session → receives sessionId
  2. H5 app launches wallet with deeplink containing only sessionId
  3. Wallet app extracts sessionId from deeplink
  4. Wallet app fetches payment data from your backend using sessionId
  5. User confirms payment in wallet interface
  6. Wallet app executes transaction and reports back to backend
  7. Backend marks session as completed

Practical Tips

  • Keep sessions short-lived: 2-5 minutes is usually sufficient
  • Implement proper logging: Track all payment sessions for debugging
  • Add rate limiting: Prevent abuse of session creation endpoints
  • Use HTTPS everywhere: Ensure all API communications are encrypted
  • Consider wallet-specific deeplink formats: Different wallets may have different URL schemes

Summary

By implementing this session-based approach, you've created a significantly more secure payment flow that eliminates the risks associated with plaintext deeplinks. The wallet now fetches trusted payment data directly from your backend rather than relying on potentially tampered URL parameters.

This minimal implementation provides a solid foundation that you can extend with additional features like multi-signature support, advanced fraud detection, or integration with existing payment processors. The key principle remains the same: never trust sensitive data that comes through client-side channels when server-side verification is possible.

Makuhari Development Corporation
法人番号: 6040001134259
サイトマップ
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.