diff --git a/gauntlet-coa-autopost/README.md b/gauntlet-coa-autopost/README.md new file mode 100644 index 0000000..544d06f --- /dev/null +++ b/gauntlet-coa-autopost/README.md @@ -0,0 +1,115 @@ +# TrueCOA Auto-Post NFT Pipeline + +Auto-mint NFTs, create ScoreDetect certificates, and list on OpenSea when a new COA row is filled in Google Sheets. + +## Flow + +``` +Google Sheet edit → GAS trigger → Backend webhook → Mint NFT on Polygon → +ScoreDetect certificate → OpenSea auto-indexes → Results written back to sheet +``` + +## New Files + +| File | Purpose | +|------|---------| +| `backend/services/nftMinter.js` | Mint NFTs on Polygon via ethers.js (queued, nonce-safe) | +| `backend/services/scoreDetect.js` | ScoreDetect blockchain certification API | +| `backend/services/sheetWriter.js` | Write results back to Google Sheets (read/write scope) | +| `backend/services/automationPipeline.js` | Orchestrates the full pipeline per row | +| `backend/routes/automation.js` | Express routes: webhook, poll, retry, status | +| `google-apps-script/AutoPostTrigger.gs` | GAS installable trigger + menu for auto-post | + +## Modified Files + +| File | Changes | +|------|---------| +| `backend/index.js` | Added automation routes, sheets writer init, cron polling | +| `backend/package.json` | Added `node-cron` dependency | +| `backend/.env.example` | Added auto-post environment variables | + +## Setup + +### 1. Railway Environment Variables + +Add to your Railway backend deployment: + +```env +PRIVATE_KEY=your_polygon_wallet_private_key +MINT_RECIPIENT=0xYourGalleryWalletAddress +WEBHOOK_SECRET=your-random-secret-string +SCOREDETECT_API_KEY=your-scoredetect-key +OPENSEA_API_KEY=your-opensea-key # optional +POLL_INTERVAL=*/3 * * * * # optional, default 3 min +``` + +### 2. Google Sheets Scope + +The service account now needs **read/write** access to the spreadsheet (not just Viewer). +Share the sheet with the service account email as **Editor**. + +### 3. Google Apps Script + +1. Open your COA2 Google Sheet +2. Extensions > Apps Script +3. Create a new file: `AutoPostTrigger.gs` +4. Paste contents of `google-apps-script/AutoPostTrigger.gs` +5. Set Script Properties (File > Project settings > Script properties): + - `WEBHOOK_URL` = `https://coa.up.railway.app/api/automate` + - `WEBHOOK_SECRET` = (same secret as Railway) +6. Run `setupAutoPostTrigger()` once from the editor + +### 4. Deploy Backend + +```bash +cd backend +npm install +# Push to Railway (auto-deploys on git push) +``` + +## API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/api/automate/:coaCode` | POST | Webhook - process single COA | +| `/api/automate/poll` | POST | Manually trigger poll for all unprocessed rows | +| `/api/automate/retry/:coaCode` | POST | Retry a failed COA | +| `/api/automate/status` | GET | Pipeline status, wallet balance, config check | + +## How It Works + +1. **You edit a row** in the COA2 sheet (add artist, title, etc.) +2. **GAS trigger fires** → calls backend webhook with the COA code +3. **Backend processes**: + - Validates required fields + - Mints NFT on Polygon (queued to prevent nonce collisions) + - Creates ScoreDetect blockchain certificate + - Triggers OpenSea metadata refresh + - Writes token ID, URLs, and timestamp back to the sheet +4. **Polling fallback** runs every 3 minutes to catch anything the trigger missed + +## Sheet Column Mapping (COA2, 21 columns A-U) + +| Column | Index | Field | Description | +|--------|-------|-------|-------------| +| A | 0 | COA_CODE | COA identifier | +| B | 1 | QR_CODE | QR code image URL (output) | +| C | 2 | SIGNER | Artist / signer name | +| D | 3 | TITLE | Artwork title | +| E | 4 | DATE | Date of artwork | +| F | 5 | SIZE | Dimensions | +| G | 6 | CONDITION | Condition of artwork | +| H | 7 | DESCRIPTION | Description | +| I | 8 | PROVENANCE | Provenance (header: "Provience") | +| J | 9 | EDITION | Edition info | +| K | 10 | MEDIUM | Medium | +| L | 11 | ASSIGNEE | Assignee | +| M | 12 | THIRD_PARTY_AUTH_NOTES | Third party auth notes | +| N | 13 | IMAGE_URL | Image URL | +| O | 14 | NFT_TOKEN_ID | NFT token ID (output) | +| P | 15 | SHORT_URL | Bit.ly short URL (output) | +| Q | 16 | BLOCKCHAIN_URL | Polygonscan URL (output) | +| R | 17 | NFT_URL | OpenSea URL (output) | +| S | 18 | CERT_URL | ScoreDetect certificate URL (output) | +| T | 19 | STATUS | Status / Done timestamp (output) | +| U | 20 | COMPLETION_DATE | Completion timestamp (output) | diff --git a/gauntlet-coa-autopost/backend/.env.example b/gauntlet-coa-autopost/backend/.env.example new file mode 100644 index 0000000..6c2ec23 --- /dev/null +++ b/gauntlet-coa-autopost/backend/.env.example @@ -0,0 +1,42 @@ +# Google Sheets Configuration +SPREADSHEET_ID=16Kya2WQD0tbXTdsug9zSuoP03XmWXsZRTIykbEbBJBU +SHEET_NAME=COA + +# Google Service Account Credentials (JSON string) +# Get this from Google Cloud Console > APIs & Services > Credentials +GOOGLE_CREDENTIALS={"type":"service_account","project_id":"your-project",...} + +# Polygon Configuration +# For Amoy Testnet: +POLYGON_RPC=https://rpc-amoy.polygon.technology +# For Mainnet (later): +# POLYGON_RPC=https://polygon-rpc.com + +# Smart Contract Address (fill after deployment) +CONTRACT_ADDRESS= + +# Server Port +PORT=3001 + +# ============================================================================ +# AUTO-POST NFT PIPELINE (new) +# ============================================================================ + +# Polygon wallet private key for NFT minting (contract owner) +# NEVER commit this to git - set in Railway environment only +PRIVATE_KEY= + +# Default wallet address to receive minted NFTs +MINT_RECIPIENT=0xYourGalleryWalletAddress + +# Shared secret for Google Apps Script webhook authentication +WEBHOOK_SECRET=your-random-secret-string + +# ScoreDetect API key for blockchain certification +SCOREDETECT_API_KEY= + +# OpenSea API key for metadata refresh (optional, get from opensea.io/account/developer) +OPENSEA_API_KEY= + +# Polling interval (cron expression, default: every 3 minutes) +POLL_INTERVAL=*/3 * * * * diff --git a/gauntlet-coa-autopost/backend/index.js b/gauntlet-coa-autopost/backend/index.js new file mode 100644 index 0000000..a1baec1 --- /dev/null +++ b/gauntlet-coa-autopost/backend/index.js @@ -0,0 +1,754 @@ +/** + * ============================================================================ + * TRUECOA - BACKEND API SERVER + * ============================================================================ + * + * Express.js server that provides the API layer for the Certificate of + * Authenticity verification system. This server acts as the bridge between: + * + * 1. Frontend React application + * 2. Google Sheets (artwork metadata database) + * 3. Polygon blockchain (NFT verification) + * + * Architecture: + * ┌─────────────────────────────────────────────────────────────────────┐ + * │ API Server │ + * │ │ + * │ Frontend ──► /api/verify/:code ──► Google Sheets + Blockchain │ + * │ Frontend ──► /api/image/:code ──► Google Drive (proxy) │ + * │ Monitors ──► /health ──► Status check │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * Deployed: Railway (https://coa.up.railway.app) + * + * @author John Shay + * @version 1.0.0 + */ + +// ============================================================================ +// DEPENDENCIES +// ============================================================================ + +// Load environment variables from .env file (development only) +require('dotenv').config(); + +// Express.js - Web framework for Node.js +const express = require('express'); + +// CORS - Cross-Origin Resource Sharing middleware +// Allows frontend on different domain to access this API +const cors = require('cors'); + +// Google APIs - Official Google client library +// Used for reading artwork data from Google Sheets +const { google } = require('googleapis'); + +// Ethers.js v6 - Ethereum library for blockchain interaction +// Used for verifying NFTs on Polygon +const { ethers } = require('ethers'); + +// Node-cron - Scheduled tasks for polling automation +const cron = require('node-cron'); + +// Automation services +const automationRoutes = require('./routes/automation'); +const { initSheetsWriter } = require('./services/sheetWriter'); +const { pollAndProcess } = require('./services/automationPipeline'); + +// ============================================================================ +// APPLICATION SETUP +// ============================================================================ + +// Initialize Express application +const app = express(); + +// Enable CORS for all origins +// TODO: In production, restrict to specific domains: +// app.use(cors({ origin: ['https://truecoa.com', 'https://gauntlet-coa-frontend.vercel.app'] })); +app.use(cors()); + +// Parse JSON request bodies +// Enables req.body for POST/PUT requests with JSON content +app.use(express.json()); + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +/** + * Google Sheet containing artwork data + * Format: 16Kya2WQD0tbXTdsug9zSuoP03XmWXsZRTIykbEbBJBU (from sheet URL) + */ +const SPREADSHEET_ID = process.env.SPREADSHEET_ID; + +/** + * Name of the tab/sheet within the spreadsheet + * Default: 'COA' + */ +const SHEET_NAME = process.env.SHEET_NAME || 'COA'; + +/** + * Deployed smart contract address on Polygon + * This is the GauntletCOA ERC-721 contract + */ +const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || '0xD55496144F8CD69046656ddd5bb894c8b0C2d1b1'; + +/** + * Polygon RPC endpoint for blockchain queries + * Using public Polygon RPC for mainnet + */ +const POLYGON_RPC = process.env.POLYGON_RPC || 'https://1rpc.io/matic'; + +// ============================================================================ +// SMART CONTRACT INTERFACE +// ============================================================================ + +/** + * Minimal ABI (Application Binary Interface) for the GauntletCOA contract + * + * We only include the read-only functions needed for verification: + * - isCoaMinted: Check if a COA code has been minted + * - getTokenIdByCoaCode: Get the token ID for a COA code + * - getCoaOwner: Get the owner address of a COA + * - tokenURI: Get the metadata URI for a token + * + * Full contract ABI not needed since we don't mint from the backend + */ +const CONTRACT_ABI = [ + "function isCoaMinted(string memory coaCode) public view returns (bool)", + "function getTokenIdByCoaCode(string memory coaCode) public view returns (uint256)", + "function getCoaOwner(string memory coaCode) public view returns (address)", + "function tokenURI(uint256 tokenId) public view returns (string memory)" +]; + +// ============================================================================ +// GOOGLE SHEETS INTEGRATION +// ============================================================================ + +/** + * Google Sheets API client instance + * Initialized once at startup, reused for all requests + */ +let sheets; + +/** + * Initialize Google Sheets API client with service account credentials + * + * Authentication uses a Google Cloud service account, which allows + * server-to-server communication without user interaction. + * + * The service account email must be given Viewer access to the spreadsheet. + * + * @returns {Promise} + */ +async function initGoogleSheets() { + // Check if credentials are configured + if (!process.env.GOOGLE_CREDENTIALS) { + console.log('Warning: GOOGLE_CREDENTIALS not set, Google Sheets disabled'); + return; + } + + try { + let credentialsJson = process.env.GOOGLE_CREDENTIALS; + + // Check if credentials are base64 encoded (doesn't start with {) + if (!credentialsJson.trim().startsWith('{')) { + console.log('Decoding base64 credentials'); + credentialsJson = Buffer.from(credentialsJson, 'base64').toString('utf8'); + } + + console.log('Parsing credentials...'); + let credentials = JSON.parse(credentialsJson); + console.log('Parsed successfully, client_email:', credentials.client_email); + + const auth = new google.auth.GoogleAuth({ + credentials: credentials, + scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'] + }); + + // Create Sheets API client + sheets = google.sheets({ version: 'v4', auth }); + console.log('Google Sheets initialized successfully'); + } catch (err) { + console.error('Failed to init Google Sheets:', err.message); + console.error('Full error:', err); + } +} + +/** + * Retrieve COA data from Google Sheets by COA code + * + * Searches the spreadsheet for a row matching the given COA code + * and returns all columns as a key-value object. + * + * Sheet Structure Expected: + * | coa_code | artist | title | date | length | width | edition | number | history | image_url | + * + * @param {string} coaCode - The COA code to search for (e.g., "290745") + * @returns {Promise} - COA data object or null if not found + * + * @example + * const coa = await getCOAFromSheet("290745"); + * // Returns: { coa_code: "290745", artist: "Shepard Fairey", title: "...", ... } + */ +async function getCOAFromSheet(coaCode) { + // Check if sheets client is initialized + if (!sheets) { + throw new Error('Google Sheets not initialized - check GOOGLE_CREDENTIALS'); + } + + // Fetch all data from the sheet (columns A through K) + // Range format: SheetName!A:K means columns A-K, all rows + const response = await sheets.spreadsheets.values.get({ + spreadsheetId: SPREADSHEET_ID, + range: `${SHEET_NAME}!A:AZ` // All columns including any new additions + }); + + const rows = response.data.values; + + // Check if sheet has data + if (!rows || rows.length === 0) return null; + + // First row contains headers - normalize to lowercase with underscores + // Example: "COA Code" becomes "coa_code" + // Handle duplicate column names by appending _2, _3, etc. + const rawHeaders = rows[0].map(h => String(h) + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, '')); + const headerCount = {}; + const headers = rawHeaders.map(h => { + headerCount[h] = (headerCount[h] || 0) + 1; + return headerCount[h] > 1 ? `${h}_${headerCount[h]}` : h; + }); + console.log('Sheet headers (deduplicated):', headers); + + // Find the index of the coa_code column + const coaCodeIndex = headers.indexOf('coa_code'); + + if (coaCodeIndex === -1) { + throw new Error('COA_Code column not found in spreadsheet'); + } + + // Search for matching row (skip header row, start at index 1) + for (let i = 1; i < rows.length; i++) { + // Compare COA codes (case-insensitive comparison done by caller) + if (rows[i][coaCodeIndex] === coaCode) { + // Build object from headers and row values + const coaData = {}; + headers.forEach((header, index) => { + coaData[header] = rows[i][index] || ''; // Default to empty string if cell is empty + }); + return coaData; + } + } + + // No matching row found + return null; +} + +// ============================================================================ +// BLOCKCHAIN VERIFICATION +// ============================================================================ + +/** + * Verify NFT status on Polygon blockchain + * + * Checks if a COA code has been minted as an NFT and retrieves + * ownership and metadata information. + * + * @param {string} coaCode - The COA code to verify + * @returns {Promise} - Verification result object + * + * @example + * // NFT exists: + * { verified: true, tokenId: "1", owner: "0x...", tokenURI: "ipfs://...", ... } + * + * // NFT not minted: + * { verified: false, reason: "NFT not minted for this COA" } + */ +async function verifyNFT(coaCode) { + // Check if contract is configured + if (!CONTRACT_ADDRESS) { + return { verified: false, reason: 'Contract not deployed yet' }; + } + + try { + // Create read-only connection to Polygon network + // JsonRpcProvider is for HTTP-based connections + const provider = new ethers.JsonRpcProvider(POLYGON_RPC); + + // Create contract instance with provider (read-only, no signer needed) + const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); + + // Check if this COA code has been minted + const isMinted = await contract.isCoaMinted(coaCode); + + if (!isMinted) { + return { verified: false, reason: 'NFT not minted for this COA' }; + } + + // COA is minted - fetch additional details + const tokenId = await contract.getTokenIdByCoaCode(coaCode); + const owner = await contract.getCoaOwner(coaCode); + let tokenURI = ''; + try { + tokenURI = await contract.tokenURI(tokenId); + } catch (e) { + // tokenURI may not be set - that's OK + console.log('tokenURI not available for token', tokenId.toString()); + } + + return { + verified: true, + tokenId: tokenId.toString(), + owner, + tokenURI, + contractAddress: CONTRACT_ADDRESS, + network: 'Polygon' + }; + } catch (error) { + // Return error as unverified status + // Common errors: network issues, contract not found, RPC rate limit + return { verified: false, reason: error.message }; + } +} + +// ============================================================================ +// API ROUTES +// ============================================================================ + +/** + * Health Check Endpoint + * GET /health + * + * Used by: + * - Railway for deployment health checks + * - Monitoring systems + * - Manual verification that server is running + * + * @returns {Object} { status: "ok", timestamp: "ISO date string" } + */ +app.get('/health', (req, res) => { + let clientEmail = null; + try { + let raw = process.env.GOOGLE_CREDENTIALS || '{}'; + if (!raw.trim().startsWith('{')) raw = Buffer.from(raw, 'base64').toString('utf8'); + const creds = JSON.parse(raw); + clientEmail = creds.client_email || 'not found'; + } catch (e) { + clientEmail = 'parse error: ' + e.message; + } + + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + sheetsReady: !!sheets, + credentialsSet: !!process.env.GOOGLE_CREDENTIALS, + clientEmail: clientEmail + }); +}); + +/** + * COA Verification Endpoint + * GET /api/verify/:coaCode + * + * Main endpoint for verifying artwork authenticity. Combines data from + * Google Sheets (metadata) and Polygon blockchain (NFT verification). + * + * @param {string} coaCode - URL parameter, the COA code to verify + * + * @returns {Object} Combined verification response: + * { + * success: true, + * coa: { code, artist, title, date, dimensions, edition, number, history, imageUrl }, + * blockchain: { verified, tokenId?, owner?, tokenURI?, contractAddress?, network? }, + * verifiedAt: "ISO timestamp" + * } + * + * @example + * GET /api/verify/290745 + */ +app.get('/api/verify/:coaCode', async (req, res) => { + try { + const { coaCode } = req.params; + + // Validate input + if (!coaCode) { + return res.status(400).json({ error: 'COA code is required' }); + } + + // Normalize COA code to uppercase for consistent matching + const normalizedCode = coaCode.toUpperCase(); + + // Static fallback metadata for known minted tokens (used when Google Sheets is unavailable) + const FALLBACK_METADATA = { + '291046': { + artist: 'Shepard Fairey', title: 'Lenin Record', date: '2005', + dimensions: '24" x 18"', edition: '101 of 300', medium: 'Drawing on Heavy Matte Paper', + condition: 'Very good', sku: '1_Obey_Lenin-Record_P', + description: 'Felt-tip drawing on heavy paper depicting actor James Dean. Preparatory drawing for the Japanese ads series.', + provenance: 'Acquired by Executor of Warhol\'s Estate Fredrick Hughes and subsequently sold to the grandfather of the current owner, where upon the passing of said grandfather, the current owner, grandchild inherited the work.' + }, + '291047': { + artist: 'Shepard Fairey', title: 'Rose Soldier', date: '2017', + dimensions: '13" x 10"', edition: '2 of 450', medium: '', + condition: '', sku: '2_Obey_Rose-Soldier_P', + description: '', provenance: '' + }, + '291048': { + artist: 'Shepard Fairey', title: 'Chinese Soldiers', date: '2006', + dimensions: '24" x 18"', edition: '3 of 300', medium: '', + condition: '', sku: '3_Obey_Chinese-Soldiers_P', + description: '', provenance: '' + }, + 'W1': { + artist: 'Andy Warhol', title: 'Rebel Without a Cause (James Dean)', date: '1985', + dimensions: '', edition: 'Unique', medium: 'Felt-tip drawing on heavy matte paper', + condition: 'Very good', sku: 'W1_Warhol_James-Dean_L', + description: 'Felt-tip drawing on heavy paper depicting actor James Dean. Preparatory drawing for the Japanese ads series Andy Warhol Rebel Without a Cause (James Dean) from the 1985 Ad Series.', + provenance: 'Acquired by Executor of Warhol\'s Estate Fredrick Hughes and subsequently sold to the grandfather of the current owner, where upon the passing of said grandfather, the current owner, grandchild inherited the work.' + } + }; + + // Step 1: Get COA data from Google Sheets (with fallback) + let coaData = null; + try { + coaData = await getCOAFromSheet(normalizedCode); + } catch (sheetErr) { + console.error('Google Sheets error, using fallback:', sheetErr.message); + } + + // Use fallback if Sheets failed or returned nothing + if (!coaData) { + const fallback = FALLBACK_METADATA[normalizedCode]; + if (!fallback) { + return res.status(404).json({ error: 'COA not found', code: coaCode }); + } + coaData = { + signer: fallback.artist, + title: fallback.title, + date: fallback.date, + size: fallback.dimensions, + edition: fallback.edition, + medium: fallback.medium, + condition: fallback.condition, + description: fallback.description, + providence: fallback.provenance, + image_url: '' + }; + } + + // Step 2: Verify on blockchain + const blockchainStatus = await verifyNFT(normalizedCode); + + // Step 3: Build and return response + // Map column names to standard fields + // COA sheet headers (normalized): + // coa_code, qr_code, signer, title, date, medium, edition, size, condition, + // description, providence, assignor, assignee, third_party_authentication_notes, + // sku, image_url, nft_tokenid, short_url, blockchain_url, nft_url, cert_url, + // status, completion_date + const artist = coaData.signer || coaData.artist || coaData.Artist || ''; + const title = coaData.title || coaData.Title || 'Untitled'; + const description = coaData.description || coaData.Description || ''; + const provenance = coaData.providence || coaData.provenance || coaData.notes_providence || ''; + const medium = coaData.medium || coaData.Medium || ''; + const condition = coaData.condition || coaData.Condition || ''; + const size = coaData.size || coaData.dimensions || ''; + const edition = coaData.edition || coaData.edition_ || coaData.Edition || ''; + const year = coaData.date || coaData.Date || coaData.year || ''; + const imageUrl = coaData.image_url || coaData.Image_URL || ''; + const sku = coaData.sku || coaData.SKU || ''; + const assignor = coaData.assignor || coaData.authenticator || ''; + const assignee = coaData.assignee || ''; + const authNotes = coaData.third_party_authentication_notes || coaData.auth_notes || ''; + const completionDate = coaData.completion_date || ''; + const qrCodeUrl = coaData.qr_code || ''; + const shortUrl = coaData.short_url || ''; + const blockchainUrl = coaData.blockchain_url || ''; + const nftUrl = coaData.nft_url || ''; + const certUrl = coaData.cert_url || ''; + const authenticator = coaData.authenticator || ''; + const authenticatorNumber = coaData.authenticator_number || coaData.number || ''; + const authenticatorDate = coaData.authenticator_date || ''; + const authenticatorLink = coaData.third_party_coa_link || coaData.authenticator_link || ''; + + // Build image URL - prefer Image_URL from sheet + let nftImage = imageUrl; + if (!nftImage || nftImage.includes('#REF')) { + nftImage = ''; + } + + // Build rich description from all available COA fields + let nftDescription = `Certificate of Authenticity for "${title}" by ${artist}.`; + if (year) nftDescription += ` Created in ${year}.`; + if (medium) nftDescription += `\n\nMedium: ${medium}`; + if (size) nftDescription += `\nSize: ${size}`; + if (edition) nftDescription += `\nEdition: ${edition}`; + if (condition) nftDescription += `\nCondition: ${condition}`; + if (description) nftDescription += `\n\n${description}`; + if (provenance) nftDescription += `\n\nProvenance: ${provenance}`; + nftDescription += `\n\nThis certificate is cryptographically secured on the Polygon blockchain and linked to a unique NFT. Verified by TrueCOA.`; + + // Build attributes array with all available fields + const attributes = [ + { trait_type: "Signer", value: artist || 'Unknown' }, + { trait_type: "Title", value: title }, + { trait_type: "COA Code", value: normalizedCode } + ]; + if (year) attributes.push({ trait_type: "Year", value: year }); + if (medium) attributes.push({ trait_type: "Medium", value: medium }); + if (size) attributes.push({ trait_type: "Size", value: size }); + if (edition) attributes.push({ trait_type: "Edition", value: edition }); + if (condition) attributes.push({ trait_type: "Condition", value: condition }); + if (assignor) attributes.push({ trait_type: "Assignor", value: assignor }); + attributes.push({ trait_type: "Verified By", value: "TrueCOA" }); + attributes.push({ trait_type: "Blockchain", value: "Polygon" }); + + const response = { + // === ERC-721 Metadata fields (for OpenSea/NFT marketplaces) === + name: `TrueCOA - ${title}`, + description: nftDescription.trim(), + image: nftImage, + external_url: `https://truecoa.com/AUTHENTICATE/${normalizedCode}`, + attributes, + // === Legacy fields (for frontend app) === + success: true, + coa: { + code: normalizedCode, + artist, + title, + date: year, + completionDate, + size, + edition, + medium, + condition, + description, + provenance, + assignor, + assignee, + authNotes, + authenticator, + authenticatorNumber, + authenticatorDate, + authenticatorLink, + qrCodeUrl, + shortUrl, + blockchainUrl, + nftUrl, + certUrl, + sku, + imageUrl: imageUrl + }, + blockchain: blockchainStatus, + verifiedAt: new Date().toISOString() + }; + + res.json(response); + } catch (error) { + // Log error for debugging (visible in Railway logs) + console.error('Verification error:', error); + + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); + } +}); + +/** + * Image Proxy Endpoint + * GET /api/image/:coaCode + * + * Retrieves the artwork image URL from the sheet and redirects to it. + * Handles Google Drive URL conversion for direct image access. + * + * Why a proxy? + * - Google Drive links don't work directly in tags + * - Abstracts storage location from frontend + * - Could add caching/CDN in the future + * + * @param {string} coaCode - URL parameter, the COA code + * + * @returns Redirect (302) to the image URL + */ +app.get('/api/image/:coaCode', async (req, res) => { + try { + const { coaCode } = req.params; + + // Get COA data to find image URL + const coaData = await getCOAFromSheet(coaCode.toUpperCase()); + + if (!coaData || !coaData.image_url) { + return res.status(404).json({ error: 'Image not found' }); + } + + // Convert Google Drive sharing link to direct download URL + // Input formats: + // https://drive.google.com/file/d/FILE_ID/view + // https://drive.google.com/open?id=FILE_ID + // Output format: + // https://drive.google.com/uc?export=view&id=FILE_ID + let imageUrl = coaData.image_url; + + if (imageUrl.includes('drive.google.com')) { + // Extract file ID using regex + const fileId = imageUrl.match(/\/d\/([a-zA-Z0-9_-]+)/)?.[1] + || imageUrl.match(/id=([a-zA-Z0-9_-]+)/)?.[1]; + + if (fileId) { + // Convert to direct view URL + imageUrl = `https://drive.google.com/uc?export=view&id=${fileId}`; + } + } + + // Redirect to the image URL + // 302 = temporary redirect (allows URL to change in future) + res.redirect(imageUrl); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch image' }); + } +}); + +/** + * NFT Metadata Endpoint (OpenSea Compatible) + * GET /api/nft/:coaCode + * + * Returns NFT metadata in OpenSea-compatible format for wallets and marketplaces. + * + * @param {string} coaCode - URL parameter, the COA code + * + * @returns {Object} OpenSea-compatible metadata: + * { + * name: "COA #291045 - Title", + * description: "Certificate of Authenticity...", + * image: "https://...", + * external_url: "https://...", + * attributes: [...] + * } + */ +// ============================================================================ +// AUTOMATION ROUTES +// ============================================================================ + +/** + * Auto-post NFT pipeline routes + * POST /api/automate/:coaCode - Webhook from GAS + * POST /api/automate/poll - Manual poll trigger + * GET /api/automate/status - Pipeline status + * POST /api/automate/retry/:coaCode - Retry failed + */ +app.use('/api/automate', automationRoutes); + +app.get('/api/nft/:coaCode', async (req, res) => { + try { + const { coaCode } = req.params; + const normalizedCode = coaCode.toUpperCase(); + + // Get COA data from Google Sheets + const coaData = await getCOAFromSheet(normalizedCode); + + if (!coaData) { + return res.status(404).json({ error: 'COA not found' }); + } + + // Build image URL + let imageUrl = coaData.image_url || ''; + if (imageUrl.includes('drive.google.com')) { + const fileId = imageUrl.match(/\/d\/([a-zA-Z0-9_-]+)/)?.[1] + || imageUrl.match(/id=([a-zA-Z0-9_-]+)/)?.[1]; + if (fileId) { + imageUrl = `https://drive.google.com/uc?export=view&id=${fileId}`; + } + } + + // Build OpenSea-compatible metadata + const metadata = { + name: `TrueCOA #${normalizedCode} - ${coaData.title || 'Untitled'}`, + description: `Certificate of Authenticity for "${coaData.title || 'Untitled'}" by ${coaData.signer || coaData.artist || 'Unknown'}. Verified on Polygon blockchain. ${coaData.description || ''}`.trim(), + image: imageUrl || `https://coa.up.railway.app/api/image/${normalizedCode}`, + external_url: `https://truecoa.com/AUTHENTICATE/${normalizedCode}`, + attributes: [ + { trait_type: "Signer", value: coaData.signer || coaData.artist || 'Unknown' }, + { trait_type: "Title", value: coaData.title || 'Untitled' }, + { trait_type: "Year", value: coaData.date || 'Unknown' }, + { trait_type: "Size", value: coaData.size || '' }, + { trait_type: "COA Code", value: normalizedCode } + ] + }; + + // Add edition info if available + if (coaData.edition) { + metadata.attributes.push({ trait_type: "Edition", value: coaData.edition }); + } + + res.json(metadata); + } catch (error) { + console.error('NFT metadata error:', error); + res.status(500).json({ error: 'Failed to fetch NFT metadata' }); + } +}); + +// ============================================================================ +// SERVER STARTUP +// ============================================================================ + +/** + * Server port - defaults to 3001 for local development + * Railway sets PORT environment variable automatically + */ +const PORT = process.env.PORT || 3001; + +/** + * Initialize services and start the server + * + * Startup sequence: + * 1. Initialize Google Sheets connection + * 2. Start Express server + * 3. If Sheets fails, server still starts (graceful degradation) + */ +/** + * Startup sequence: + * 1. Initialize Google Sheets (read-only for verification) + * 2. Initialize Sheets Writer (read/write for automation) + * 3. Start Express server + * 4. Start polling cron job for auto-post pipeline + */ +Promise.all([ + initGoogleSheets().catch(err => console.error('Sheets read init warning:', err.message)), + initSheetsWriter().catch(err => console.error('Sheets write init warning:', err.message)) +]) + .then(() => { + // Start server, binding to 0.0.0.0 for container environments + app.listen(PORT, '0.0.0.0', () => { + console.log(`TrueCOA API running on port ${PORT}`); + + // Start auto-post polling every 3 minutes (if PRIVATE_KEY is configured) + if (process.env.PRIVATE_KEY) { + const POLL_INTERVAL = process.env.POLL_INTERVAL || '*/3 * * * *'; // every 3 min + cron.schedule(POLL_INTERVAL, async () => { + console.log('[cron] Polling for unprocessed COA rows...'); + try { + const result = await pollAndProcess(); + if (result.processed > 0) { + console.log(`[cron] Processed ${result.processed} row(s)`); + } + } catch (err) { + console.error('[cron] Poll error:', err.message); + } + }); + console.log(`Auto-post polling enabled (${POLL_INTERVAL})`); + } else { + console.log('Auto-post polling disabled (PRIVATE_KEY not set)'); + } + }); + }) + .catch(err => { + console.error('Warning:', err.message); + app.listen(PORT, '0.0.0.0', () => { + console.log(`TrueCOA API running on port ${PORT} (degraded mode)`); + }); + }); diff --git a/gauntlet-coa-autopost/backend/package.json b/gauntlet-coa-autopost/backend/package.json new file mode 100644 index 0000000..c32bc12 --- /dev/null +++ b/gauntlet-coa-autopost/backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "gauntlet-coa-api", + "version": "1.0.0", + "description": "Backend API for Gauntlet Gallery COA verification", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "googleapis": "^129.0.0", + "ethers": "^6.9.0", + "node-cron": "^3.0.3" + }, + "devDependencies": { + "nodemon": "^3.0.2" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/gauntlet-coa-autopost/backend/routes/automation.js b/gauntlet-coa-autopost/backend/routes/automation.js new file mode 100644 index 0000000..e2ccccc --- /dev/null +++ b/gauntlet-coa-autopost/backend/routes/automation.js @@ -0,0 +1,160 @@ +/** + * ============================================================================ + * TRUECOA - AUTOMATION ROUTES + * ============================================================================ + * + * Express routes for the auto-post NFT pipeline: + * POST /api/automate/:coaCode - Webhook (triggered by GAS on sheet edit) + * POST /api/automate/poll - Manually trigger polling + * GET /api/automate/status - Pipeline status & wallet balance + * POST /api/automate/retry/:coaCode - Retry a failed row + */ + +const express = require('express'); +const router = express.Router(); +const { processCoaCode, pollAndProcess, processing } = require('../services/automationPipeline'); +const { getWalletBalance } = require('../services/nftMinter'); +const { getUnprocessedRows } = require('../services/sheetWriter'); + +/** + * Webhook authentication middleware + * Validates the shared secret from Google Apps Script + */ +function authenticateWebhook(req, res, next) { + const secret = process.env.WEBHOOK_SECRET; + + // If no secret configured, allow all requests (dev mode) + if (!secret) { + console.log('Warning: WEBHOOK_SECRET not set, webhook auth disabled'); + return next(); + } + + const provided = req.headers['x-webhook-secret'] || req.body?.secret; + if (provided !== secret) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + next(); +} + +/** + * POST /api/automate/:coaCode + * + * Webhook endpoint called by Google Apps Script when a row is edited. + * Processes a single COA code through the full pipeline. + */ +router.post('/:coaCode', authenticateWebhook, async (req, res) => { + try { + const { coaCode } = req.params; + + if (!coaCode) { + return res.status(400).json({ error: 'COA code is required' }); + } + + console.log(`Webhook received for COA: ${coaCode}`); + + // Process asynchronously - return immediately + res.json({ + accepted: true, + coaCode, + message: 'Processing started. Check /api/automate/status for progress.' + }); + + // Process in background + processCoaCode(coaCode) + .then(result => { + console.log(`Webhook processing complete for ${coaCode}:`, result.success ? 'SUCCESS' : 'FAILED'); + }) + .catch(err => { + console.error(`Webhook processing error for ${coaCode}:`, err.message); + }); + + } catch (error) { + console.error('Webhook error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * POST /api/automate/poll + * + * Manually trigger a poll for unprocessed rows. + * Useful for testing or when the cron job needs a manual kick. + */ +router.post('/poll', authenticateWebhook, async (req, res) => { + try { + console.log('Manual poll triggered'); + const result = await pollAndProcess(); + res.json(result); + } catch (error) { + console.error('Poll error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * POST /api/automate/retry/:coaCode + * + * Retry a failed COA code. Clears the error status and re-processes. + */ +router.post('/retry/:coaCode', authenticateWebhook, async (req, res) => { + try { + const { coaCode } = req.params; + console.log(`Retry requested for COA: ${coaCode}`); + + const result = await processCoaCode(coaCode); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +/** + * GET /api/automate/status + * + * Returns current pipeline status: + * - Currently processing COA codes + * - Unprocessed row count + * - Wallet MATIC balance + * - Configuration status + */ +router.get('/status', async (req, res) => { + try { + let walletInfo = null; + try { + walletInfo = await getWalletBalance(); + } catch (e) { + walletInfo = { error: e.message }; + } + + let unprocessedCount = 0; + try { + const rows = await getUnprocessedRows(); + unprocessedCount = rows.length; + } catch (e) { + // Sheets may not be initialized + } + + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + currentlyProcessing: Array.from(processing), + unprocessedRows: unprocessedCount, + wallet: walletInfo, + config: { + privateKeySet: !!process.env.PRIVATE_KEY, + webhookSecretSet: !!process.env.WEBHOOK_SECRET, + scoreDetectKeySet: !!process.env.SCOREDETECT_API_KEY, + openSeaKeySet: !!process.env.OPENSEA_API_KEY, + mintRecipientSet: !!process.env.MINT_RECIPIENT, + contractAddress: process.env.CONTRACT_ADDRESS || '0xD55496144F8CD69046656ddd5bb894c8b0C2d1b1', + spreadsheetId: process.env.SPREADSHEET_ID ? '***configured***' : 'NOT SET', + sheetName: process.env.SHEET_NAME || 'COA2' + } + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/gauntlet-coa-autopost/backend/services/automationPipeline.js b/gauntlet-coa-autopost/backend/services/automationPipeline.js new file mode 100644 index 0000000..c88ae73 --- /dev/null +++ b/gauntlet-coa-autopost/backend/services/automationPipeline.js @@ -0,0 +1,225 @@ +/** + * ============================================================================ + * TRUECOA - AUTOMATION PIPELINE + * ============================================================================ + * + * Orchestrates the full auto-post NFT pipeline: + * 1. Validate row data + * 2. Mint NFT on Polygon + * 3. Create ScoreDetect certificate + * 4. Force OpenSea metadata refresh + * 5. Write results back to Google Sheet + * + * Triggered by: + * - Google Apps Script webhook (near-instant on sheet edit) + * - Backend polling (fallback, every 3 minutes) + * - Manual API call (/api/automate/:coaCode) + */ + +const { mintCOA } = require('./nftMinter'); +const { createCertificate } = require('./scoreDetect'); +const { writeResults, setRowStatus, getUnprocessedRows } = require('./sheetWriter'); + +// Track in-flight processing to prevent duplicates +const processing = new Set(); + +/** + * Process a single COA row through the full automation pipeline + * + * @param {Object} rowData - Row data from getUnprocessedRows() + * @param {number} rowData.rowNumber - Sheet row number + * @param {string} rowData.coaCode - COA code + * @param {string} rowData.signer - Signer name + * @param {string} rowData.title - Title + * @returns {Promise} Pipeline result + */ +async function processRow(rowData) { + const { rowNumber, coaCode, signer, title } = rowData; + + // Prevent duplicate processing + if (processing.has(coaCode)) { + console.log(`COA ${coaCode} already being processed, skipping`); + return { skipped: true, reason: 'already processing' }; + } + + processing.add(coaCode); + const results = { coaCode, rowNumber, steps: {} }; + + try { + // Mark row as processing + await setRowStatus(rowNumber, 'processing'); + console.log(`\n========== Processing COA ${coaCode} (Row ${rowNumber}) ==========`); + + // --- Step 1: Validate --- + if (!coaCode || (!signer && !title)) { + throw new Error('Missing required fields: coaCode and (signer or title)'); + } + results.steps.validate = { success: true }; + console.log(`[1/4] Validated: "${title}" by ${signer}`); + + // --- Step 2: Mint NFT on Polygon --- + try { + const mintResult = await mintCOA(coaCode); + results.steps.mint = { success: true, ...mintResult }; + results.tokenId = mintResult.tokenId; + results.polygonscanUrl = mintResult.polygonscanUrl; + results.openSeaUrl = mintResult.openSeaUrl; + + if (mintResult.alreadyMinted) { + console.log(`[2/4] NFT already minted: Token #${mintResult.tokenId}`); + } else { + console.log(`[2/4] NFT minted: Token #${mintResult.tokenId} (tx: ${mintResult.txHash})`); + } + } catch (mintErr) { + console.error(`[2/4] Mint failed: ${mintErr.message}`); + results.steps.mint = { success: false, error: mintErr.message }; + // Continue - minting failure shouldn't block ScoreDetect + } + + // --- Step 3: ScoreDetect Certificate --- + try { + const certResult = await createCertificate({ + coaCode, + signer, + title, + description: rowData.description, + medium: rowData.medium, + edition: rowData.edition, + condition: rowData.condition, + provenance: rowData.provenance, + imageUrl: rowData.imageUrl + }); + results.steps.scoreDetect = certResult; + + if (certResult.skipped) { + console.log(`[3/4] ScoreDetect skipped: ${certResult.reason}`); + } else if (certResult.error) { + console.log(`[3/4] ScoreDetect error: ${certResult.error}`); + } else { + results.certificateUrl = certResult.certificateUrl; + console.log(`[3/4] ScoreDetect certificate: ${certResult.certificateUrl}`); + } + } catch (certErr) { + console.error(`[3/4] ScoreDetect failed: ${certErr.message}`); + results.steps.scoreDetect = { success: false, error: certErr.message }; + } + + // --- Step 4: Force OpenSea metadata refresh --- + try { + if (results.tokenId) { + const refreshResult = await refreshOpenSeaMetadata( + process.env.CONTRACT_ADDRESS || '0xD55496144F8CD69046656ddd5bb894c8b0C2d1b1', + results.tokenId + ); + results.steps.openSea = refreshResult; + console.log(`[4/4] OpenSea refresh: ${refreshResult.success ? 'done' : refreshResult.reason || 'skipped'}`); + } else { + results.steps.openSea = { skipped: true, reason: 'No token ID (mint failed)' }; + console.log('[4/4] OpenSea refresh skipped (no token)'); + } + } catch (osErr) { + console.error(`[4/4] OpenSea refresh failed: ${osErr.message}`); + results.steps.openSea = { success: false, error: osErr.message }; + } + + // --- Write results back to sheet --- + const writeData = { + tokenId: results.tokenId, + polygonscanUrl: results.polygonscanUrl, + openSeaUrl: results.openSeaUrl, + certificateUrl: results.certificateUrl, + shortUrl: rowData.shortUrl, // preserve existing + status: new Date().toISOString() + }; + + await writeResults(rowNumber, writeData); + results.success = true; + console.log(`========== COA ${coaCode} complete ==========\n`); + + } catch (error) { + console.error(`Pipeline error for COA ${coaCode}:`, error.message); + results.success = false; + results.error = error.message; + + // Write error status to sheet + try { + await setRowStatus(rowNumber, `error: ${error.message}`); + } catch (writeErr) { + console.error('Failed to write error status:', writeErr.message); + } + } finally { + processing.delete(coaCode); + } + + return results; +} + +/** + * Force OpenSea to refresh NFT metadata + */ +async function refreshOpenSeaMetadata(contractAddress, tokenId) { + const apiKey = process.env.OPENSEA_API_KEY; + if (!apiKey) { + return { success: false, reason: 'OPENSEA_API_KEY not set' }; + } + + try { + const url = `https://api.opensea.io/api/v2/chain/matic/contract/${contractAddress}/nfts/${tokenId}/refresh`; + const response = await fetch(url, { + method: 'POST', + headers: { 'X-API-KEY': apiKey } + }); + + return { success: response.ok, status: response.status }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +/** + * Poll for unprocessed rows and process them + * Called by the cron job / setInterval + */ +async function pollAndProcess() { + try { + const unprocessed = await getUnprocessedRows(); + + if (unprocessed.length === 0) { + return { processed: 0, message: 'No unprocessed rows' }; + } + + console.log(`Found ${unprocessed.length} unprocessed row(s)`); + + const results = []; + for (const row of unprocessed) { + const result = await processRow(row); + results.push(result); + } + + return { + processed: results.length, + results + }; + } catch (error) { + console.error('Poll error:', error.message); + return { processed: 0, error: error.message }; + } +} + +/** + * Process a single COA code (for webhook/manual trigger) + * Looks up the row in the sheet first + */ +async function processCoaCode(coaCode) { + const unprocessed = await getUnprocessedRows(); + const row = unprocessed.find(r => r.coaCode.toUpperCase() === coaCode.toUpperCase()); + + if (!row) { + // Check all rows (including processed) for retry + throw new Error(`COA code ${coaCode} not found or already processed`); + } + + return processRow(row); +} + +module.exports = { processRow, pollAndProcess, processCoaCode, processing }; diff --git a/gauntlet-coa-autopost/backend/services/nftMinter.js b/gauntlet-coa-autopost/backend/services/nftMinter.js new file mode 100644 index 0000000..295b11d --- /dev/null +++ b/gauntlet-coa-autopost/backend/services/nftMinter.js @@ -0,0 +1,172 @@ +/** + * ============================================================================ + * TRUECOA - NFT MINTING SERVICE + * ============================================================================ + * + * Mints COA NFTs on Polygon blockchain using the deployed GauntletCOA contract. + * Adapted from scripts/mint.js for use as a backend service. + * + * Contract: 0xD55496144F8CD69046656ddd5bb894c8b0C2d1b1 (Polygon Mainnet) + */ + +const { ethers } = require('ethers'); + +// Contract ABI - includes write functions for minting +const CONTRACT_ABI = [ + "function mintCOA(address to, string memory coaCode, string memory uri) public returns (uint256)", + "function batchMintCOA(address to, string[] memory coaCodes, string[] memory uris) public", + "function isCoaMinted(string memory coaCode) public view returns (bool)", + "function getTokenIdByCoaCode(string memory coaCode) public view returns (uint256)", + "function getCoaOwner(string memory coaCode) public view returns (address)", + "function tokenURI(uint256 tokenId) public view returns (string memory)", + "event COAMinted(uint256 indexed tokenId, string coaCode, address indexed owner)" +]; + +// Mutex for sequential minting (prevents nonce collisions) +let mintLock = false; +const mintQueue = []; + +/** + * Process the mint queue one at a time + */ +async function processMintQueue() { + if (mintLock || mintQueue.length === 0) return; + mintLock = true; + + const { coaCode, recipientAddress, resolve, reject } = mintQueue.shift(); + + try { + const result = await _executeMint(coaCode, recipientAddress); + resolve(result); + } catch (err) { + reject(err); + } finally { + mintLock = false; + if (mintQueue.length > 0) { + processMintQueue(); + } + } +} + +/** + * Execute a single NFT mint on Polygon + */ +async function _executeMint(coaCode, recipientAddress) { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY environment variable not set'); + } + + const contractAddress = process.env.CONTRACT_ADDRESS || '0xD55496144F8CD69046656ddd5bb894c8b0C2d1b1'; + const rpcUrl = process.env.POLYGON_RPC || 'https://1rpc.io/matic'; + const recipient = recipientAddress || process.env.MINT_RECIPIENT; + + if (!recipient) { + throw new Error('No recipient address provided and MINT_RECIPIENT not set'); + } + + // Connect to Polygon with a signer (wallet) + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + const contract = new ethers.Contract(contractAddress, CONTRACT_ABI, wallet); + + // Check MATIC balance + const balance = await provider.getBalance(wallet.address); + const balanceMatic = parseFloat(ethers.formatEther(balance)); + if (balanceMatic < 0.01) { + throw new Error(`Low MATIC balance: ${balanceMatic.toFixed(4)} MATIC. Need at least 0.01 to mint.`); + } + + // Check if already minted (idempotency) + const isMinted = await contract.isCoaMinted(coaCode); + if (isMinted) { + const tokenId = await contract.getTokenIdByCoaCode(coaCode); + const owner = await contract.getCoaOwner(coaCode); + console.log(`COA ${coaCode} already minted as token #${tokenId}`); + return { + alreadyMinted: true, + tokenId: tokenId.toString(), + owner, + contractAddress, + polygonscanUrl: `https://polygonscan.com/token/${contractAddress}?a=${tokenId}`, + openSeaUrl: `https://opensea.io/assets/matic/${contractAddress}/${tokenId}` + }; + } + + // Build metadata URI pointing to the backend API + const metadataUri = `https://coa.up.railway.app/api/nft/${coaCode}`; + + console.log(`Minting COA ${coaCode} to ${recipient}...`); + + // Send mint transaction + const tx = await contract.mintCOA(recipient, coaCode, metadataUri); + console.log(`Mint tx sent: ${tx.hash}`); + + // Wait for confirmation + const receipt = await tx.wait(); + console.log(`Confirmed in block ${receipt.blockNumber}`); + + // Get the token ID + const tokenId = await contract.getTokenIdByCoaCode(coaCode); + + const result = { + alreadyMinted: false, + tokenId: tokenId.toString(), + owner: recipient, + txHash: tx.hash, + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed.toString(), + contractAddress, + polygonscanUrl: `https://polygonscan.com/token/${contractAddress}?a=${tokenId}`, + openSeaUrl: `https://opensea.io/assets/matic/${contractAddress}/${tokenId}`, + metadataUri + }; + + console.log(`Minted COA ${coaCode} -> Token #${tokenId}`); + return result; +} + +/** + * Mint a COA NFT (queued to prevent nonce collisions) + * + * @param {string} coaCode - The COA code to mint + * @param {string} [recipientAddress] - Wallet to receive the NFT (defaults to MINT_RECIPIENT) + * @returns {Promise} Mint result with tokenId, txHash, URLs + */ +function mintCOA(coaCode, recipientAddress) { + return new Promise((resolve, reject) => { + mintQueue.push({ coaCode, recipientAddress, resolve, reject }); + processMintQueue(); + }); +} + +/** + * Check if a COA code has been minted + */ +async function isCoaMinted(coaCode) { + const contractAddress = process.env.CONTRACT_ADDRESS || '0xD55496144F8CD69046656ddd5bb894c8b0C2d1b1'; + const rpcUrl = process.env.POLYGON_RPC || 'https://1rpc.io/matic'; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const contract = new ethers.Contract(contractAddress, CONTRACT_ABI, provider); + return contract.isCoaMinted(coaCode); +} + +/** + * Get wallet MATIC balance + */ +async function getWalletBalance() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) return null; + + const rpcUrl = process.env.POLYGON_RPC || 'https://1rpc.io/matic'; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + const balance = await provider.getBalance(wallet.address); + return { + address: wallet.address, + balanceMatic: parseFloat(ethers.formatEther(balance)), + balanceWei: balance.toString() + }; +} + +module.exports = { mintCOA, isCoaMinted, getWalletBalance }; diff --git a/gauntlet-coa-autopost/backend/services/scoreDetect.js b/gauntlet-coa-autopost/backend/services/scoreDetect.js new file mode 100644 index 0000000..eed6366 --- /dev/null +++ b/gauntlet-coa-autopost/backend/services/scoreDetect.js @@ -0,0 +1,131 @@ +/** + * ============================================================================ + * TRUECOA - SCOREDETECT INTEGRATION SERVICE + * ============================================================================ + * + * Integrates with ScoreDetect (scoredetect.com) to create blockchain-verified + * certificates of authenticity on Polygon. + * + * ScoreDetect provides content certification - it hashes certificate content + * and registers it on the Polygon blockchain, creating an immutable proof + * of the certificate's existence and content at a specific point in time. + */ + +const https = require('https'); +const http = require('http'); + +const SCOREDETECT_API_BASE = 'https://api.scoredetect.com/v1'; + +/** + * Create a ScoreDetect certificate for a COA + * + * @param {Object} coaData - COA metadata from Google Sheets + * @param {string} coaData.coaCode - The COA code + * @param {string} coaData.artist - Artist name + * @param {string} coaData.title - Artwork title + * @param {string} [coaData.description] - Description + * @param {string} [coaData.imageUrl] - Image URL + * @returns {Promise} ScoreDetect result with certificateUrl + */ +async function createCertificate(coaData) { + const apiKey = process.env.SCOREDETECT_API_KEY; + + if (!apiKey) { + console.log('SCOREDETECT_API_KEY not set, skipping ScoreDetect certification'); + return { skipped: true, reason: 'API key not configured' }; + } + + try { + // Build certificate content to be hashed and registered + const certContent = { + type: 'certificate_of_authenticity', + version: '1.0', + issuer: 'TrueCOA', + coaCode: coaData.coaCode, + artist: coaData.artist, + title: coaData.title, + description: coaData.description || '', + medium: coaData.medium || '', + edition: coaData.edition || '', + condition: coaData.condition || '', + provenance: coaData.provenance || '', + imageUrl: coaData.imageUrl || '', + timestamp: new Date().toISOString(), + network: 'polygon' + }; + + // Submit to ScoreDetect API + const response = await fetch(`${SCOREDETECT_API_BASE}/certificates`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + content: JSON.stringify(certContent), + title: `TrueCOA #${coaData.coaCode} - ${coaData.title}`, + description: `Certificate of Authenticity for "${coaData.title}" by ${coaData.artist}. Verified by TrueCOA.`, + metadata: { + coaCode: coaData.coaCode, + artist: coaData.artist, + title: coaData.title + } + }) + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`ScoreDetect API error ${response.status}: ${errText}`); + } + + const result = await response.json(); + + return { + skipped: false, + certificateId: result.id || result.certificate_id, + certificateUrl: result.url || result.certificate_url || result.verification_url, + transactionHash: result.transaction_hash || result.tx_hash, + timestamp: result.timestamp || new Date().toISOString() + }; + } catch (error) { + console.error(`ScoreDetect error for COA ${coaData.coaCode}:`, error.message); + return { + skipped: false, + error: error.message + }; + } +} + +/** + * Verify an existing ScoreDetect certificate + * + * @param {string} certificateId - The ScoreDetect certificate ID + * @returns {Promise} Verification result + */ +async function verifyCertificate(certificateId) { + const apiKey = process.env.SCOREDETECT_API_KEY; + if (!apiKey) return { verified: false, reason: 'API key not configured' }; + + try { + const response = await fetch(`${SCOREDETECT_API_BASE}/certificates/${certificateId}`, { + headers: { + 'Authorization': `Bearer ${apiKey}` + } + }); + + if (!response.ok) { + return { verified: false, reason: `API error: ${response.status}` }; + } + + const result = await response.json(); + return { + verified: true, + certificateUrl: result.url || result.certificate_url, + status: result.status + }; + } catch (error) { + return { verified: false, reason: error.message }; + } +} + +module.exports = { createCertificate, verifyCertificate }; diff --git a/gauntlet-coa-autopost/backend/services/sheetWriter.js b/gauntlet-coa-autopost/backend/services/sheetWriter.js new file mode 100644 index 0000000..053e852 --- /dev/null +++ b/gauntlet-coa-autopost/backend/services/sheetWriter.js @@ -0,0 +1,240 @@ +/** + * ============================================================================ + * TRUECOA - GOOGLE SHEETS WRITER SERVICE + * ============================================================================ + * + * Writes automation results back to Google Sheets. + * Upgrades the existing read-only Sheets integration to read/write. + * + * Column mapping (0-indexed, matches COA2 sheet with 21 columns A-U): + * A(0): COA_CODE B(1): QR_CODE C(2): SIGNER + * D(3): TITLE E(4): DATE F(5): SIZE + * G(6): CONDITION H(7): DESCRIPTION I(8): PROVENANCE + * J(9): EDITION K(10): MEDIUM L(11): ASSIGNEE + * M(12): THIRD_PARTY_AUTH_NOTES N(13): IMAGE_URL + * O(14): NFT_TOKEN_ID P(15): SHORT_URL Q(16): BLOCKCHAIN_URL + * R(17): NFT_URL S(18): CERT_URL T(19): STATUS + * U(20): COMPLETION_DATE + */ + +const { google } = require('googleapis'); + +// Column indices for write-back (0-indexed) +const COLUMNS = { + COA_CODE: 0, // A + QR_CODE: 1, // B + SIGNER: 2, // C + TITLE: 3, // D + DATE: 4, // E + SIZE: 5, // F + CONDITION: 6, // G + DESCRIPTION: 7, // H + PROVENANCE: 8, // I (header says "Provience" - misspelled) + EDITION: 9, // J + MEDIUM: 10, // K + ASSIGNEE: 11, // L + THIRD_PARTY_AUTH_NOTES: 12, // M + IMAGE_URL: 13, // N + NFT_TOKEN_ID: 14, // O + SHORT_URL: 15, // P + BLOCKCHAIN_URL: 16, // Q + NFT_URL: 17, // R + CERT_URL: 18, // S + STATUS: 19, // T + COMPLETION_DATE: 20 // U +}; + +let sheetsClient = null; + +/** + * Initialize Google Sheets API with read/write scope + */ +async function initSheetsWriter() { + if (!process.env.GOOGLE_CREDENTIALS) { + console.log('Warning: GOOGLE_CREDENTIALS not set, Sheets writer disabled'); + return; + } + + try { + let credentialsJson = process.env.GOOGLE_CREDENTIALS; + if (!credentialsJson.trim().startsWith('{')) { + credentialsJson = Buffer.from(credentialsJson, 'base64').toString('utf8'); + } + + const credentials = JSON.parse(credentialsJson); + const auth = new google.auth.GoogleAuth({ + credentials, + // Upgraded to full read/write scope + scopes: ['https://www.googleapis.com/auth/spreadsheets'] + }); + + sheetsClient = google.sheets({ version: 'v4', auth }); + console.log('Sheets writer initialized (read/write)'); + } catch (err) { + console.error('Failed to init Sheets writer:', err.message); + } +} + +/** + * Convert column index (0-based) to A1 notation letter + */ +function colToLetter(col) { + let letter = ''; + let temp = col; + while (temp >= 0) { + letter = String.fromCharCode((temp % 26) + 65) + letter; + temp = Math.floor(temp / 26) - 1; + } + return letter; +} + +/** + * Update a single cell in the sheet + */ +async function updateCell(spreadsheetId, sheetName, row, col, value) { + if (!sheetsClient) throw new Error('Sheets writer not initialized'); + + const cell = `${sheetName}!${colToLetter(col)}${row}`; + await sheetsClient.spreadsheets.values.update({ + spreadsheetId, + range: cell, + valueInputOption: 'RAW', + requestBody: { values: [[value]] } + }); +} + +/** + * Write automation results for a COA row + * + * @param {number} rowNumber - Sheet row number (1-indexed) + * @param {Object} results - Automation results to write + * @param {string} [results.tokenId] - NFT token ID + * @param {string} [results.polygonscanUrl] - Polygonscan transaction URL + * @param {string} [results.openSeaUrl] - OpenSea NFT URL + * @param {string} [results.certificateUrl] - ScoreDetect certificate URL + * @param {string} [results.shortUrl] - Bit.ly short URL + * @param {string} [results.status] - Status message + */ +async function writeResults(rowNumber, results) { + const spreadsheetId = process.env.SPREADSHEET_ID; + const sheetName = process.env.SHEET_NAME || 'COA2'; + + if (!spreadsheetId) throw new Error('SPREADSHEET_ID not set'); + if (!sheetsClient) throw new Error('Sheets writer not initialized'); + + const updates = []; + + if (results.tokenId) { + updates.push([colToLetter(COLUMNS.NFT_TOKEN_ID), results.tokenId]); + } + if (results.polygonscanUrl) { + updates.push([colToLetter(COLUMNS.BLOCKCHAIN_URL), results.polygonscanUrl]); + } + if (results.openSeaUrl) { + updates.push([colToLetter(COLUMNS.NFT_URL), results.openSeaUrl]); + } + if (results.certificateUrl) { + updates.push([colToLetter(COLUMNS.CERT_URL), results.certificateUrl]); + } + if (results.shortUrl) { + updates.push([colToLetter(COLUMNS.SHORT_URL), results.shortUrl]); + } + + // Always update the Status/completion column + const status = results.status || new Date().toISOString(); + updates.push([colToLetter(COLUMNS.STATUS), status]); + updates.push([colToLetter(COLUMNS.COMPLETION_DATE), new Date().toISOString()]); + + // Batch update all cells + const data = updates.map(([col, value]) => ({ + range: `${sheetName}!${col}${rowNumber}`, + values: [[value]] + })); + + await sheetsClient.spreadsheets.values.batchUpdate({ + spreadsheetId, + requestBody: { + valueInputOption: 'RAW', + data + } + }); + + console.log(`Updated row ${rowNumber} with ${updates.length} fields`); +} + +/** + * Set the status/Done column for a row (used for in-progress tracking) + */ +async function setRowStatus(rowNumber, status) { + const spreadsheetId = process.env.SPREADSHEET_ID; + const sheetName = process.env.SHEET_NAME || 'COA2'; + await updateCell(spreadsheetId, sheetName, rowNumber, COLUMNS.STATUS, status); +} + +/** + * Get all unprocessed rows (COA code present, STATUS column empty) + * + * @returns {Promise} Array of { rowNumber, coaCode, signer, title, ... } + */ +async function getUnprocessedRows() { + const spreadsheetId = process.env.SPREADSHEET_ID; + const sheetName = process.env.SHEET_NAME || 'COA2'; + + if (!sheetsClient) throw new Error('Sheets writer not initialized'); + + const response = await sheetsClient.spreadsheets.values.get({ + spreadsheetId, + range: `${sheetName}!A:U` + }); + + const rows = response.data.values; + if (!rows || rows.length < 2) return []; + + const unprocessed = []; + + for (let i = 1; i < rows.length; i++) { + const row = rows[i]; + const coaCode = (row[COLUMNS.COA_CODE] || '').toString().trim(); + const signer = (row[COLUMNS.SIGNER] || '').toString().trim(); + const title = (row[COLUMNS.TITLE] || '').toString().trim(); + const status = (row[COLUMNS.STATUS] || '').toString().trim(); + const imageUrl = (row[COLUMNS.IMAGE_URL] || '').toString().trim(); + + // Skip rows without COA code or required fields + if (!coaCode) continue; + + // Skip rows already processed or currently processing + if (status && status !== 'error') continue; + + // Require at least signer or title to be filled in + if (!signer && !title) continue; + + unprocessed.push({ + rowNumber: i + 1, // 1-indexed for Sheets API + coaCode, + signer, + title, + date: (row[COLUMNS.DATE] || '').toString().trim(), + size: (row[COLUMNS.SIZE] || '').toString().trim(), + condition: (row[COLUMNS.CONDITION] || '').toString().trim(), + description: (row[COLUMNS.DESCRIPTION] || '').toString().trim(), + provenance: (row[COLUMNS.PROVENANCE] || '').toString().trim(), + edition: (row[COLUMNS.EDITION] || '').toString().trim(), + medium: (row[COLUMNS.MEDIUM] || '').toString().trim(), + assignee: (row[COLUMNS.ASSIGNEE] || '').toString().trim(), + thirdPartyAuthNotes: (row[COLUMNS.THIRD_PARTY_AUTH_NOTES] || '').toString().trim(), + imageUrl, + shortUrl: (row[COLUMNS.SHORT_URL] || '').toString().trim() + }); + } + + return unprocessed; +} + +module.exports = { + initSheetsWriter, + writeResults, + setRowStatus, + getUnprocessedRows, + COLUMNS +}; diff --git a/gauntlet-coa-autopost/google-apps-script/AutoPostTrigger.gs b/gauntlet-coa-autopost/google-apps-script/AutoPostTrigger.gs new file mode 100644 index 0000000..d540b6b --- /dev/null +++ b/gauntlet-coa-autopost/google-apps-script/AutoPostTrigger.gs @@ -0,0 +1,297 @@ +/** + * ============================================================================ + * TRUECOA - AUTO-POST NFT TRIGGER + * ============================================================================ + * + * Google Apps Script installable trigger that fires when the COA2 sheet + * is edited. When a row has a COA code and required fields filled in, + * it calls the backend webhook to start the auto-post NFT pipeline. + * + * Setup: + * 1. Open Google Sheets > Extensions > Apps Script + * 2. Paste this file + * 3. Run setupAutoPostTrigger() once to install the trigger + * 4. Set WEBHOOK_URL and WEBHOOK_SECRET in script properties: + * File > Project properties > Script properties + * + * Pipeline flow: + * Sheet edit → This trigger → Backend webhook → Mint NFT → ScoreDetect → + * OpenSea auto-index → Write results back to sheet + */ + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +/** + * Get configuration from Script Properties + * Set these in: File > Project settings > Script properties + */ +function getAutoPostConfig() { + const props = PropertiesService.getScriptProperties(); + return { + WEBHOOK_URL: props.getProperty('WEBHOOK_URL') || 'https://coa.up.railway.app/api/automate', + WEBHOOK_SECRET: props.getProperty('WEBHOOK_SECRET') || '', + SHEET_NAME: 'COA2', + COA_CODE_COL: 0, // A: COA_CODE + SIGNER_COL: 2, // C: SIGNER + TITLE_COL: 3, // D: TITLE + STATUS_COL: 19 // T: STATUS + }; +} + +// ============================================================================ +// TRIGGER SETUP +// ============================================================================ + +/** + * Install the auto-post trigger. + * Run this function ONCE from the Apps Script editor. + * + * Creates an installable onChange trigger (required for UrlFetchApp access). + * Simple onEdit triggers cannot make external HTTP calls. + */ +function setupAutoPostTrigger() { + // Remove existing auto-post triggers to prevent duplicates + const triggers = ScriptApp.getProjectTriggers(); + triggers.forEach(trigger => { + if (trigger.getHandlerFunction() === 'onSheetEditAutoPost') { + ScriptApp.deleteTrigger(trigger); + } + }); + + // Create new installable trigger + ScriptApp.newTrigger('onSheetEditAutoPost') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onEdit() + .create(); + + Logger.log('Auto-post trigger installed successfully.'); + SpreadsheetApp.getUi().alert( + 'Auto-Post Trigger Installed', + 'The auto-post NFT trigger is now active.\n\n' + + 'When you fill in a COA row (COA Code + Artist/Title), it will automatically:\n' + + '1. Mint an NFT on Polygon\n' + + '2. Create a ScoreDetect certificate\n' + + '3. Register on OpenSea\n' + + '4. Write results back to the sheet\n\n' + + 'Make sure WEBHOOK_URL and WEBHOOK_SECRET are set in Script Properties.', + SpreadsheetApp.getUi().ButtonSet.OK + ); +} + +/** + * Remove the auto-post trigger + */ +function removeAutoPostTrigger() { + const triggers = ScriptApp.getProjectTriggers(); + let removed = 0; + triggers.forEach(trigger => { + if (trigger.getHandlerFunction() === 'onSheetEditAutoPost') { + ScriptApp.deleteTrigger(trigger); + removed++; + } + }); + Logger.log(`Removed ${removed} auto-post trigger(s).`); + SpreadsheetApp.getUi().alert(`Removed ${removed} auto-post trigger(s).`); +} + +// ============================================================================ +// TRIGGER HANDLER +// ============================================================================ + +/** + * Installable onEdit handler - fires when any cell is edited in the sheet. + * + * Checks if the edited row in COA2 has: + * - A COA code (column A) + * - At least signer OR title filled in + * - STATUS column (T) is empty (not yet processed) + * + * If all conditions met, calls the backend webhook to start processing. + * + * @param {Object} e - Edit event object + */ +function onSheetEditAutoPost(e) { + try { + const config = getAutoPostConfig(); + const sheet = e.source.getActiveSheet(); + + // Only trigger on the COA2 sheet + if (sheet.getName() !== config.SHEET_NAME) return; + + // Get the edited range + const range = e.range; + const row = range.getRow(); + + // Skip header row + if (row <= 1) return; + + // Get row data + const rowData = sheet.getRange(row, 1, 1, 21).getValues()[0]; + + // Check required fields + const coaCode = String(rowData[config.COA_CODE_COL] || '').trim(); + const signer = String(rowData[config.SIGNER_COL] || '').trim(); + const title = String(rowData[config.TITLE_COL] || '').trim(); + const status = String(rowData[config.STATUS_COL] || '').trim(); + + // Skip if no COA code + if (!coaCode) return; + + // Skip if already processed or processing + if (status && status !== 'error') return; + + // Require at least signer or title + if (!signer && !title) return; + + // Call the backend webhook + Logger.log(`Auto-post triggered for COA: ${coaCode} (Row ${row})`); + callWebhook(config, coaCode, row); + + } catch (error) { + Logger.log(`Auto-post trigger error: ${error.message}`); + } +} + +/** + * Call the backend webhook to start the automation pipeline + * + * @param {Object} config - Configuration + * @param {string} coaCode - COA code to process + * @param {number} rowNumber - Sheet row number + */ +function callWebhook(config, coaCode, rowNumber) { + try { + const url = `${config.WEBHOOK_URL}/${encodeURIComponent(coaCode)}`; + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': config.WEBHOOK_SECRET + }, + payload: JSON.stringify({ + coaCode: coaCode, + rowNumber: rowNumber, + source: 'google-apps-script', + timestamp: new Date().toISOString() + }), + muteHttpExceptions: true, + // 10 second timeout - webhook returns immediately, processing is async + followRedirects: true + }; + + const response = UrlFetchApp.fetch(url, options); + const code = response.getResponseCode(); + const body = response.getContentText(); + + if (code >= 200 && code < 300) { + Logger.log(`Webhook OK for ${coaCode}: ${body}`); + } else { + Logger.log(`Webhook error for ${coaCode} (${code}): ${body}`); + } + } catch (error) { + Logger.log(`Webhook call failed for ${coaCode}: ${error.message}`); + } +} + +// ============================================================================ +// MENU INTEGRATION +// ============================================================================ + +/** + * Add auto-post options to the existing TrueCOA menu + * Call this from onOpen() or add to the existing menu setup + */ +function addAutoPostMenu() { + const ui = SpreadsheetApp.getUi(); + ui.createMenu('Auto-Post NFT') + .addItem('Setup Auto-Post Trigger', 'setupAutoPostTrigger') + .addItem('Remove Auto-Post Trigger', 'removeAutoPostTrigger') + .addSeparator() + .addItem('Process All Unprocessed Rows', 'triggerPollAll') + .addItem('Process Selected Row', 'triggerProcessSelected') + .addSeparator() + .addItem('Check Pipeline Status', 'checkPipelineStatus') + .addToUi(); +} + +/** + * Trigger polling for all unprocessed rows + */ +function triggerPollAll() { + const config = getAutoPostConfig(); + try { + const response = UrlFetchApp.fetch(`${config.WEBHOOK_URL}/poll`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': config.WEBHOOK_SECRET + }, + muteHttpExceptions: true + }); + + const result = JSON.parse(response.getContentText()); + SpreadsheetApp.getUi().alert( + 'Poll Result', + `Processed: ${result.processed || 0} row(s)\n${result.message || ''}`, + SpreadsheetApp.getUi().ButtonSet.OK + ); + } catch (e) { + SpreadsheetApp.getUi().alert('Error: ' + e.message); + } +} + +/** + * Process the currently selected row + */ +function triggerProcessSelected() { + const config = getAutoPostConfig(); + const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(config.SHEET_NAME); + const row = sheet.getActiveRange().getRow(); + + if (row <= 1) { + SpreadsheetApp.getUi().alert('Please select a data row (not the header).'); + return; + } + + const coaCode = String(sheet.getRange(row, 1).getValue()).trim(); + if (!coaCode) { + SpreadsheetApp.getUi().alert('Selected row has no COA code.'); + return; + } + + callWebhook(config, coaCode, row); + SpreadsheetApp.getUi().alert(`Processing started for COA: ${coaCode}\nCheck column T for status.`); +} + +/** + * Check the backend pipeline status + */ +function checkPipelineStatus() { + const config = getAutoPostConfig(); + try { + const response = UrlFetchApp.fetch(`${config.WEBHOOK_URL}/status`, { + muteHttpExceptions: true + }); + + const status = JSON.parse(response.getContentText()); + const walletBalance = status.wallet?.balanceMatic?.toFixed(4) || 'unknown'; + + SpreadsheetApp.getUi().alert( + 'Pipeline Status', + `Status: ${status.status}\n` + + `Unprocessed rows: ${status.unprocessedRows}\n` + + `Currently processing: ${status.currentlyProcessing?.length || 0}\n` + + `Wallet balance: ${walletBalance} MATIC\n\n` + + `Config:\n` + + ` Private key: ${status.config?.privateKeySet ? 'SET' : 'NOT SET'}\n` + + ` ScoreDetect key: ${status.config?.scoreDetectKeySet ? 'SET' : 'NOT SET'}\n` + + ` OpenSea key: ${status.config?.openSeaKeySet ? 'SET' : 'NOT SET'}`, + SpreadsheetApp.getUi().ButtonSet.OK + ); + } catch (e) { + SpreadsheetApp.getUi().alert('Error: ' + e.message); + } +} diff --git a/gauntlet-coa-autopost/google-apps-script/COAGenerator_Combined.gs b/gauntlet-coa-autopost/google-apps-script/COAGenerator_Combined.gs new file mode 100644 index 0000000..3fe47ce --- /dev/null +++ b/gauntlet-coa-autopost/google-apps-script/COAGenerator_Combined.gs @@ -0,0 +1,1280 @@ +/** + * ============================================================================ + * TRUECOA - COA CERTIFICATE GENERATOR (COMBINED) + * ============================================================================ + * + * Unified Google Apps Script for automated Certificate of Authenticity generation. + * Combines main generator, mobile view, PDF view, and preview functions. + * + * Features: + * - Reads artwork data from Google Sheets COA2 tab + * - Generates Bit.ly short links for verification URLs + * - Creates certificate HTML/PDF with QR codes (desktop, mobile, PDF formats) + * - Saves certificates to Google Drive + * - Updates spreadsheet with generated links + * - Supports fields: Medium, Condition, Description, Notes/Provenance + * + * Setup: + * 1. Open Google Sheets + * 2. Extensions > Apps Script + * 3. Paste this code + * 4. Run setupTriggers() once to create menu + * + * Author: John Shay / TrueCOA + * ============================================================================ + */ + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const CONFIG = { + // Bit.ly API + BITLY_API_KEY: '485a216ca4141d6f381d6d16bf1ae5ef33a4e49e', + BITLY_API_URL: 'https://api-ssl.bitly.com/v4/shorten', + + // Verification URLs + VERCEL_FRONTEND_URL: 'https://gauntlet-coa-frontend.vercel.app', + + // Google Drive folder for certificates (will be created if not exists) + DRIVE_FOLDER_NAME: 'TrueCOA Certificates', + + // Sheet configuration + SHEET_NAME: 'COA2', + + // Column mapping (0-indexed) - matches COA2 sheet (21 columns, A-U) + COLUMNS: { + COA_CODE: 0, // A: COA_Code + QR_CODE: 1, // B: QR_Code (output - QR image URL) + SIGNER: 2, // C: Signer + TITLE: 3, // D: Title + DATE: 4, // E: Date + SIZE: 5, // F: Size + CONDITION: 6, // G: Condition + DESCRIPTION: 7, // H: Description + PROVENANCE: 8, // I: Provenance (header says "Provience" - misspelled) + EDITION: 9, // J: Edition + MEDIUM: 10, // K: Medium + ASSIGNEE: 11, // L: Assignee + THIRD_PARTY_AUTH_NOTES: 12, // M: Third Party Auth Notes + IMAGE_URL: 13, // N: Image_URL + NFT_TOKEN_ID: 14, // O: NFT_TokenID + SHORT_URL: 15, // P: Short_URL (output - Bit.ly link) + BLOCKCHAIN_URL: 16, // Q: Blockchain_URL + NFT_URL: 17, // R: NFT_URL + CERT_URL: 18, // S: Cert_URL (output - Google Drive link) + STATUS: 19, // T: Status / Done + COMPLETION_DATE: 20 // U: Completion Date (output - timestamp) + }, + + // Certificate styling + CERT_WIDTH: 800, + CERT_HEIGHT: 1100, + QR_SIZE: 200 +}; + +// ============================================================================ +// LOGO IMAGES (Base64 encoded for embedding in HTML templates) +// ============================================================================ + +const LOGO_SCOREDETECT_URL = 'data:image/png;base64,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'; +const LOGO_POLYGON_URL = 'data:image/png;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwK/8LTvPc12h0TasAAAAASUVORK5CYII='; +const LOGO_GAUNTLET_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAIAAABEtEjdAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEvWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLyc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpBdHRyaWI9J2h0dHA6Ly9ucy5hdHRyaWJ1dGlvbi5jb20vYWRzLzEuMC8nPgogIDxBdHRyaWI6QWRzPgogICA8cmRmOlNlcT4KICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0nUmVzb3VyY2UnPgogICAgIDxBdHRyaWI6Q3JlYXRlZD4yMDI1LTExLTA3PC9BdHRyaWI6Q3JlYXRlZD4KICAgICA8QXR0cmliOkV4dElkPjQ3ZTA3MjIyLThjYzYtNDBkYS04NjgwLTEwYTJhMmQwYzIxMjwvQXR0cmliOkV4dElkPgogICAgIDxBdHRyaWI6RmJJZD41MjUyNjU5MTQxNzk1ODA8L0F0dHJpYjpGYklkPgogICAgIDxBdHRyaWI6VG91Y2hUeXBlPjI8L0F0dHJpYjpUb3VjaFR5cGU+CiAgICA8L3JkZjpsaT4KICAgPC9yZGY6U2VxPgogIDwvQXR0cmliOkFkcz4KIDwvcmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PScnCiAgeG1sbnM6ZGM9J2h0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvJz4KICA8ZGM6dGl0bGU+CiAgIDxyZGY6QWx0PgogICAgPHJkZjpsaSB4bWw6bGFuZz0neC1kZWZhdWx0Jz5VbnRpdGxlZCBkZXNpZ24gLSA2PC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOnRpdGxlPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlv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'; + +// ============================================================================ +// MENU & TRIGGERS +// ============================================================================ + +/** + * Creates custom menu when spreadsheet opens + */ +function onOpen() { + SpreadsheetApp.getUi().createMenu('\u{1F3A8} COA Generator') + .addItem('\u{1F4F1} Digital - Mobile View', 'generateMobile') + .addItem('\u{1F4C4} PDF (Large View)', 'generatePDFView') + .addItem('Preview Certificate HTML', 'previewCert') + .addSeparator() + .addItem('Generate All Certificates', 'generateAllCertificates') + .addItem('Generate Selected Row', 'generateSelectedRow') + .addSeparator() + .addItem('Create Short Links Only', 'createShortLinksOnly') + .addItem('Refresh QR Codes', 'refreshQRCodes') + .addSeparator() + .addItem('Test Bit.ly', 'testBitlyAPI') + .addItem('Setup/Check Folders', 'setupFolders') + .addToUi(); +} + +/** + * Run once to setup triggers + */ +function setupTriggers() { + const triggers = ScriptApp.getProjectTriggers(); + const hasOnOpen = triggers.some(t => t.getHandlerFunction() === 'onOpen'); + + if (!hasOnOpen) { + ScriptApp.newTrigger('onOpen') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onOpen() + .create(); + } + + Logger.log('Triggers setup complete. Menu will appear on next sheet open.'); +} + +// ============================================================================ +// MAIN GENERATION FUNCTIONS +// ============================================================================ + +/** + * Generate certificates for all rows that don't have one yet + */ +function generateAllCertificates() { + const sheet = getSheet(); + const data = sheet.getDataRange().getValues(); + const folder = getOrCreateFolder(); + + let generated = 0; + let errors = []; + + for (let i = 1; i < data.length; i++) { + const row = data[i]; + const coaCode = row[CONFIG.COLUMNS.COA_CODE]; + const alreadyGenerated = row[CONFIG.COLUMNS.STATUS] || row[CONFIG.COLUMNS.COMPLETION_DATE]; + + if (!coaCode || alreadyGenerated) { + continue; + } + + try { + const result = processRow(sheet, i + 1, row, folder); + if (result) { + generated++; + } + } catch (e) { + errors.push(`Row ${i + 1} (${coaCode}): ${e.message}`); + Logger.log(`Error processing row ${i + 1}: ${e.message}`); + } + + Utilities.sleep(600); + } + + const ui = SpreadsheetApp.getUi(); + let message = `Generated ${generated} certificates.`; + if (errors.length > 0) { + message += `\n\nErrors (${errors.length}):\n${errors.slice(0, 5).join('\n')}`; + if (errors.length > 5) { + message += `\n...and ${errors.length - 5} more`; + } + } + ui.alert('Generation Complete', message, ui.ButtonSet.OK); +} + +/** + * Generate certificate for the currently selected row + */ +function generateSelectedRow() { + const sheet = getSheet(); + const activeRange = sheet.getActiveRange(); + const rowNum = activeRange.getRow(); + + if (rowNum <= 1) { + SpreadsheetApp.getUi().alert('Please select a data row (not the header).'); + return; + } + + const row = sheet.getRange(rowNum, 1, 1, sheet.getLastColumn()).getValues()[0]; + const folder = getOrCreateFolder(); + + try { + const result = processRow(sheet, rowNum, row, folder); + SpreadsheetApp.getUi().alert('Certificate Generated!', + `HTML: ${result.htmlUrl}\n\nPDF: ${result.pdfUrl}`, + SpreadsheetApp.getUi().ButtonSet.OK); + } catch (e) { + SpreadsheetApp.getUi().alert(`Error: ${e.message}`); + } +} + +/** + * Generate Mobile/Digital view for selected row + */ +function generateMobile() { + const sheet = getSheet(); + const row = sheet.getActiveRange().getRow(); + if (row <= 1) { SpreadsheetApp.getUi().alert('Select a data row'); return; } + + const data = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0]; + const code = String(data[CONFIG.COLUMNS.COA_CODE]).trim(); + const folder = getOrCreateFolder(); + + const certData = buildCertData(data); + const html = generateMobileHTML(certData); + + const mobileFile = folder.createFile(`COA_Mobile_${code}.html`, html, MimeType.HTML); + + SpreadsheetApp.getUi().alert('Mobile Certificate Created', + `Open this URL on your phone or desktop:\n\n${mobileFile.getUrl()}`, + SpreadsheetApp.getUi().ButtonSet.OK); +} + +/** + * Generate PDF/Large view for selected row + */ +function generatePDFView() { + const sheet = getSheet(); + const row = sheet.getActiveRange().getRow(); + if (row <= 1) { SpreadsheetApp.getUi().alert('Select a data row'); return; } + + const data = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0]; + const code = String(data[CONFIG.COLUMNS.COA_CODE]).trim(); + const folder = getOrCreateFolder(); + + const certData = buildCertData(data); + const html = generatePDFHTML(certData); + + const htmlFile = folder.createFile(`COA_PDF_${code}.html`, html, MimeType.HTML); + + const pdfBlob = htmlFile.getAs(MimeType.PDF); + pdfBlob.setName(`COA_${code}_${certData.artist.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`); + const pdfFile = folder.createFile(pdfBlob); + + SpreadsheetApp.getUi().alert('PDF Certificate Created', + `HTML Preview: ${htmlFile.getUrl()}\n\nPDF Download: ${pdfFile.getUrl()}`, + SpreadsheetApp.getUi().ButtonSet.OK); +} + +/** + * Preview certificate for selected row without generating short links + */ +function previewCert() { + const sheet = getSheet(); + const row = sheet.getActiveRange().getRow(); + if (row <= 1) { SpreadsheetApp.getUi().alert('Select a data row'); return; } + + const data = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0]; + const code = String(data[CONFIG.COLUMNS.COA_CODE]).trim(); + + const certData = buildCertData(data); + // Use placeholder data for preview if missing + if (!certData.code) certData.code = 'SAMPLE'; + if (certData.artist === 'Unknown Artist') certData.artist = 'Sample Artist'; + if (certData.title === 'Untitled') certData.title = 'Sample Artwork'; + if (!certData.year) certData.year = '2025'; + if (certData.dims === '-') certData.dims = '24" x 18"'; + if (certData.edition === '-') certData.edition = '1 of 50'; + if (certData.assignee === '-') certData.assignee = 'Collector Name'; + if (!certData.image) certData.image = 'https://via.placeholder.com/400x500?text=Artwork+Image'; + + const html = generateCertHTML(certData); + const folder = getOrCreateFolder(); + const preview = folder.createFile(`preview_${code || 'sample'}.html`, html, MimeType.HTML); + + SpreadsheetApp.getUi().alert('Preview Created', + `Open this URL to preview:\n\n${preview.getUrl()}`, + SpreadsheetApp.getUi().ButtonSet.OK); +} + +// ============================================================================ +// ROW PROCESSING +// ============================================================================ + +/** + * Build certificate data object from a spreadsheet row (uses COA2 column mapping) + */ +function buildCertData(data) { + const code = String(data[CONFIG.COLUMNS.COA_CODE] || '').trim(); + const today = new Date(); + const transactionDate = today.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); + const short = data[CONFIG.COLUMNS.SHORT_URL] || `${CONFIG.VERCEL_FRONTEND_URL}/AUTHENTICATE/${code}`; + + return { + code: code, + short: short, + qr: `https://api.qrserver.com/v1/create-qr-code/?size=${CONFIG.QR_SIZE}x${CONFIG.QR_SIZE}&data=${encodeURIComponent(short)}`, + artist: data[CONFIG.COLUMNS.SIGNER] || 'Unknown Artist', + title: data[CONFIG.COLUMNS.TITLE] || 'Untitled', + year: data[CONFIG.COLUMNS.DATE] || '', + dims: data[CONFIG.COLUMNS.SIZE] || '-', + edition: data[CONFIG.COLUMNS.EDITION] || '-', + medium: data[CONFIG.COLUMNS.MEDIUM] || '', + condition: data[CONFIG.COLUMNS.CONDITION] || '', + description: data[CONFIG.COLUMNS.DESCRIPTION] || '', + notes: data[CONFIG.COLUMNS.PROVENANCE] || '', + assignee: data[CONFIG.COLUMNS.ASSIGNEE] || '-', + image: data[CONFIG.COLUMNS.IMAGE_URL] || '', + transactionDate: transactionDate, + blockchainUrl: data[CONFIG.COLUMNS.BLOCKCHAIN_URL] || short, + nftUrl: data[CONFIG.COLUMNS.NFT_URL] || short + }; +} + +/** + * Process a single row - create short link, QR code, certificate HTML + PDF + */ +function processRow(sheet, rowNum, row, folder) { + const code = String(row[CONFIG.COLUMNS.COA_CODE]).trim(); + if (!code) return false; + + // Build verification URL + const verifyUrl = `${CONFIG.VERCEL_FRONTEND_URL}/AUTHENTICATE/${code}`; + + // Create short link + const shortUrl = createBitlyShortLink(verifyUrl, code) || verifyUrl; + + // Generate QR code URL + const qrUrl = generateQRCodeUrl(shortUrl); + + // Build cert data using shared builder + const certData = buildCertData(row); + // Override with freshly generated short/qr + certData.short = shortUrl; + certData.qr = qrUrl; + + // Create HTML certificate + const html = generateCertHTML(certData); + const htmlFile = folder.createFile(`COA_${code}.html`, html, MimeType.HTML); + + // Also create PDF version + const pdfBlob = htmlFile.getAs(MimeType.PDF); + pdfBlob.setName(`COA_${code}_${certData.artist.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`); + const pdfFile = folder.createFile(pdfBlob); + + // Update spreadsheet with generated data + const COL = CONFIG.COLUMNS; + sheet.getRange(rowNum, COL.QR_CODE + 1).setValue(qrUrl); + sheet.getRange(rowNum, COL.SHORT_URL + 1).setValue(shortUrl); + sheet.getRange(rowNum, COL.CERT_URL + 1).setValue(htmlFile.getUrl()); + sheet.getRange(rowNum, COL.STATUS + 1).setValue(new Date().toISOString()); + sheet.getRange(rowNum, COL.COMPLETION_DATE + 1).setValue(new Date().toISOString()); + + Logger.log(`Generated: ${code} -> ${htmlFile.getUrl()}`); + + return { + htmlUrl: htmlFile.getUrl(), + pdfUrl: pdfFile.getUrl() + }; +} + +// ============================================================================ +// BIT.LY API FUNCTIONS +// ============================================================================ + +/** + * Create a Bit.ly short link for a verification URL + */ +function createBitlyShortLink(longUrl, coaCode) { + try { + const payload = { + long_url: longUrl, + title: `COA-${coaCode}` + }; + + const options = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${CONFIG.BITLY_API_KEY}`, + 'Content-Type': 'application/json' + }, + payload: JSON.stringify(payload), + muteHttpExceptions: true + }; + + const response = UrlFetchApp.fetch(CONFIG.BITLY_API_URL, options); + const result = JSON.parse(response.getContentText()); + + if (response.getResponseCode() < 300) { + return result.link; + } else { + Logger.log(`Bit.ly error for ${coaCode}: ${JSON.stringify(result)}`); + return null; + } + } catch (e) { + Logger.log(`Bit.ly API error: ${e.message}`); + return null; + } +} + +/** + * Create short links for all rows without them + */ +function createShortLinksOnly() { + const sheet = getSheet(); + const data = sheet.getDataRange().getValues(); + let created = 0; + + for (let i = 1; i < data.length; i++) { + const row = data[i]; + const coaCode = row[CONFIG.COLUMNS.COA_CODE]; + const existingShortUrl = row[CONFIG.COLUMNS.SHORT_URL]; + + if (!coaCode || existingShortUrl) { + continue; + } + + const verifyUrl = `${CONFIG.VERCEL_FRONTEND_URL}/AUTHENTICATE/${coaCode}`; + const shortUrl = createBitlyShortLink(verifyUrl, coaCode); + + if (shortUrl) { + sheet.getRange(i + 1, CONFIG.COLUMNS.SHORT_URL + 1).setValue(shortUrl); + created++; + } + + Utilities.sleep(600); + } + + SpreadsheetApp.getUi().alert(`Created ${created} short links.`); +} + +// ============================================================================ +// QR CODE FUNCTIONS +// ============================================================================ + +/** + * Generate QR code URL using qrserver.com API + */ +function generateQRCodeUrl(data) { + const encodedData = encodeURIComponent(data); + return `https://api.qrserver.com/v1/create-qr-code/?size=${CONFIG.QR_SIZE}x${CONFIG.QR_SIZE}&data=${encodedData}`; +} + +/** + * Refresh QR codes for all rows + */ +function refreshQRCodes() { + const sheet = getSheet(); + const data = sheet.getDataRange().getValues(); + let updated = 0; + + for (let i = 1; i < data.length; i++) { + const row = data[i]; + const coaCode = row[CONFIG.COLUMNS.COA_CODE]; + const shortUrl = row[CONFIG.COLUMNS.SHORT_URL]; + + if (!coaCode) continue; + + const urlForQR = shortUrl || `${CONFIG.VERCEL_FRONTEND_URL}/AUTHENTICATE/${coaCode}`; + const qrUrl = generateQRCodeUrl(urlForQR); + + sheet.getRange(i + 1, CONFIG.COLUMNS.QR_CODE + 1).setValue(qrUrl); + updated++; + } + + SpreadsheetApp.getUi().alert(`Updated ${updated} QR codes.`); +} + +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ + +/** + * Get the COA sheet + */ +function getSheet() { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = ss.getSheetByName(CONFIG.SHEET_NAME); + + if (!sheet) { + throw new Error(`Sheet "${CONFIG.SHEET_NAME}" not found. Please create it or update CONFIG.SHEET_NAME.`); + } + + return sheet; +} + +/** + * Get or create the certificates folder in Google Drive + */ +function getOrCreateFolder() { + const folders = DriveApp.getFoldersByName(CONFIG.DRIVE_FOLDER_NAME); + + if (folders.hasNext()) { + return folders.next(); + } + + return DriveApp.createFolder(CONFIG.DRIVE_FOLDER_NAME); +} + +/** + * Setup folders and verify configuration + */ +function setupFolders() { + try { + const folder = getOrCreateFolder(); + const sheet = getSheet(); + + SpreadsheetApp.getUi().alert( + 'Setup Complete', + `Drive folder: ${folder.getName()}\n` + + ` URL: ${folder.getUrl()}\n\n` + + `Sheet: ${sheet.getName()}\n` + + ` Rows: ${sheet.getLastRow() - 1} (excluding header)\n\n` + + `Bit.ly API: Configured\n` + + `Verification URL: ${CONFIG.VERCEL_FRONTEND_URL}`, + SpreadsheetApp.getUi().ButtonSet.OK + ); + } catch (e) { + SpreadsheetApp.getUi().alert('Setup Error', e.message, SpreadsheetApp.getUi().ButtonSet.OK); + } +} + +/** + * Escape HTML special characters + */ +function escapeHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/\'/g, '''); +} + +/** Short alias for escapeHtml */ +function esc(s) { + return escapeHtml(s); +} + +// ============================================================================ +// TEST FUNCTIONS +// ============================================================================ + +/** + * Test Bit.ly API connection + */ +function testBitlyAPI() { + const testUrl = 'https://gauntlet-coa-frontend.vercel.app/AUTHENTICATE/TEST123'; + const result = createBitlyShortLink(testUrl, 'TEST123'); + Logger.log(`Bit.ly test result: ${result}`); + + if (result) { + SpreadsheetApp.getUi().alert('Bit.ly API working!', `Short URL: ${result}`, SpreadsheetApp.getUi().ButtonSet.OK); + } else { + SpreadsheetApp.getUi().alert('Bit.ly API Error', 'Check logs for details.', SpreadsheetApp.getUi().ButtonSet.OK); + } +} + +/** + * Test certificate generation with sample data + */ +function testCertificateGeneration() { + const testData = { + code: 'TEST001', + artist: 'Test Artist', + title: 'Test Artwork', + year: '2025', + dims: '18" x 24"', + edition: '1 of 50', + medium: 'Screenprint on cream Speckletone paper', + condition: 'Excellent', + description: 'A vibrant screenprint showcasing bold graphic design.', + notes: 'Acquired directly from the artist at gallery opening.', + assignee: 'Test Collector', + image: '', + short: 'https://bit.ly/test', + qr: generateQRCodeUrl('https://bit.ly/test'), + transactionDate: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), + blockchainUrl: 'https://bit.ly/blockchain', + nftUrl: 'https://bit.ly/nft' + }; + + const html = generateCertHTML(testData); + Logger.log('Generated certificate HTML length: ' + html.length); + + const folder = getOrCreateFolder(); + const testFile = folder.createFile('test_certificate.html', html, MimeType.HTML); + + SpreadsheetApp.getUi().alert( + 'Test Certificate Created', + `HTML file: ${testFile.getUrl()}\n\nOpen this link to preview the certificate design.`, + SpreadsheetApp.getUi().ButtonSet.OK + ); +} + +// ============================================================================ +// CERTIFICATE HTML TEMPLATES +// ============================================================================ + +/** + * Generate certificate HTML matching the TrueCOA certificate design + * Used for the main certificate view (desktop-optimized, responsive) + * Features: gray header bar with QR, watermark background, two-column layout + */ +function generateCertHTML(d) { + return ` + + + + + Certificate of Authenticity - ${esc(d.code)} + + + +
+ + +
+

