bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
224 lines (223 loc) • 7.63 kB
JavaScript
import { PROTOCOL_IDS } from "../../social/types/protocol.js";
// Protocol prefixes for MetaLens
export const METALENS_PROTOCOL = {
APP: "metalens",
VERSION: "1.0.0",
};
/**
* Build MAP protocol data for MetaLens comments
*/
export function buildMetaLensMapData(params) {
const mapData = {
app: METALENS_PROTOCOL.APP,
type: params.parentId ? "reply" : "comment",
context: params.context,
[params.context]: params.value,
timestamp: new Date().toISOString(),
};
// Add subcontext if provided
if (params.subcontext && params.subcontextValue) {
mapData.subcontext = params.subcontext;
mapData[params.subcontext] = params.subcontextValue;
}
// Add parent reference for replies
if (params.parentId) {
mapData.thread = params.parentId;
}
// Add tags if provided
if (params.tags && params.tags.length > 0) {
mapData.tags = params.tags.join(",");
}
// Add mentions if provided
if (params.mentions && params.mentions.length > 0) {
mapData.mentions = params.mentions.join(",");
}
return mapData;
}
/**
* Build a complete MetaLens comment transaction
*/
export function buildMetaLensTransaction(params) {
const mapData = buildMetaLensMapData(params);
// Build protocol data array
// Format: [B protocol data, MAP protocol data, AIP will be added by signing]
const protocolData = [];
// B protocol for content
protocolData.push(PROTOCOL_IDS.B, // B prefix
params.content, params.contentType || "text/plain", "utf8");
// MAP protocol for metadata
protocolData.push(PROTOCOL_IDS.MAP, // MAP prefix
"SET");
// Add all MAP data as key-value pairs
for (const [key, value] of Object.entries(mapData)) {
protocolData.push(key, value);
}
return protocolData;
}
/**
* Parse MetaLens comment from transaction data
*/
export function parseMetaLensComment(data) {
try {
// Extract MAP data
const mapData = data.MAP?.[0] || {};
// Extract content from B protocol
const content = data.B?.[0]?.content || "";
const contentType = data.B?.[0]?.["content-type"] || "text/plain";
// Extract author from AIP (for properly signed transactions)
const aipData = data.AIP?.[0] || {};
const author = aipData.address || "Anonymous";
// For now, accept all social posts as potential MetaLens comments
// Later we'll filter for proper MetaLens protocol data
const isMetaLensComment = mapData.app === METALENS_PROTOCOL.APP;
// If it's a proper MetaLens comment, parse the context
let contextType = "custom";
let contextValue = "";
if (isMetaLensComment) {
contextType = mapData.context;
contextValue = mapData[contextType] || "";
}
else {
// For legacy posts, try to infer context from content
// This is a fallback until we have proper MetaLens protocol adoption
if (content.includes("http://") || content.includes("https://")) {
contextType = "url";
const urlMatch = content.match(/(https?:\/\/[^\s]+)/);
contextValue = urlMatch?.[1] || content;
}
else {
contextType = "custom";
contextValue = content.substring(0, 50); // Use first 50 chars as context
}
}
// Parse timestamp
const timestamp = mapData.timestamp
? new Date(mapData.timestamp)
: new Date(data.timestamp || Date.now());
// Build comment object
const comment = {
txid: data.tx?.h || "",
author,
content,
contentType: contentType,
timestamp,
context: {
type: contextType,
value: contextValue,
subcontext: mapData.subcontext,
subcontextValue: mapData.subcontext
? mapData[mapData.subcontext]
: undefined,
},
metadata: {
app: mapData.app || "unknown",
thread: mapData.thread,
tags: mapData.tags?.split(",").filter(Boolean),
mentions: mapData.mentions?.split(",").filter(Boolean),
},
};
return comment;
}
catch (error) {
console.error("Failed to parse MetaLens comment:", error);
return null;
}
}
/**
* Build EventSource URL for real-time updates
*/
export function buildMetaLensStreamUrl(apiUrl, context, value) {
const params = new URLSearchParams({
context,
value,
app: METALENS_PROTOCOL.APP,
});
return `${apiUrl}/stream/comments?${params.toString()}`;
}
/**
* Validate context type and value
*/
export function validateContext(type, value) {
if (!value || typeof value !== "string") {
return { valid: false, error: "Context value is required" };
}
switch (type) {
case "tx":
// Validate transaction ID (64 hex chars)
if (!/^[a-fA-F0-9]{64}$/.test(value)) {
return { valid: false, error: "Invalid transaction ID format" };
}
break;
case "upc":
// Validate UPC (12-14 digits)
if (!/^\d{12,14}$/.test(value)) {
return { valid: false, error: "Invalid UPC format" };
}
break;
case "url":
// Validate URL
try {
new URL(value);
}
catch {
return { valid: false, error: "Invalid URL format" };
}
break;
case "geohash":
// Basic geohash validation (alphanumeric, 4-12 chars)
if (!/^[0-9a-z]{4,12}$/.test(value)) {
return { valid: false, error: "Invalid geohash format" };
}
break;
case "ipfs":
// IPFS hash validation
if (!value.startsWith("Qm") && !value.startsWith("ipfs://")) {
return { valid: false, error: "Invalid IPFS hash format" };
}
break;
case "isbn": {
// ISBN validation (10 or 13 digits)
const cleanIsbn = value.replace(/-/g, "");
if (!/^(?:\d{9}[\dX]|\d{13})$/.test(cleanIsbn)) {
return { valid: false, error: "Invalid ISBN format" };
}
break;
}
case "doi":
// DOI validation
if (!/^10\.\d{4,}\/[-._;()\/:a-zA-Z0-9]+$/.test(value)) {
return { valid: false, error: "Invalid DOI format" };
}
break;
}
return { valid: true };
}
/**
* Format context for display
*/
export function formatContextDisplay(type, value) {
switch (type) {
case "url":
try {
const url = new URL(value);
return url.hostname + (url.pathname !== "/" ? url.pathname : "");
}
catch {
return value;
}
case "tx":
return `${value.substring(0, 8)}...${value.substring(56)}`;
case "upc":
return `Product ${value}`;
case "geohash":
return `Location ${value.substring(0, 6)}`;
case "isbn":
return `Book ISBN ${value}`;
case "doi":
return `Paper ${value}`;
case "ipfs":
return `IPFS ${value.substring(0, 12)}...`;
default:
return value;
}
}