UNPKG

cpay-kit

Version:

Composable Pay kit for merchants and users (Avalanche Fuji USDC payments)

426 lines (416 loc) 11.7 kB
"use strict"; 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 });