Certificate of Authenticity

+
+ QR Code +
#${esc(d.code)}
+
+
+ + +
+ ${d.image ? `
` : ''} +
+ + +
+
+ ${d.image ? `${esc(d.title)}` : '
No image
'} +
+
+ + +
+
Details
+
+ Artist: + ${esc(d.artist)} +
+
+ Title: + ${esc(d.title)} +
+ ${d.medium ? ` +
+ Medium: + ${esc(d.medium)} +
` : ''} + ${d.dims ? ` +
+ Dimensions: + ${esc(d.dims)} +
` : ''} + ${d.edition ? ` +
+ Edition: + ${esc(d.edition)} +
` : ''} + ${d.condition ? ` +
+ Condition: + ${esc(d.condition)} +
` : ''} + +
+ +
Digital Authentication
+
+ Blockchain: + ${esc(d.blockchainUrl)} +
+
+ Certificate: + ${esc(d.code)} +
+
+ Date: + ${esc(d.transactionDate)} +
+
+ Assignor: + TrueCOA +
+
+ Assignee: + ${esc(d.assignee)} +
+
+
+
+ + + + +
+ +`; +} + +/** + * Generate Mobile-optimized HTML (single-column, phone-friendly) + * Simplified version of the TrueCOA certificate design + */ +function generateMobileHTML(d) { + return ` + + + + + COA - ${esc(d.code)} + + + +
+
+

Certificate of Authenticity

+
+ QR Code +
#${esc(d.code)}
+
+
+
+ ${d.image ? `
` : ''} +
+ ${d.image ? `
${esc(d.title)}
` : ''} +
Details
+
Artist:${esc(d.artist)}
+
Title:${esc(d.title)}
+ ${d.medium ? `
Medium:${esc(d.medium)}
` : ''} + ${d.dims ? `
Dimensions:${esc(d.dims)}
` : ''} + ${d.edition ? `
Edition:${esc(d.edition)}
` : ''} + ${d.condition ? `
Condition:${esc(d.condition)}
` : ''} +
+
Digital Authentication
+
Blockchain:${esc(d.blockchainUrl)}
+
Certificate:${esc(d.code)}
+
Date:${esc(d.transactionDate)}
+
Assignor:TrueCOA
+
Assignee:${esc(d.assignee)}
+
+
+ +
+ +`; +} + +/** + * Generate PDF-optimized HTML (A4 print-ready) + * TrueCOA certificate design with watermark, optimized for print + */ +function generatePDFHTML(d) { + return ` + + + + Certificate of Authenticity - ${esc(d.code)} + + + +
+
+

Certificate of Authenticity

+
+ QR Code +
#${esc(d.code)}
+
+
+
+ ${d.image ? `
` : ''} +
+
+
+ ${d.image ? `${esc(d.title)}` : '
Artwork Image
'} +
+
+
+
Details
+
Artist:${esc(d.artist)}
+
Title:${esc(d.title)}
+ ${d.medium ? `
Medium:${esc(d.medium)}
` : ''} + ${d.dims ? `
Dimensions:${esc(d.dims)}
` : ''} + ${d.edition ? `
Edition:${esc(d.edition)}
` : ''} + ${d.condition ? `
Condition:${esc(d.condition)}
` : ''} +
+
Digital Authentication
+
Blockchain:${esc(d.blockchainUrl)}
+
Certificate:${esc(d.code)}
+
Date:${esc(d.transactionDate)}
+
Assignor:TrueCOA
+
Assignee:${esc(d.assignee)}
+
+
+
+ +
+ +`; +} diff --git a/gauntlet-coa-autopost/test_certificate_marilyn.html b/gauntlet-coa-autopost/test_certificate_marilyn.html new file mode 100644 index 0000000..b06f02b --- /dev/null +++ b/gauntlet-coa-autopost/test_certificate_marilyn.html @@ -0,0 +1,332 @@ + + + + + + Certificate of Authenticity - W2 + + + +
+ + +
+

Certificate of Authenticity

+
+ QR Code +
#W2
+
+
+ + +
+
+
+ + +
+
+ Marilyn Monroe +
+
+ + +
+
Details
+
+ Artist: + Warhol +
+
+ Title: + Marilyn Monroe (F&S II.30) +
+
+ Medium: + Screenprint on paper +
+
+ Dimensions: + 36" x 36" +
+
+ Edition: + 250 +
+
+ Condition: + Excellent +
+ +
+ +
Digital Authentication
+
+ Blockchain: + https://bit.ly/3MUgdsd +
+
+ Certificate: + 2bc3a495-bfb2-578c-ae4g-929917gd934b +
+
+ Date: + April 02, 2026 +
+
+ Assignor: + TrueCOA +
+
+ Assignee: + Chris Smith +
+
+
+
+ + + + +
+ + diff --git a/gauntlet-coa-autopost/test_certificate_marilyn_pdf.html b/gauntlet-coa-autopost/test_certificate_marilyn_pdf.html new file mode 100644 index 0000000..fc83e57 --- /dev/null +++ b/gauntlet-coa-autopost/test_certificate_marilyn_pdf.html @@ -0,0 +1,190 @@ + + + + + Certificate of Authenticity - W2 + + + +
+
+

Certificate of Authenticity

+
+ QR +
#W2
+
+
+
+
+
+
+
Warhol
Marilyn Monroe
(F&S II.30)
+
+
+
+
Details
+
Artist:Warhol
+
Title:Marilyn Monroe (F&S II.30)
+
Medium:Screenprint on paper
+
Dimensions:36" x 36"
+
Edition:250
+
Condition:Excellent
+
+
Digital Authentication
+
Blockchain:https://bit.ly/3MUgdsd
+
Certificate:2bc3a495-bfb2-578c-ae4g-929917gd934b
+
Date:April 02, 2026
+
Assignor:TrueCOA
+
Assignee:Chris Smith
+
+
+
+ +
+ + diff --git a/news_responder_gui.py b/news_responder_gui.py index fc368db..3b40c8b 100644 --- a/news_responder_gui.py +++ b/news_responder_gui.py @@ -651,7 +651,7 @@ def connect_thread(): self.root.after(0, lambda: self.update_tech_status('ai', True)) except Exception as e: - self.root.after(0, lambda: self.update_status(f"Warning: Could not load API keys: {e}", 50)) + self.root.after(0, lambda err=e: self.update_status(f"Warning: Could not load API keys: {err}", 50)) self.root.after(0, lambda: self.update_status("Loading Feedly RSS feed...", 60)) self.refresh_rss_feed() @@ -679,9 +679,9 @@ def connect_thread(): self.root.after(0, lambda: self.connect_btn.configure(text="Reconnect", bg=self.colors['bg_input'])) except Exception as e: - self.root.after(0, lambda: self.update_status(f"Connection failed: {str(e)}", 0)) + self.root.after(0, lambda err=e: self.update_status(f"Connection failed: {str(err)}", 0)) self.root.after(0, lambda: self.update_progress_title("Error")) - self.root.after(0, lambda: messagebox.showerror("Connection Error", str(e))) + self.root.after(0, lambda err=e: messagebox.showerror("Connection Error", str(err))) threading.Thread(target=connect_thread, daemon=True).start() @@ -824,8 +824,8 @@ def process_thread(): self.root.after(0, lambda: self.update_progress_title("Complete")) except Exception as e: - self.root.after(0, lambda: self.update_status(f"Error: {str(e)}", 0)) - self.root.after(0, lambda: messagebox.showerror("Processing Error", str(e))) + self.root.after(0, lambda err=e: self.update_status(f"Error: {str(err)}", 0)) + self.root.after(0, lambda err=e: messagebox.showerror("Processing Error", str(err))) finally: self.is_processing = False