Integration Guide¶
This guide provides step-by-step instructions for integrating UniVoucher functionality into your own applications. Whether you're building a dApp, a rewards platform, or a gifting service, you can leverage UniVoucher's capabilities through the smart contract interface.
Integration Architecture Patterns¶
Understanding the right architecture for your use case is crucial for a successful integration. Choose the pattern that best fits your application's requirements.
Client-Side Integration (Recommended for Web Apps)¶
Best for user-facing applications where users directly interact with cards.
Architecture: - Frontend calls UniVoucher API directly for card data retrieval - Frontend handles decryption and signature creation using Web Crypto API - Backend handles business logic and optional gasless transactions
Advantages: - Better security (secrets never leave user's device) - Reduced server load - Real-time blockchain interaction
Use Cases: - Gift card marketplaces - User dashboards - Direct redemption interfaces
Server-Side Integration¶
Best for server-to-server integrations and automated systems.
Architecture: - Backend handles all UniVoucher operations - Requires secure secret management - Direct smart contract interaction from server
Advantages: - Centralized control - Easier monitoring and logging - Suitable for batch operations
Use Cases: - Automated reward distribution - API-based card creation - Backend reward systems
Hybrid Integration¶
Optimal balance for applications requiring both user interaction and server control.
Architecture: - Frontend: Card checking, decryption, signature creation - Backend: Gasless transaction execution, business logic - Combined approach for optimal UX and security
Advantages: - Best user experience - Secure secret handling - Flexible transaction management
Use Cases: - Gasless redemption services - Enterprise integrations - Multi-user platforms
Integration Approach¶
UniVoucher is a fully decentralized application, which means all core functionality operates directly through blockchain smart contracts. Unlike traditional platforms, there is no centralized backend that processes or stores tangible gift card data - everything is securely stored on the blockchain and generated locally on users' devices.
This architecture requires direct interaction with the UniVoucher smart contract for all actions that modify state (creating cards, redeeming cards, cancellations). However, we do provide a read-only API at api.univoucher.com that indexes blockchain data for easier querying and integration. This API can be helpful for retrieving card information, checking statuses, and building user interfaces without having to directly query the blockchain for historical data.
For the complete API specification, refer to the OpenAPI documentation or check our API Reference page. The API provides endpoints for retrieving card data, fee information, and blockchain details, but remember that all state-changing operations must be performed via direct smart contract interaction.
Prerequisites
Before integrating, ensure you have:
- Basic understanding of Web3 development
- Experience with Ethereum contract interactions
- A Web3 provider setup in your application
- Production-ready RPC provider (Alchemy, Infura, QuickNode)
Security & Configuration Requirements¶
Content Security Policy (CSP) Configuration¶
For web applications, configure CSP to allow necessary external connections:
// Express.js with Helmet example
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
connectSrc: [
"'self'",
"https://api.univoucher.com", // UniVoucher API
"https://*.g.alchemy.com", // Alchemy RPC
"https://*.infura.io", // Infura RPC (if used)
"https://*.quicknode.com" // QuickNode RPC (if used)
],
scriptSrc: [
"'self'",
"https://unpkg.com", // For ethers.js CDN
"https://cdn.jsdelivr.net" // Alternative CDN
],
styleSrc: ["'self'", "'unsafe-inline'"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"]
},
},
}));
Production RPC Configuration¶
⚠️ Critical: Never use public RPC endpoints in production
Public RPCs are unreliable, rate-limited, and unsuitable for production applications.
Recommended Providers: - Alchemy (Recommended) - Reliable, well-documented - Infura - Established provider with good uptime - QuickNode - High-performance option
Secure RPC Pattern for Web Applications:
// Backend endpoint to provide RPC URLs securely
app.get('/api/rpc/:chainId', (req, res) => {
const chainId = parseInt(req.params.chainId);
const alchemyNetworks = {
1: 'eth-mainnet',
10: 'opt-mainnet',
56: 'bnb-mainnet',
137: 'polygon-mainnet',
8453: 'base-mainnet',
42161: 'arb-mainnet',
43114: 'avax-mainnet'
};
const network = alchemyNetworks[chainId];
if (!network || !process.env.ALCHEMY_KEY) {
return res.status(400).json({ error: 'Unsupported chain or missing API key' });
}
const rpcUrl = `https://${network}.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`;
res.json({ rpcUrl });
});
// Frontend usage
async function getProvider(chainId) {
const response = await fetch(`/api/rpc/${chainId}`);
const { rpcUrl } = await response.json();
return new ethers.providers.JsonRpcProvider(rpcUrl);
}
Gas Estimation and Pricing¶
Proper gas estimation is crucial for successful transactions across different networks. Each blockchain has different gas pricing mechanisms and costs.
Backend/Server-Side Applications¶
For server-side integrations (like gasless services), implement automatic gas estimation:
// Alchemy-powered gas estimation (recommended)
async function getGasEstimate(provider, contract, method, params) {
// Get current gas price from provider
const gasPrice = await provider.getGasPrice();
// Estimate gas for the specific transaction
const gasEstimate = await contract.estimateGas[method](...params);
// Add 20% buffer for safety
const gasLimit = gasEstimate.mul(120).div(100);
return {
gasLimit,
gasPrice
};
}
// Example usage in transaction
async function executeTransaction(chainId, signer, contract, method, params) {
const gasSettings = await getGasEstimate(signer.provider, contract, method, params);
const tx = await contract[method](...params, gasSettings);
return await tx.wait();
}
Alternative: Using Wallet Kit or Other Gas APIs
// Using a wallet kit's gas estimation
async function getGasWithWalletKit(walletKit, chainId, txData) {
const gasEstimate = await walletKit.estimateGas({
chainId,
to: txData.to,
data: txData.data
});
return {
gasLimit: gasEstimate.gasLimit,
gasPrice: gasEstimate.gasPrice
};
}
// Using Alchemy Gas Manager API
async function getGasWithAlchemyAPI(alchemyKey, chainId, txData) {
const response = await fetch(`https://gas-api.metaswap.codefi.network/networks/${chainId}/suggestedGasFees`, {
headers: { 'Authorization': `Bearer ${alchemyKey}` }
});
const gasData = await response.json();
return {
gasLimit: await estimateGasLimit(txData),
gasPrice: ethers.utils.parseUnits(gasData.medium.suggestedMaxFeePerGas, 'gwei')
};
}
Frontend/User Wallet Applications¶
For user-facing applications where users connect their wallets, always let the user's wallet handle gas estimation:
// ✅ Correct: Let wallet handle gas estimation
async function redeemCardUserWallet(provider, cardId, cardSecret, recipientAddress) {
const signer = provider.getSigner();
const contract = new ethers.Contract(UNIVOUCHER_ADDRESS, ABI, signer);
// Don't specify gas settings - let wallet estimate
const tx = await contract.redeemCard(cardId, recipientAddress, signature, partnerAddress);
return await tx.wait();
}
// ❌ Incorrect: Don't override wallet gas estimation
async function redeemCardWithManualGas(provider, cardId, cardSecret, recipientAddress) {
const signer = provider.getSigner();
const contract = new ethers.Contract(UNIVOUCHER_ADDRESS, ABI, signer);
// This overrides the user's wallet settings and may cause failures
const tx = await contract.redeemCard(cardId, recipientAddress, signature, partnerAddress, {
gasLimit: 150000,
gasPrice: ethers.utils.parseUnits('20', 'gwei')
});
return await tx.wait();
}
Why Let Wallets Handle Gas: - Wallets have real-time network awareness - Users can adjust gas based on urgency - Wallets handle EIP-1559 vs legacy gas pricing automatically - Better UX - users control their transaction costs - Prevents failed transactions due to outdated gas estimates
Step 1: Set Up Contract Interface¶
This step initializes the contract interface using ethers.js. We define the minimal ABI (Application Binary Interface) containing only the essential functions needed for integration, and set up a helper function to create contract instances.
import { ethers } from 'ethers';
// UniVoucher ABI (minimal version with essential functions)
const UniVoucherABI = [
"function depositETH(address slotId, uint256 amount, string memory message, string memory encryptedPrivateKey) external payable",
"function depositERC20(address slotId, address tokenAddress, uint256 amount, string memory message, string memory encryptedPrivateKey) external",
"function redeemCard(string memory cardId, address payable to, bytes memory signature, address payable partner) external",
"function cancelCard(string memory cardId) external",
"function getCardData(string memory cardId) external view returns (bool active, address tokenAddress, uint256 tokenAmount, uint256 feePaid, address creator, string memory message, string memory encryptedPrivateKey, address slotId, uint256 timestamp, address redeemedBy, address cancelledBy, address partnerAddress, uint256 finalizedTimestamp)",
"function calculateFee(uint256 amount) external view returns (uint256)",
"function isCardActive(string memory cardId) external view returns (bool)",
"event CardCreated(string cardId, address indexed slotId, address indexed creator, address tokenAddress, uint256 tokenAmount, uint256 feePaid, string message, string encryptedPrivateKey, uint256 timestamp)",
"event CardRedeemed(string cardId, address indexed slotId, address indexed to, address tokenAddress, uint256 amount, address indexed partner, uint256 timestamp)"
];
// Contract address (same on all networks)
const UNIVOUCHER_ADDRESS = '0x51553818203e38ce0E78e4dA05C07ac779ec5b58';
// Create contract instance
function getUniVoucherContract(providerOrSigner) {
return new ethers.Contract(UNIVOUCHER_ADDRESS, UniVoucherABI, providerOrSigner);
}
// Reliable ethers.js loading for web applications
function loadEthers() {
return new Promise((resolve, reject) => {
if (typeof ethers !== 'undefined') {
resolve(ethers);
return;
}
const script = document.createElement('script');
script.src = 'https://unpkg.com/[email protected]/dist/ethers.umd.min.js';
script.onload = () => {
if (typeof ethers !== 'undefined') {
resolve(ethers);
} else {
reject(new Error('Ethers.js failed to load'));
}
};
script.onerror = () => reject(new Error('Failed to load ethers.js'));
document.head.appendChild(script);
});
}
Smart Contract Details
For complete details on the smart contract logic and functions, see the Smart Contract documentation.
Step 2: Implement Cryptographic Helper Functions¶
These cryptographic functions handle the secure generation and management of card secrets. The implementation differs between browser and Node.js environments.
Browser Implementation (Web Crypto API)¶
// Generate a friendly secret
function generateFriendlySecret() {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = '';
// Generate 4 groups of 5 characters
for (let group = 0; group < 4; group++) {
for (let i = 0; i < 5; i++) {
const randomIndex = crypto.getRandomValues(new Uint8Array(1))[0] % charset.length;
result += charset[randomIndex];
}
if (group < 3) {
result += '-';
}
}
return result;
}
// Encrypt a private key using PBKDF2 and AES-GCM (Browser)
async function encryptPrivateKey(privateKey, friendlySecret) {
// Remove hyphens from the secret
const normalizedSecret = friendlySecret.replace(/-/g, '');
// Generate salt and IV
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
// Import the secret as key material
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(normalizedSecret),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
);
// Derive a key using PBKDF2
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 310000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
);
// Encrypt the private key
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
key,
new TextEncoder().encode(privateKey)
);
// Format the result as JSON
return JSON.stringify({
salt: Array.from(new Uint8Array(salt)).map(b => b.toString(16).padStart(2, '0')).join(''),
iv: Array.from(new Uint8Array(iv)).map(b => b.toString(16).padStart(2, '0')).join(''),
ciphertext: btoa(String.fromCharCode(...new Uint8Array(encryptedData)))
});
}
// Decrypt an encrypted private key (Browser)
async function decryptPrivateKey(encryptedData, friendlySecret) {
try {
// Parse the encrypted data
const data = JSON.parse(encryptedData);
// Convert formats
const normalizedSecret = friendlySecret.replace(/-/g, '');
const salt = new Uint8Array(data.salt.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const iv = new Uint8Array(data.iv.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const ciphertext = Uint8Array.from(atob(data.ciphertext), c => c.charCodeAt(0));
// Import the secret as key material
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(normalizedSecret),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
);
// Derive the key
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 310000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
// Decrypt the private key
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
key,
ciphertext
);
// Return the decrypted private key
return new TextDecoder().decode(decrypted);
} catch (error) {
throw new Error("Invalid card secret or corrupted data");
}
}
Node.js Implementation¶
const crypto = require('crypto');
// Decrypt an encrypted private key (Node.js)
async function decryptPrivateKeyNodeJS(encryptedData, cardSecret) {
try {
// Parse the encrypted data JSON
const data = JSON.parse(encryptedData);
// Normalize the secret (remove hyphens)
const normalizedSecret = cardSecret.replace(/-/g, '');
// Convert hex strings to Buffers
const salt = Buffer.from(data.salt, 'hex');
const iv = Buffer.from(data.iv, 'hex');
const ciphertext = Buffer.from(data.ciphertext, 'base64');
// Derive key using PBKDF2 (same parameters as Web Crypto API)
const key = crypto.pbkdf2Sync(normalizedSecret, salt, 310000, 32, 'sha256');
// Handle AES-GCM decryption in Node.js
const authTagLength = 16;
if (ciphertext.length < authTagLength) {
throw new Error('Invalid ciphertext length');
}
const authTag = ciphertext.slice(-authTagLength);
const encryptedContent = ciphertext.slice(0, -authTagLength);
// Use Node.js crypto API for AES-GCM
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
// Decrypt
let decrypted = decipher.update(encryptedContent);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString('utf8');
} catch (error) {
console.error('Decryption error:', error.message);
throw new Error("Invalid card secret");
}
}
Signature Verification
For details on how card signatures are verified during redemption, see the Signature Verification documentation.
Step 3: Token Information Retrieval¶
Proper token display requires fetching actual token metadata from contracts rather than using default values.
// Get token information (symbol and decimals) from contract
async function getTokenInfo(tokenAddress, chainId, provider) {
if (tokenAddress === '0x0000000000000000000000000000000000000000') {
// Native token - get from chain info
const chainInfo = await getChainInfo(chainId);
return {
symbol: chainInfo?.nativeCurrencySymbol || 'ETH',
decimals: 18
};
} else {
try {
// ERC-20 ABI for symbol and decimals
const erc20Abi = [
'function symbol() view returns (string)',
'function decimals() view returns (uint8)'
];
const contract = new ethers.Contract(tokenAddress, erc20Abi, provider);
// Get symbol and decimals with timeout
const [symbol, decimals] = await Promise.race([
Promise.all([contract.symbol(), contract.decimals()]),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
)
]);
return { symbol, decimals };
} catch (error) {
console.warn('Failed to get token info:', error);
// Fallback to generic values
return { symbol: 'TOKEN', decimals: 18 };
}
}
}
// Format token amount using correct decimals
async function formatTokenAmount(amount, tokenAddress, chainId, provider) {
try {
const tokenInfo = await getTokenInfo(tokenAddress, chainId, provider);
const formattedAmount = (parseFloat(amount) / Math.pow(10, tokenInfo.decimals));
// Remove trailing zeros and unnecessary decimals
const formatted = formattedAmount % 1 === 0 ?
formattedAmount.toString() :
formattedAmount.toFixed(6).replace(/\.?0+$/, '');
return `${formatted} ${tokenInfo.symbol}`;
} catch (error) {
return `${amount} TOKEN`;
}
}
// Get chain information from UniVoucher API
async function getChainInfo(chainId) {
try {
const response = await fetch('https://api.univoucher.com/v1/chains');
const data = await response.json();
return data.chains?.find(chain => chain.id === chainId);
} catch (error) {
console.error('Failed to fetch chain info:', error);
return null;
}
}
Step 4: Implement Card Creation¶
This function handles the creation of new gift cards, supporting both native tokens (ETH, BNB, etc.) and ERC-20 tokens. It generates a random wallet for the card, encrypts the private key with a user-friendly secret, and handles the blockchain transactions including fee calculations and token approvals.
async function createCard(provider, tokenAddress, amount, message = "") {
try {
const signer = provider.getSigner();
const contract = getUniVoucherContract(signer);
// Generate a random wallet
const wallet = ethers.Wallet.createRandom();
const slotId = wallet.address;
const privateKey = wallet.privateKey;
// Generate a friendly secret and encrypt the private key
const friendlySecret = generateFriendlySecret();
const encryptedPrivateKey = await encryptPrivateKey(privateKey, friendlySecret);
let tx, receipt;
if (tokenAddress === ethers.constants.AddressZero) {
// Native token (ETH, BNB, AVAX, etc.)
const amountWei = ethers.utils.parseEther(amount.toString());
const feeWei = await contract.calculateFee(amountWei);
const totalValue = amountWei.add(feeWei);
tx = await contract.depositETH(slotId, amountWei, message, encryptedPrivateKey, {
value: totalValue
});
} else {
// ERC-20 token
const tokenContract = new ethers.Contract(
tokenAddress,
["function decimals() view returns (uint8)", "function approve(address, uint256) returns (bool)"],
signer
);
// Get token decimals
let decimals;
try {
decimals = await tokenContract.decimals();
} catch (err) {
decimals = 18; // Default
}
// Parse amount with proper decimals
const amountInTokenUnits = ethers.utils.parseUnits(amount.toString(), decimals);
const feeInTokenUnits = await contract.calculateFee(amountInTokenUnits);
const totalTokens = amountInTokenUnits.add(feeInTokenUnits);
// Approve tokens for the contract
const approveTx = await tokenContract.approve(contract.address, totalTokens);
await approveTx.wait();
// Create ERC-20 card
tx = await contract.depositERC20(slotId, tokenAddress, amountInTokenUnits, message, encryptedPrivateKey);
}
// Wait for transaction to be mined
receipt = await tx.wait();
// Find the CardCreated event
const event = receipt.events.find(e => e.event === 'CardCreated' && e.args.slotId === slotId);
if (!event) {
throw new Error("Card creation event not found");
}
// Return card details
return {
cardId: event.args.cardId,
cardSecret: friendlySecret,
txHash: receipt.transactionHash,
amount,
tokenAddress,
message
};
} catch (error) {
console.error("Error creating card:", error);
throw error;
}
}
Step 5: Implement Card Redemption¶
This function handles the redemption process for gift cards. It decrypts the private key using the provided card secret, creates a cryptographic signature to prove ownership, and calls the smart contract to transfer the funds to the recipient address. Optionally, it can specify a partner address to receive 1% of the card amount as a partner fee.
async function redeemCard(provider, cardId, cardSecret, recipientAddress = null, partnerAddress = null) {
try {
const signer = provider.getSigner();
const contract = getUniVoucherContract(signer);
// If no recipient provided, use the connected wallet
if (!recipientAddress) {
recipientAddress = await signer.getAddress();
}
// Get card data
const cardData = await contract.getCardData(cardId);
if (!cardData[0]) { // Not active
throw new Error("This card is not active");
}
// Get encrypted private key from card data
const encryptedPrivateKey = cardData[6];
// Decrypt private key
const privateKey = await decryptPrivateKey(encryptedPrivateKey, cardSecret);
// Create wallet from private key
const wallet = new ethers.Wallet(privateKey);
// Create message hash
const messageHash = ethers.utils.solidityKeccak256(
["string", "string", "string", "address"],
["Redeem card:", cardId, "to:", recipientAddress]
);
// Sign the hash
const arrayifiedHash = ethers.utils.arrayify(messageHash);
const signature = await wallet.signMessage(arrayifiedHash);
// Call the redeem function with partner address (use address(0) if no partner)
const partner = partnerAddress || ethers.constants.AddressZero;
const tx = await contract.redeemCard(cardId, recipientAddress, signature, partner);
const receipt = await tx.wait();
return {
success: true,
txHash: receipt.transactionHash,
recipientAddress,
partnerAddress: partnerAddress
};
} catch (error) {
console.error("Error redeeming card:", error);
throw error;
}
}
// Alternative: Redeem with pre-generated signature (for gasless integrations)
async function redeemCardWithSignature(provider, cardId, signature, recipientAddress, partnerAddress = null) {
try {
const signer = provider.getSigner();
const contract = getUniVoucherContract(signer);
// Get card data to verify it's active
const cardData = await contract.getCardData(cardId);
if (!cardData[0]) {
throw new Error("This card is not active");
}
// Execute redemption with provided signature
const partner = partnerAddress || ethers.constants.AddressZero;
const tx = await contract.redeemCard(cardId, recipientAddress, signature, partner);
const receipt = await tx.wait();
return {
success: true,
txHash: receipt.transactionHash,
recipientAddress,
partnerAddress
};
} catch (error) {
console.error("Error redeeming card:", error);
throw error;
}
}
Step 6: Implement Card Cancellation¶
This function allows card creators to cancel active cards and reclaim their funds. It verifies the card is still active before attempting cancellation and handles the blockchain transaction process.
async function cancelCard(provider, cardId) {
try {
const signer = provider.getSigner();
const contract = getUniVoucherContract(signer);
// Check if card is active
const isActive = await contract.isCardActive(cardId);
if (!isActive) {
throw new Error("Card is not active");
}
// Cancel the card
const tx = await contract.cancelCard(cardId);
const receipt = await tx.wait();
return {
success: true,
txHash: receipt.transactionHash
};
} catch (error) {
console.error("Error cancelling card:", error);
throw error;
}
}
Step 7: Implement Card Data Retrieval¶
This function retrieves detailed information about a specific card from the blockchain. It formats the raw contract data into a more developer-friendly structure that can be easily used in applications.
// Get card details from smart contract
async function getCardDetails(provider, cardId) {
try {
const contract = getUniVoucherContract(provider);
// Get card data
const data = await contract.getCardData(cardId);
// Format the result
return {
active: data[0],
tokenAddress: data[1],
tokenAmount: data[2].toString(),
feePaid: data[3].toString(),
creator: data[4],
message: data[5],
encryptedPrivateKey: data[6],
slotId: data[7],
timestamp: data[8].toNumber(),
redeemedBy: data[9],
cancelledBy: data[10],
partnerAddress: data[11],
finalizedTimestamp: data[12].toNumber()
};
} catch (error) {
console.error("Error fetching card data:", error);
throw error;
}
}
// Get card details from UniVoucher API (alternative method)
async function getCardDetailsFromAPI(cardId) {
try {
const response = await fetch(`https://api.univoucher.com/v1/cards/single?id=${cardId}`, {
timeout: 10000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
if (!response.ok) {
if (response.status === 404) {
throw new Error('Card not found');
}
if (response.status === 400) {
throw new Error('Invalid card ID format');
}
throw new Error(`API error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching card from API:", error);
throw error;
}
}
// Format card data for display with proper token information
async function formatCardDataForDisplay(cardData, provider) {
const tokenInfo = await getTokenInfo(cardData.tokenAddress, cardData.chainId, provider);
const formattedAmount = await formatTokenAmount(
cardData.tokenAmount,
cardData.tokenAddress,
cardData.chainId,
provider
);
return {
...cardData,
formattedAmount,
tokenSymbol: tokenInfo.symbol,
statusText: cardData.active ? 'Active' :
cardData.redeemedBy ? 'Redeemed' : 'Cancelled'
};
}
Partner Integration¶
Earning Partner Fees¶
Partners can earn a 1% fee from card redemptions by providing their address during the redemption process:
// Example: Redeem a card with partner fee
async function redeemWithPartnerFee(provider, cardId, cardSecret, partnerAddress) {
const recipientAddress = await provider.getSigner().getAddress();
return await redeemCard(provider, cardId, cardSecret, recipientAddress, partnerAddress);
}
// Example: Check if a card was redeemed through a partner
async function checkPartnerRedemption(provider, cardId) {
const cardDetails = await getCardDetails(provider, cardId);
if (cardDetails.partnerAddress && cardDetails.partnerAddress !== ethers.constants.AddressZero) {
console.log(`Card was redeemed through partner: ${cardDetails.partnerAddress}`);
return cardDetails.partnerAddress;
}
return null;
}
Partner Fee Details¶
- Fee Amount: 1% of the card amount (minimum 1 wei)
- Fee Source: Deducted from the card amount
- Payment: Sent directly to partner address during redemption
- Recipient: Gets the remaining 99% of the card amount
For example, a 100 USDC card redeemed through a partner: - Partner receives: 1 USDC - Recipient receives: 99 USDC
API Integration Examples¶
Using UniVoucher API for Card Data¶
``javascript
// Check card status without blockchain interaction
async function checkCardStatus(cardId) {
try {
const response = await fetch(
https://api.univoucher.com/v1/cards/single?id=${cardId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const cardData = await response.json();
return {
exists: true,
active: cardData.active,
status: cardData.status,
chainId: cardData.chainId,
tokenAddress: cardData.tokenAddress,
tokenAmount: cardData.tokenAmount
};
} catch (error) { if (error.message.includes('404')) { return { exists: false }; } throw error; } }
// Get supported chains async function getSupportedChains() { try { const response = await fetch('https://api.univoucher.com/v1/chains'); const data = await response.json(); return data.chains; } catch (error) { console.error('Failed to fetch supported chains:', error); return []; } }
// Get current fees for a chain
async function getChainFees(chainId) {
try {
const response = await fetch(https://api.univoucher.com/v1/fees?chainId=${chainId}
);
const data = await response.json();
return data.feePercentage;
} catch (error) {
console.error('Failed to fetch chain fees:', error);
return null;
}
}