This commit is contained in:
SaraJane
2026-01-20 02:17:10 +00:00
parent f6a6cbc810
commit e1ac2c0e04
2 changed files with 57 additions and 65 deletions

View File

@ -1,99 +1,88 @@
// server.js (ESM) — deployable on Vercel
import pinataSDK from '@pinata/sdk' import pinataSDK from '@pinata/sdk'
import cors from 'cors' import cors from 'cors'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import express from 'express' import express from 'express'
import multer from 'multer' import multer from 'multer'
import path from 'path'
import { Readable } from 'stream' import { Readable } from 'stream'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Load local .env for dev. In Vercel, env vars come from platform. // Load local .env for dev. In Vercel, env vars come from platform.
dotenv.config({ path: path.resolve(__dirname, '.env') }) dotenv.config()
const app = express() const app = express()
/** /**
* CORS * CORS
* - Allows localhost for local dev * - Allows localhost dev
* - Allows your main frontend via FRONTEND_ORIGIN * - Allows your main frontend
* - Allows ANY *.vercel.app (so forks work for non-technical founders)
*
* Optional: set ALLOWED_ORIGINS in Vercel env as comma-separated list
* Example:
* ALLOWED_ORIGINS=https://tokenize-rwa-template.vercel.app,http://localhost:5173
*/ */
const allowedOrigins = [ const explicitAllowed = (process.env.ALLOWED_ORIGINS || '')
'http://localhost:5173', .split(',')
process.env.FRONTEND_ORIGIN, // e.g. https://tokenize-rwa-template.vercel.app .map((s) => s.trim())
]
.filter(Boolean) .filter(Boolean)
.map((o) => o.trim())
const isAllowedOrigin = (origin: string) => {
// explicitly allowed
if (explicitAllowed.includes(origin)) return true
// local dev
if (origin === 'http://localhost:5173') return true
// allow any Vercel preview/prod frontend (great for forks)
try {
const host = new URL(origin).hostname
if (host.endsWith('.vercel.app')) return true
} catch {
// ignore
}
return false
}
app.use( app.use(
cors({ cors({
origin: (origin, cb) => { origin: (origin, cb) => {
// allow server-to-server / curl (no origin) // allow server-to-server / curl / same-origin (no Origin header)
if (!origin) return cb(null, true) if (!origin) return cb(null, true)
if (isAllowedOrigin(origin)) return cb(null, true)
if (allowedOrigins.includes(origin)) return cb(null, true)
// Allow any frontend on vercel.app (great for forks + previews)
try {
const host = new URL(origin).hostname
if (host.endsWith('.vercel.app')) return cb(null, true)
} catch {
// ignore URL parse errors
}
return cb(null, false) return cb(null, false)
}, },
methods: ['GET', 'POST', 'OPTIONS'], methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'], allowedHeaders: ['Content-Type', 'Authorization'],
credentials: false,
}), }),
) )
// Preflight for all routes // Handle preflight requests for ALL routes
app.options('*', cors()) app.options('*', cors())
// For non-multipart endpoints (multer handles multipart/form-data) app.use(express.json())
app.use(express.json({ limit: '15mb' }))
// Pinata client // Pinata client
const pinata = process.env.PINATA_JWT const pinata =
process.env.PINATA_JWT && process.env.PINATA_JWT.trim().length > 0
? new pinataSDK({ pinataJWTKey: process.env.PINATA_JWT }) ? new pinataSDK({ pinataJWTKey: process.env.PINATA_JWT })
: new pinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_API_SECRET) : new pinataSDK(process.env.PINATA_API_KEY || '', process.env.PINATA_API_SECRET || '')
// Optional: test credentials at cold start // Uploads (multipart/form-data)
;(async () => {
try {
if (typeof pinata.testAuthentication === 'function') {
await pinata.testAuthentication()
console.log('Pinata auth OK')
} else {
console.log('Pinata SDK loaded (no testAuthentication method)')
}
} catch (e) {
console.error('Pinata authentication FAILED. Check env vars.', e?.message || e)
}
})()
// health
app.get('/health', (_req, res) => {
res.set('Cache-Control', 'no-store')
res.status(200).json({ ok: true, ts: Date.now() })
})
// uploads
const upload = multer({ const upload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
}) })
function safeTrim(v) { app.get('/health', (_req, res) => {
res.set('Cache-Control', 'no-store')
res.status(200).json({ ok: true, ts: Date.now() })
})
function safeTrim(v: unknown) {
return typeof v === 'string' ? v.trim() : '' return typeof v === 'string' ? v.trim() : ''
} }
function safeJsonParse(v, fallback) { function safeJsonParse(v: unknown, fallback: any) {
try { try {
if (typeof v !== 'string' || !v.trim()) return fallback if (typeof v !== 'string' || !v.trim()) return fallback
return JSON.parse(v) return JSON.parse(v)
@ -107,14 +96,13 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
const file = req.file const file = req.file
if (!file) return res.status(400).json({ error: 'No file uploaded' }) if (!file) return res.status(400).json({ error: 'No file uploaded' })
// Optional multipart fields from frontend // Optional form fields (friendly for vibe-coders)
const metaName = safeTrim(req.body?.metaName) || 'NFT Example' const metaName = safeTrim(req.body?.metaName) || 'NFT Example'
const metaDescription = safeTrim(req.body?.metaDescription) || 'This is an unchangeable NFT' const metaDescription = safeTrim(req.body?.metaDescription) || 'Pinned via TokenizeRWA template'
const properties = safeJsonParse(req.body?.properties, {}) const properties = safeJsonParse(req.body?.properties, {})
// Pin image // Pin image
const stream = Readable.from(file.buffer) const stream = Readable.from(file.buffer) as any
// Pinata SDK sometimes expects a .path/filename
stream.path = file.originalname || 'upload' stream.path = file.originalname || 'upload'
const imageResult = await pinata.pinFileToIPFS(stream, { const imageResult = await pinata.pinFileToIPFS(stream, {
@ -138,8 +126,12 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
const metadataUrl = `ipfs://${jsonResult.IpfsHash}` const metadataUrl = `ipfs://${jsonResult.IpfsHash}`
return res.status(200).json({ metadataUrl }) return res.status(200).json({ metadataUrl })
} catch (error) { } catch (error: any) {
const msg = error?.response?.data?.error || error?.response?.data || error?.message || 'Failed to pin to IPFS.' const msg =
error?.response?.data?.error ||
error?.response?.data ||
error?.message ||
'Failed to pin to IPFS.'
return res.status(500).json({ error: msg }) return res.status(500).json({ error: msg })
} }
}) })

View File

@ -12,7 +12,7 @@ import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClien
* Captures ASA configuration including compliance fields * Captures ASA configuration including compliance fields
*/ */
type CreatedAsset = { type CreatedAsset = {
assetId: bigint assetId: string
assetName: string assetName: string
unitName: string unitName: string
total: string total: string
@ -575,7 +575,7 @@ export default function TokenizeAsset() {
const assetId = createResult.assetId const assetId = createResult.assetId
const newEntry: CreatedAsset = { const newEntry: CreatedAsset = {
assetId: BigInt(assetId), assetId: String(assetId),
assetName: String(assetName), assetName: String(assetName),
unitName: String(unitName), unitName: String(unitName),
total: String(total), total: String(total),
@ -900,7 +900,7 @@ export default function TokenizeAsset() {
// ✅ Persist minted NFT into SAME history list (NFTs are ASAs) // ✅ Persist minted NFT into SAME history list (NFTs are ASAs)
const nftEntry: CreatedAsset = { const nftEntry: CreatedAsset = {
assetId: BigInt(assetId), assetId: String(assetId),
assetName: String(nftName), assetName: String(nftName),
unitName: String(nftUnit), unitName: String(nftUnit),
total: String(nftSupply), total: String(nftSupply),