stacks-pay
Version:
A Payment Request Standard for Stacks Blockchain Payments
182 lines (181 loc) • 6.57 kB
JavaScript
;
// src/index.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeStacksPayURL = encodeStacksPayURL;
exports.decodeStacksPayURL = decodeStacksPayURL;
const utils_1 = require("./utils");
// Operation parameters configuration
const operationParameters = {
support: {
required: ["operation", "recipient"],
optional: ["token", "description", "expiresAt", "memo"],
ignore: [
"amount",
"contractName",
"dueDate",
"functionName",
"invoiceNumber",
],
},
invoice: {
required: ["operation", "recipient", "token", "amount"],
optional: ["description", "expiresAt", "invoiceNumber", "dueDate", "memo"],
ignore: ["contractName", "functionName"],
},
mint: {
required: ["operation", "contractName", "functionName", "token", "amount"],
optional: [
"recipient",
"description",
"expiresAt",
"invoiceNumber",
"memo",
],
ignore: ["dueDate"],
},
};
// Validate parameters based on operation
function validateParameters(params) {
const opType = params.operation;
const opConfig = operationParameters[opType];
if (!opConfig) {
if (!params.operation.startsWith("custom-")) {
throw new Error("Unknown operation type");
}
// For custom operations, no specific validation is enforced
return;
}
// Validate required parameters
for (const param of opConfig.required) {
if (!(param in params) ||
params[param] === undefined) {
throw new Error(`Parameter '${param}' is required for operation '${opType}'`);
}
}
// Validate and sanitize parameters
for (const key in params) {
const value = params[key];
if (value === undefined || value === "")
continue;
if (opConfig.ignore.includes(key)) {
// Ignore this parameter
delete params[key];
}
else if (![...opConfig.required, ...opConfig.optional].includes(key)) {
throw new Error(`Parameter '${key}' is not allowed for operation '${opType}'`);
}
else {
// Validate individual parameters
switch (key) {
case "recipient":
if (!(0, utils_1.validateStacksAddress)(value)) {
throw new Error("Invalid Stacks address for recipient");
}
break;
case "token":
if (!(0, utils_1.isValidToken)(value)) {
throw new Error("Invalid token format");
}
break;
case "amount":
if (!(0, utils_1.isValidAmount)(value)) {
throw new Error("Invalid amount format");
}
break;
case "expiresAt":
case "dueDate":
if (!(0, utils_1.isValidISODate)(value)) {
throw new Error(`Invalid date format for '${key}'`);
}
break;
case "contractName":
if (opType === "mint" && value.length === 0) {
throw new Error("Contract name is required for mint operation");
}
break;
case "functionName":
if (opType === "mint" && value.length === 0) {
throw new Error("Function name is required for mint operation");
}
break;
// Additional validations can be added here
}
}
}
}
// Encode Stacks Pay URL
function encodeStacksPayURL(params) {
try {
// Validate and sanitize parameters
validateParameters(params);
const operation = params.operation;
const queryParams = new URLSearchParams();
let allowedParams = [];
if (operationParameters[operation]) {
allowedParams = [
...operationParameters[operation].required,
...operationParameters[operation].optional,
];
}
else if (operation.startsWith("custom-")) {
// For custom operations, include all parameters except 'operation'
allowedParams = Object.keys(params).filter((k) => k !== "operation");
}
else {
throw new Error("Unknown operation type");
}
for (const key of allowedParams) {
const value = params[key];
if (value !== undefined && value !== "") {
queryParams.append(key, value);
}
}
const url = `${operation}?${queryParams.toString()}`;
// Bech32m encode the URL with HRP 'stx'
const bech32mEncodedURL = (0, utils_1.bech32mEncode)(url);
return `web+stx:${bech32mEncodedURL}`;
}
catch (error) {
console.error("Error encoding Stacks Pay URL:", error.message);
throw error;
}
}
// Decode Stacks Pay URL
function decodeStacksPayURL(fullURL) {
try {
if (!fullURL.startsWith("web+stx:")) {
throw new Error("Invalid protocol in URL");
}
const bech32mEncodedURL = fullURL.replace("web+stx:", "");
const url = (0, utils_1.bech32mDecode)(bech32mEncodedURL);
const questionMarkIndex = url.indexOf("?");
let operation;
let queryString;
if (questionMarkIndex >= 0) {
operation = url.substring(0, questionMarkIndex);
queryString = url.substring(questionMarkIndex + 1);
}
else {
operation = url;
queryString = "";
}
const queryParams = new URLSearchParams(queryString);
const queryEntries = Object.fromEntries(queryParams.entries());
// Construct params with operation included
const params = Object.assign({ operation }, queryEntries);
// Remove ignored parameters
const opConfig = operationParameters[operation];
if (opConfig) {
for (const param of opConfig.ignore) {
delete params[param];
}
}
// Validate parameters after decoding
validateParameters(params);
return params;
}
catch (error) {
console.error("Error decoding Stacks Pay URL:", error.message);
throw error;
}
}