cpay-kit
Version:
Composable Pay kit for merchants and users (Avalanche Fuji USDC payments)
426 lines (416 loc) • 11.7 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
decodePayment: () => decodePayment,
encodePayment: () => encodePayment,
formatAmount: () => formatAmount,
generateQrAndListen: () => generateQrAndListen,
generateQrFromPayload: () => generateQrFromPayload,
parseQRInput: () => parseQRInput,
scanDecodeAndPay: () => scanDecodeAndPay,
scanQr: () => scanQr,
validateEVMAddress: () => validateEVMAddress,
validatePaymentId: () => validatePaymentId
});
module.exports = __toCommonJS(index_exports);
// src/qr.ts
var import_html5_qrcode = require("html5-qrcode");
var import_qrcode = __toESM(require("qrcode"), 1);
async function scanQr(elementId, onScan, { fps = 10, qrbox = { width: 250, height: 250 } } = {}) {
const html5QrCode = new import_html5_qrcode.Html5Qrcode(elementId);
let isRunning = false;
try {
const devices = await import_html5_qrcode.Html5Qrcode.getCameras();
if (!devices || devices.length === 0) {
throw new Error("No cameras found");
}
const cameraId = devices[0]?.id;
if (!cameraId) {
throw new Error("Camera device ID not found");
}
await html5QrCode.start(
cameraId,
{ fps, qrbox },
(decodedText) => onScan(decodedText),
(error) => console.log("Scan error:", error)
);
isRunning = true;
} catch (err) {
console.error("QR Scanner failed:", err);
}
return {
stop: async () => {
if (isRunning) {
await html5QrCode.stop();
isRunning = false;
}
}
};
}
async function generateQrFromPayload(payload) {
try {
const encoded = btoa(JSON.stringify(payload));
const dataUrl = await import_qrcode.default.toDataURL(encoded, {
errorCorrectionLevel: "H",
width: 256,
margin: 4
});
return dataUrl;
} catch (err) {
throw new Error(
`Failed to generate QR: ${err instanceof Error ? err.message : "Unknown"}`
);
}
}
// src/merchant.ts
var import_ethers = require("ethers");
// src/abi/StablecoinMerchant.json
var StablecoinMerchant_default = [
{
inputs: [
{
internalType: "address",
name: "_merchant",
type: "address"
},
{
internalType: "address[]",
name: "allowedTokens",
type: "address[]"
}
],
stateMutability: "nonpayable",
type: "constructor"
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "bytes32",
name: "paymentId",
type: "bytes32"
},
{
indexed: true,
internalType: "address",
name: "payer",
type: "address"
},
{
indexed: true,
internalType: "address",
name: "token",
type: "address"
},
{
indexed: false,
internalType: "uint256",
name: "amount",
type: "uint256"
}
],
name: "StablecoinPaymentReceived",
type: "event"
},
{
inputs: [],
name: "merchant",
outputs: [
{
internalType: "address",
name: "",
type: "address"
}
],
stateMutability: "view",
type: "function"
},
{
inputs: [
{
internalType: "bytes32",
name: "paymentId",
type: "bytes32"
},
{
internalType: "address",
name: "token",
type: "address"
},
{
internalType: "uint256",
name: "amount",
type: "uint256"
}
],
name: "pay",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "address",
name: "newMerchant",
type: "address"
}
],
name: "setMerchant",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "address",
name: "token",
type: "address"
},
{
internalType: "bool",
name: "allowed",
type: "bool"
}
],
name: "setStablecoin",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "address",
name: "",
type: "address"
}
],
name: "stablecoins",
outputs: [
{
internalType: "bool",
name: "",
type: "bool"
}
],
stateMutability: "view",
type: "function"
}
];
// src/merchant.ts
var CONTRACT_ADDRESS = "0x113efaC740bCDF8BfD033a1C6784a16Fa91Db01e";
async function pollForReceipt(provider, txHash, maxAttempts = 30, interval = 2e3) {
let attempts = 0;
while (attempts < maxAttempts) {
const receipt = await provider.getTransactionReceipt(txHash);
if (receipt) return receipt;
attempts++;
await new Promise((res) => setTimeout(res, interval));
}
throw new Error("Transaction receipt not found after polling");
}
async function generateQrAndListen(payload, provider) {
const qrDataUrl = await generateQrFromPayload(payload);
const contract = new import_ethers.ethers.Contract(
CONTRACT_ADDRESS,
StablecoinMerchant_default,
provider
);
return new Promise((resolve, reject) => {
let timeout;
if (!contract.filters || !contract.filters.StablecoinPaymentReceived) {
reject(new Error("StablecoinPaymentReceived event not found in contract ABI"));
return;
}
const listener = async (...args) => {
try {
const event = args[args.length - 1];
const eventArgs = event.args;
console.log("Event received:", {
paymentId: eventArgs[0],
payer: eventArgs[11],
token: eventArgs[12],
amount: eventArgs[13]
});
if (eventArgs && eventArgs === payload.pid) {
console.log("Payment ID matched!", payload.pid);
clearTimeout(timeout);
contract.off("StablecoinPaymentReceived", listener);
try {
const receipt = await pollForReceipt(provider, event.transactionHash);
resolve({ qrDataUrl, event, eventArgs, receipt });
} catch (err) {
reject(err);
}
} else {
console.log("Payment ID mismatch:", {
received: eventArgs[0],
expected: payload.pid
});
}
} catch (error) {
console.error("Error processing event:", error);
}
};
contract.on?.("StablecoinPaymentReceived", listener);
console.log(`Listening for StablecoinPaymentReceived events on contract: ${CONTRACT_ADDRESS}`);
console.log(`Waiting for payment ID: ${payload.pid}`);
timeout = setTimeout(() => {
contract.off?.("StablecoinPaymentReceived", listener);
reject(new Error("Payment not received within timeout (5 minutes)"));
}, 5 * 60 * 1e3);
});
}
// src/utils.ts
var import_ethers2 = require("ethers");
function encodePayment(payload) {
const compressedStr = `${payload.paymentId}|${payload.merchantAddress}|${payload.amount}`;
return btoa(compressedStr);
}
function decodePayment(base64String) {
try {
const decoded = Buffer.from(base64String, "base64").toString();
const parts = decoded.split("|");
if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
throw new Error("Invalid compressed payment format");
}
return {
pid: parts[0],
ma: parts[1],
a: parts[2]
};
} catch (error) {
throw new Error("Invalid compressed payment string: " + error.message);
}
}
function validateEVMAddress(address) {
return (0, import_ethers2.isAddress)(address);
}
function validatePaymentId(paymentId) {
return /^0x[a-fA-F0-9]{64}$/.test(paymentId);
}
function formatAmount(amount, decimals = 18) {
const num = BigInt(amount);
const divisor = BigInt(10 ** decimals);
const quotient = num / divisor;
const remainder = num % divisor;
if (remainder === 0n) {
return quotient.toString();
}
const remainderStr = remainder.toString().padStart(decimals, "0");
return `${quotient}.${remainderStr.replace(/0+$/, "")}`;
}
// src/payment.ts
var import_ethers3 = require("ethers");
async function payFromQR(payload, provider, account) {
const signer = await provider.getSigner();
const contract = new import_ethers3.ethers.Contract(
payload.ma,
StablecoinMerchant_default,
signer
);
let paymentId = payload.pid;
if (typeof paymentId === "string") {
paymentId = import_ethers3.ethers.encodeBytes32String(paymentId);
}
const tx = await contract.pay(paymentId, payload.a);
return await tx.wait();
}
// src/user.ts
async function scanDecodeAndPay(provider, account) {
return new Promise(async (resolve, reject) => {
let scanner;
try {
scanner = await scanQr("reader", async (scannedString) => {
try {
const payload = decodePayment(scannedString);
console.log("Decoded PaymentPayload:", payload);
const receipt = await payFromQR(payload, provider, account);
console.log("Payment successful. Tx receipt:", receipt);
scanner.stop();
resolve(payload);
} catch (err) {
console.error("Failed to decode QR or execute payment:", err);
scanner?.stop();
reject(err);
}
});
} catch (err) {
console.error("Failed to start QR scanner:", err);
reject(err);
}
});
}
// src/qrInput.ts
function parseQRInput(embedding) {
try {
const rawString = extractStringFromEmbedding(embedding);
if (!rawString) {
return {
success: false,
error: "Could not extract string from QR embedding"
};
}
const payload = decodePayment(rawString);
return {
success: true,
payload,
rawString
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to parse QR input"
};
}
}
function extractStringFromEmbedding(embedding) {
if (embedding.startsWith("data:") || embedding.length > 100) {
return embedding;
}
return null;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
decodePayment,
encodePayment,
formatAmount,
generateQrAndListen,
generateQrFromPayload,
parseQRInput,
scanDecodeAndPay,
scanQr,
validateEVMAddress,
validatePaymentId
});