svm-pay
Version:
A payment solution for SVM networks (Solana, Sonic SVM, Eclipse, s00n)
248 lines • 9.74 kB
JavaScript
;
/**
* SVM-Pay Metadata Handler
*
* This file implements the metadata handling for SVM-Pay transactions.
* Metadata includes label, message, and memo fields that provide context for transactions.
* Includes real Solana memo instruction support.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeMetadata = encodeMetadata;
exports.decodeMetadata = decodeMetadata;
exports.validateMetadata = validateMetadata;
exports.createSolanaMemoInstruction = createSolanaMemoInstruction;
exports.parseSolanaMemoData = parseSolanaMemoData;
exports.createNetworkMemoInstruction = createNetworkMemoInstruction;
exports.optimizeMemoContent = optimizeMemoContent;
const spl_memo_1 = require("@solana/spl-memo");
/**
* Encode metadata for inclusion in a transaction
*
* @param label Optional label describing the payment source
* @param message Optional message describing the payment purpose
* @param memo Optional memo to be included in the transaction
* @returns Encoded metadata object
*/
function encodeMetadata(label, message, memo) {
return {
encodedLabel: label ? encodeURIComponent(label) : undefined,
encodedMessage: message ? encodeURIComponent(message) : undefined,
encodedMemo: memo ? encodeURIComponent(memo) : undefined,
};
}
/**
* Decode metadata from a transaction
*
* @param encodedLabel Encoded label from the transaction
* @param encodedMessage Encoded message from the transaction
* @param encodedMemo Encoded memo from the transaction
* @returns Decoded metadata object
*/
function decodeMetadata(encodedLabel, encodedMessage, encodedMemo) {
return {
label: encodedLabel ? decodeURIComponent(encodedLabel) : undefined,
message: encodedMessage ? decodeURIComponent(encodedMessage) : undefined,
memo: encodedMemo ? decodeURIComponent(encodedMemo) : undefined,
};
}
/**
* Validate metadata fields
*
* @param label Label to validate
* @param message Message to validate
* @param memo Memo to validate
* @returns Object with validation results
*/
function validateMetadata(label, message, memo) {
const errors = [];
// Check label length
if (label && label.length > 200) {
errors.push('Label must be 200 characters or less');
}
// Check message length
if (message && message.length > 500) {
errors.push('Message must be 500 characters or less');
}
// Check memo length (Solana memo program has specific limits)
if (memo && memo.length > 566) {
errors.push('Memo must be 566 characters or less (Solana memo program limit)');
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Create a Solana memo instruction from metadata
*
* @param metadata The metadata to include in the memo
* @param signers Optional array of additional signers for the memo
* @returns Solana memo instruction
*/
function createSolanaMemoInstruction(metadata, signers = []) {
// Combine metadata into a single memo string
const memoParts = [];
if (metadata.label) {
memoParts.push(`Label: ${metadata.label}`);
}
if (metadata.message) {
memoParts.push(`Message: ${metadata.message}`);
}
if (metadata.memo) {
memoParts.push(metadata.memo);
}
const memoText = memoParts.join(' | ');
if (!memoText) {
throw new Error('No metadata provided for memo instruction');
}
// Validate memo length
if (memoText.length > 566) {
throw new Error('Combined metadata exceeds Solana memo limit of 566 characters');
}
return (0, spl_memo_1.createMemoInstruction)(memoText, signers);
}
/**
* Parse memo data from a Solana memo instruction
*
* @param memoData The memo data from the instruction
* @returns Parsed metadata object
*/
function parseSolanaMemoData(memoData) {
const metadata = {};
// Split by separator and parse each part
const parts = memoData.split(' | ');
for (const part of parts) {
if (part.startsWith('Label: ')) {
metadata.label = part.substring(7);
}
else if (part.startsWith('Message: ')) {
metadata.message = part.substring(9);
}
else {
// Treat anything else as memo content
metadata.memo = part;
}
}
return metadata;
}
/**
* Create memo instructions for different network types
*
* @param networkType The network type (solana, sonic, eclipse, soon)
* @param metadata The metadata to include
* @param signers Optional additional signers
* @returns Network-specific memo instruction or data
*/
function createNetworkMemoInstruction(networkType, metadata, signers = []) {
switch (networkType.toLowerCase()) {
case 'solana':
return createSolanaMemoInstruction(metadata, signers);
case 'sonic':
// Sonic SVM uses similar memo structure to Solana
return createSolanaMemoInstruction(metadata, signers);
case 'eclipse':
// Eclipse uses Solana-compatible memo instructions
return createSolanaMemoInstruction(metadata, signers);
case 'soon':
// s00n network memo handling (similar to Solana for now)
return createSolanaMemoInstruction(metadata, signers);
default:
throw new Error(`Unsupported network type for memo: ${networkType}`);
}
}
/**
* Optimize memo content for size and readability with improved heuristics
*
* @param metadata The metadata to optimize
* @returns Optimized metadata that fits within limits
*/
function optimizeMemoContent(metadata) {
const maxLength = 566; // Solana memo limit
let wasOptimized = false;
// Calculate current length
const memoParts = [];
if (metadata.label)
memoParts.push(`Label: ${metadata.label}`);
if (metadata.message)
memoParts.push(`Message: ${metadata.message}`);
if (metadata.memo)
memoParts.push(metadata.memo);
const currentLength = memoParts.join(' | ').length;
if (currentLength <= maxLength) {
return metadata; // No optimization needed
}
console.warn(`⚠ Memo content exceeds ${maxLength} character limit (current: ${currentLength} chars)`);
console.warn(' Applying intelligent truncation to fit within Solana memo constraints...');
// Prioritize message over label, and preserve custom memo content when possible
const optimized = { ...metadata };
const separator = ' | ';
const separatorOverhead = separator.length * (memoParts.length - 1);
let availableLength = maxLength - separatorOverhead;
// Strategy: Allocate space dynamically based on content priority and length
// Priority: 1) Custom memo (most important), 2) Message (context), 3) Label (identification)
// Reserve space for custom memo first (it's usually the most important)
let memoSpace = 0;
if (optimized.memo) {
memoSpace = Math.min(optimized.memo.length, Math.floor(availableLength * 0.6)); // 60% for memo
availableLength -= memoSpace;
}
// Allocate remaining space between label and message (favor message for context)
const remainingSpace = availableLength;
const messageSpace = Math.min(optimized.message ? optimized.message.length : 0, Math.floor(remainingSpace * 0.7) // 70% of remaining for message
);
const labelSpace = remainingSpace - messageSpace;
// Apply truncation with ellipsis
if (optimized.label && `Label: ${optimized.label}`.length > labelSpace) {
const availableForLabel = Math.max(0, labelSpace - 10); // Reserve space for "Label: ..."
if (availableForLabel > 3) { // Only truncate if we have meaningful space
optimized.label = optimized.label.substring(0, availableForLabel - 3) + '...';
wasOptimized = true;
}
else {
// If no meaningful space, remove label entirely
optimized.label = undefined;
wasOptimized = true;
console.warn(' ⚠ Label removed due to space constraints');
}
}
if (optimized.message && `Message: ${optimized.message}`.length > messageSpace) {
const availableForMessage = Math.max(0, messageSpace - 12); // Reserve space for "Message: ..."
if (availableForMessage > 3) {
optimized.message = optimized.message.substring(0, availableForMessage - 3) + '...';
wasOptimized = true;
}
else {
optimized.message = undefined;
wasOptimized = true;
console.warn(' ⚠ Message removed due to space constraints');
}
}
if (optimized.memo && optimized.memo.length > memoSpace) {
if (memoSpace > 3) {
optimized.memo = optimized.memo.substring(0, memoSpace - 3) + '...';
wasOptimized = true;
}
else {
optimized.memo = undefined;
wasOptimized = true;
console.warn(' ⚠ Custom memo removed due to space constraints');
}
}
// Final verification
const finalParts = [];
if (optimized.label)
finalParts.push(`Label: ${optimized.label}`);
if (optimized.message)
finalParts.push(`Message: ${optimized.message}`);
if (optimized.memo)
finalParts.push(optimized.memo);
const finalLength = finalParts.join(' | ').length;
if (wasOptimized) {
console.warn(` ✓ Memo optimized: ${currentLength} → ${finalLength} characters`);
if (finalLength > maxLength) {
console.error(` ❌ ERROR: Optimization failed, still exceeds limit (${finalLength}/${maxLength})`);
}
}
return optimized;
}
//# sourceMappingURL=metadata.js.map