๐ฑ Reduced Transactions & ErgoPay โ
Mobile wallet integration with reduced transactions
Reduced transactions (ReducedTx) are a special transaction format that enables ErgoPay - Ergo's protocol for mobile wallet signing.
What is a Reduced Transaction? โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UNSIGNED TX vs REDUCED TX โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ UNSIGNED TRANSACTION REDUCED TRANSACTION โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โข Full input boxes โ โ โข Minimal data โ โ
โ โ โข All registers โ โโโโโถ โ โข Hints for signing โ โ
โ โ โข Complete ErgoTree โ reduce โ โข Smaller payload โ โ
โ โ โข Heavy payload โ โ โข Mobile-friendly โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Best for: Desktop apps Best for: Mobile wallets โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโWhy Use Reduced Transactions? โ
| Feature | Benefit |
|---|---|
| Smaller Size | Optimized for QR codes and mobile |
| ErgoPay Compatible | Works with Ergo mobile wallets |
| Offline Signing | Wallet doesn't need full box data |
| Better UX | Faster QR scanning and processing |
ErgoPay Flow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ERGOPAY FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 1. dApp builds transaction โ
โ โโโโโโโโโโโโ โ
โ โ dApp โ โ
โ โ Website โ โ
โ โโโโโโฌโโโโโโ โ
โ โ โ
โ 2. Reduce transaction โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโ 3. Generate ErgoPay URL โ
โ โ Reduced โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Tx โ โ โ
โ โโโโโโโโโโโโ โผ โ
โ โโโโโโโโโโโโโโ โ
โ โ ergopay:// โ โ
โ โ URL โ โ
โ 4. Display QR Code โโโโโโโโฌโโโโโโ โ
โ โโโโโโโโโโโโ โ โ
โ โ โโโโโโโโ โ โ โ
โ โ โ QR โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โโโโโโโโ โ โ
โ โโโโโโฌโโโโโโ โ
โ โ โ
โ 5. Scan with mobile wallet โ
โ โผ โ
โ โโโโโโโโโโโโ โ
โ โ ๐ฑ Ergo โ 6. Sign & Submit โ
โ โ Wallet โโโโโโโโโโโโโโโโโโโโโโโโโโถ Blockchain โ
โ โโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโCreating a Reduced Transaction โ
typescript
import {
TransactionBuilder,
OutputBuilder,
SAFE_MIN_BOX_VALUE
} from "@fleet-sdk/core";
// Step 1: Build unsigned transaction
async function buildTransaction(
inputs: Box<string>[],
recipient: string,
amount: string,
changeAddress: string
) {
const currentHeight = await getCurrentHeight();
const unsignedTx = new TransactionBuilder(currentHeight)
.from(inputs)
.to(
new OutputBuilder(amount, recipient)
)
.sendChangeTo(changeAddress)
.payFee("1100000")
.build();
return unsignedTx;
}
// Step 2: Reduce the transaction
async function reduceTransaction(unsignedTx: any) {
// The reduce operation creates hints for signing
// This requires access to the blockchain for box data
const reducedTx = await unsignedTx.reduce({
// Provide context for reduction
baseCost: 0,
initCost: 0
});
return reducedTx;
}ErgoPay URL Format โ
typescript
/**
* ErgoPay URL format:
* ergopay://<base64_encoded_reduced_tx>
*
* Or with server callback:
* ergopay://<server_url>/path/to/tx
*/
function createErgoPayUrl(reducedTx: ReducedTransaction): string {
// Serialize and encode
const serialized = reducedTx.toBytes();
const base64 = Buffer.from(serialized).toString('base64url');
return `ergopay://${base64}`;
}
// For larger transactions, use server callback
function createErgoPayCallbackUrl(txId: string): string {
return `ergopay://your-dapp.com/api/ergopay/${txId}`;
}Complete ErgoPay Integration โ
typescript
import {
TransactionBuilder,
OutputBuilder,
ErgoAddress
} from "@fleet-sdk/core";
import QRCode from "qrcode";
class ErgoPayService {
private apiUrl: string;
constructor(apiUrl: string = "https://api.ergoplatform.com") {
this.apiUrl = apiUrl;
}
/**
* Create ErgoPay payment request
*/
async createPaymentRequest(
recipientAddress: string,
amountNanoErg: string,
senderAddress: string
): Promise<{ qrCode: string; ergoPayUrl: string }> {
// 1. Fetch sender's UTXOs
const utxos = await this.fetchUtxos(senderAddress);
// 2. Get current height
const height = await this.getCurrentHeight();
// 3. Build transaction
const unsignedTx = new TransactionBuilder(height)
.from(utxos)
.to(
new OutputBuilder(amountNanoErg, recipientAddress)
)
.sendChangeTo(senderAddress)
.payFee("1100000")
.build();
// 4. Create ErgoPay URL (simplified - actual reduction is more complex)
const txData = JSON.stringify(unsignedTx);
const encoded = Buffer.from(txData).toString('base64url');
const ergoPayUrl = `ergopay://${encoded}`;
// 5. Generate QR code
const qrCode = await QRCode.toDataURL(ergoPayUrl, {
errorCorrectionLevel: 'M',
margin: 2,
width: 300
});
return { qrCode, ergoPayUrl };
}
/**
* Create ErgoPay URL with server callback
* (Recommended for larger transactions)
*/
async createCallbackRequest(
recipientAddress: string,
amountNanoErg: string
): Promise<{ qrCode: string; requestId: string }> {
// Store transaction details on server
const requestId = crypto.randomUUID();
// Server endpoint that returns reduced tx
const callbackUrl = `ergopay://your-dapp.com/api/ergopay/${requestId}`;
const qrCode = await QRCode.toDataURL(callbackUrl);
return { qrCode, requestId };
}
private async fetchUtxos(address: string) {
const response = await fetch(
`${this.apiUrl}/api/v1/boxes/unspent/byAddress/${address}`
);
const data = await response.json() as { items: any[] };
return data.items;
}
private async getCurrentHeight(): Promise<number> {
const response = await fetch(`${this.apiUrl}/api/v1/networkState`);
const data = await response.json() as { height: number };
return data.height;
}
}Server-Side ErgoPay Endpoint โ
typescript
// Express.js example
import express from "express";
const app = express();
// Store pending transactions
const pendingTxs = new Map<string, any>();
// Endpoint to create payment request
app.post("/api/ergopay/create", async (req, res) => {
const { recipient, amount } = req.body;
const requestId = crypto.randomUUID();
// Store transaction details
pendingTxs.set(requestId, {
recipient,
amount,
createdAt: Date.now()
});
// Return ErgoPay URL
res.json({
ergoPayUrl: `ergopay://your-dapp.com/api/ergopay/${requestId}`,
requestId
});
});
// ErgoPay callback - wallet fetches reduced tx from here
app.get("/api/ergopay/:requestId", async (req, res) => {
const { requestId } = req.params;
const { address } = req.query; // Wallet provides sender address
const txDetails = pendingTxs.get(requestId);
if (!txDetails) {
return res.status(404).json({ error: "Request not found" });
}
// Build reduced transaction for this address
const reducedTx = await buildReducedTx(
address as string,
txDetails.recipient,
txDetails.amount
);
// Return in ErgoPay format
res.json({
reducedTx: reducedTx.toBase64(),
address: txDetails.recipient,
message: `Payment of ${txDetails.amount} nanoERG`
});
});QR Code Component (React) โ
tsx
import React, { useEffect, useState } from "react";
import QRCode from "qrcode";
interface ErgoPayQRProps {
ergoPayUrl: string;
size?: number;
}
export const ErgoPayQR: React.FC<ErgoPayQRProps> = ({
ergoPayUrl,
size = 256
}) => {
const [qrDataUrl, setQrDataUrl] = useState<string>("");
useEffect(() => {
QRCode.toDataURL(ergoPayUrl, {
width: size,
margin: 2,
errorCorrectionLevel: "M",
color: {
dark: "#000000",
light: "#ffffff"
}
}).then(setQrDataUrl);
}, [ergoPayUrl, size]);
return (
<div className="ergopay-qr">
{qrDataUrl && (
<img
src={qrDataUrl}
alt="Scan with Ergo wallet"
width={size}
height={size}
/>
)}
<p>Scan with Ergo mobile wallet</p>
<a href={ergoPayUrl} className="mobile-link">
Open in wallet app
</a>
</div>
);
};Mobile Deep Linking โ
html
<!-- For mobile browsers, link directly to wallet -->
<a href="ergopay://...">
Pay with Ergo Wallet
</a>
<!-- With fallback for desktop -->
<script>
function openErgoPayOrShowQR(ergoPayUrl) {
// Try to open wallet
window.location.href = ergoPayUrl;
// If still here after 2 seconds, show QR
setTimeout(() => {
showQRCode(ergoPayUrl);
}, 2000);
}
</script>Error Handling โ
typescript
interface ErgoPayError {
code: string;
message: string;
}
async function handleErgoPayRequest(requestId: string) {
try {
const response = await fetch(`/api/ergopay/${requestId}`);
if (!response.ok) {
const error: ErgoPayError = await response.json();
switch (error.code) {
case "TX_EXPIRED":
return { error: "Transaction expired, please try again" };
case "INSUFFICIENT_FUNDS":
return { error: "Insufficient funds in wallet" };
case "INVALID_ADDRESS":
return { error: "Invalid wallet address" };
default:
return { error: error.message };
}
}
return await response.json();
} catch (e) {
return { error: "Network error, please try again" };
}
}Best Practices โ
1. URL Length Limits โ
typescript
// QR codes have size limits
// For large transactions, use callback URL instead of inline data
const MAX_INLINE_SIZE = 2000; // bytes
function createErgoPayUrl(reducedTx: ReducedTransaction): string {
const serialized = reducedTx.toBytes();
if (serialized.length > MAX_INLINE_SIZE) {
// Use server callback
return createCallbackUrl(reducedTx);
}
// Inline is OK
return `ergopay://${Buffer.from(serialized).toString('base64url')}`;
}2. Transaction Expiry โ
typescript
// Set expiry for pending transactions
const TX_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes
function isExpired(createdAt: number): boolean {
return Date.now() - createdAt > TX_EXPIRY_MS;
}3. User Feedback โ
typescript
// Show transaction status
type TxStatus = "pending" | "signed" | "submitted" | "confirmed" | "failed";
function renderStatus(status: TxStatus) {
switch (status) {
case "pending": return "โณ Waiting for signature...";
case "signed": return "โ๏ธ Signed, submitting...";
case "submitted": return "๐ค Submitted to network...";
case "confirmed": return "โ
Transaction confirmed!";
case "failed": return "โ Transaction failed";
}
}Test Example โ
typescript
import { describe, it, expect } from "vitest";
describe("ErgoPay", () => {
it("should create valid ergopay URL", () => {
const txData = { amount: "1000000000", recipient: "9f..." };
const encoded = Buffer.from(JSON.stringify(txData)).toString("base64url");
const url = `ergopay://${encoded}`;
expect(url).toMatch(/^ergopay:\/\//);
});
it("should use callback for large transactions", () => {
const largeData = "x".repeat(3000);
const shouldUseCallback = largeData.length > 2000;
expect(shouldUseCallback).toBe(true);
});
});Supported Wallets โ
| Wallet | ErgoPay Support |
|---|---|
| Ergo Wallet (Android) | โ Full support |
| Ergo Wallet (iOS) | โ Full support |
| Nautilus | โ Via dApp connector |
| SAFEW | โ ๏ธ Partial |
Next Steps โ
- Data Inputs - Reference boxes without spending
- Compile Time Constants - Configure contracts
- Examples - Complete working examples