add NFT minting (frontend + Pinata/IPFS backend)
This commit is contained in:
@ -0,0 +1,23 @@
|
||||
# =========================
|
||||
# Environment Setup
|
||||
# =========================
|
||||
# 1. In your project, go to the `nft_mint_server` folder.
|
||||
# 2. Create a new file in this folder and name it exactly: `.env`
|
||||
# (right-click → New File → type `.env`).
|
||||
# 3. Open `.env.template` (this file) and copy all its contents.
|
||||
# 4. Paste the contents into your new `.env` file.
|
||||
|
||||
# =========================
|
||||
# Pinata Credentials
|
||||
# =========================
|
||||
# 5. Go to https://app.pinata.cloud/developers/api-keys
|
||||
# 6. Click "New Key", toggle to **Admin**, and click "Create".
|
||||
# 7. Copy the JWT Token and paste them into your `.env` below.
|
||||
|
||||
PINATA_API_KEY=YOUR_PINATA_API_KEY_GOES_HERE
|
||||
PINATA_API_SECRET=YOUR_PINATA_API_SECRET_GOES_HERE
|
||||
|
||||
# =========================
|
||||
# Server Config
|
||||
# =========================
|
||||
PORT=3001
|
||||
134
projects/TokenizeRWATemplate-contracts/NFT_mint_server/app.js
Normal file
134
projects/TokenizeRWATemplate-contracts/NFT_mint_server/app.js
Normal file
@ -0,0 +1,134 @@
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import multer from 'multer'
|
||||
import pinataSDK from '@pinata/sdk'
|
||||
import dotenv from 'dotenv'
|
||||
import { Readable } from 'stream'
|
||||
import path from 'path'
|
||||
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.
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env') })
|
||||
|
||||
const app = express()
|
||||
|
||||
// Allow local + prod (comma-separated in env), or * by default for dev
|
||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '*')
|
||||
.split(',')
|
||||
.map((o) => o.trim())
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: (origin, cb) => {
|
||||
if (!origin) return cb(null, true) // same-origin, curl, Postman
|
||||
if (allowedOrigins.includes('*')) return cb(null, true)
|
||||
if (allowedOrigins.includes(origin)) return cb(null, true)
|
||||
|
||||
// Allow any frontend on vercel.app (great for forks)
|
||||
try {
|
||||
const host = new URL(origin).hostname
|
||||
if (host.endsWith('.vercel.app')) return cb(null, true)
|
||||
} catch (_) {}
|
||||
|
||||
return cb(null, false)
|
||||
},
|
||||
credentials: false,
|
||||
})
|
||||
)
|
||||
|
||||
// IMPORTANT: for multipart/form-data, multer populates req.body
|
||||
// but JSON parsing is still useful for other endpoints
|
||||
app.use(express.json())
|
||||
|
||||
// Pinata client
|
||||
const pinata = process.env.PINATA_JWT
|
||||
? new pinataSDK({ pinataJWTKey: process.env.PINATA_JWT })
|
||||
: new pinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_API_SECRET)
|
||||
|
||||
// Optional: test credentials at cold start
|
||||
;(async () => {
|
||||
try {
|
||||
const auth = await pinata.testAuthentication?.()
|
||||
console.log('Pinata auth OK:', auth || 'ok')
|
||||
} 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.json({ ok: true, ts: Date.now() })
|
||||
})
|
||||
|
||||
// uploads
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB safety limit (matches frontend hint)
|
||||
},
|
||||
})
|
||||
|
||||
// Small helpers
|
||||
function safeTrim(v) {
|
||||
return typeof v === 'string' ? v.trim() : ''
|
||||
}
|
||||
|
||||
function safeJsonParse(v, fallback) {
|
||||
try {
|
||||
if (typeof v !== 'string' || !v.trim()) return fallback
|
||||
return JSON.parse(v)
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/api/pin-image', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
const file = req.file
|
||||
if (!file) return res.status(400).json({ error: 'No file uploaded' })
|
||||
|
||||
// NEW: read optional fields from multipart form-data (sent by frontend)
|
||||
const metaName = safeTrim(req.body?.metaName) || 'NFT Example'
|
||||
const metaDescription = safeTrim(req.body?.metaDescription) || 'This is an unchangeable NFT'
|
||||
const properties = safeJsonParse(req.body?.properties, {})
|
||||
|
||||
// Pin image
|
||||
const stream = Readable.from(file.buffer)
|
||||
// Pinata SDK expects a path/filename sometimes
|
||||
// @ts-ignore
|
||||
stream.path = file.originalname || 'upload'
|
||||
|
||||
const imageOptions = {
|
||||
pinataMetadata: { name: file.originalname || `${metaName} Image` },
|
||||
}
|
||||
const imageResult = await pinata.pinFileToIPFS(stream, imageOptions)
|
||||
const imageUrl = `ipfs://${imageResult.IpfsHash}`
|
||||
|
||||
// Pin metadata JSON
|
||||
const metadata = {
|
||||
name: metaName,
|
||||
description: metaDescription,
|
||||
image: imageUrl,
|
||||
properties,
|
||||
}
|
||||
|
||||
const jsonOptions = { pinataMetadata: { name: `${metaName} Metadata` } }
|
||||
const jsonResult = await pinata.pinJSONToIPFS(metadata, jsonOptions)
|
||||
const metadataUrl = `ipfs://${jsonResult.IpfsHash}`
|
||||
|
||||
res.status(200).json({ metadataUrl })
|
||||
} catch (error) {
|
||||
const msg =
|
||||
error?.response?.data?.error ||
|
||||
error?.response?.data ||
|
||||
error?.message ||
|
||||
'Failed to pin to IPFS.'
|
||||
res.status(500).json({ error: msg })
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
@ -0,0 +1,2 @@
|
||||
import app from './app.js'
|
||||
export default app
|
||||
1434
projects/TokenizeRWATemplate-contracts/NFT_mint_server/package-lock.json
generated
Normal file
1434
projects/TokenizeRWATemplate-contracts/NFT_mint_server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "nft_mint_server",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": { "node": ">=18 <21" },
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinata/sdk": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"multer": "^2.0.2"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import app from './app.js'
|
||||
|
||||
const port = process.env.PORT || 3001
|
||||
|
||||
app.listen(port, '0.0.0.0', () => {
|
||||
console.log(`✅ Backend listening at http://localhost:${port}`)
|
||||
})
|
||||
@ -65,6 +65,7 @@
|
||||
"integrity": "sha512-jILpK3PlSJ55crS8+Dl7iIiADEj/VowokTGSVEEneDG41XM3jMmogGVpCNipBil/YAGC2eh2yc4l9iHnfQdT/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3"
|
||||
},
|
||||
@ -1489,6 +1490,7 @@
|
||||
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
@ -1876,6 +1878,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -1939,6 +1942,7 @@
|
||||
"integrity": "sha512-frhGtZl1JvfrLRKmMvUm880wj4OiWsWo2FhbreNWh7pdFsKuWPj60fV682wt/CYefLI70iwHavPOwGBkTVt0VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"algorand-msgpack": "^1.1.0",
|
||||
"hi-base32": "^0.5.1",
|
||||
@ -2591,6 +2595,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -3980,6 +3985,7 @@
|
||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -4696,7 +4702,8 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
@ -4724,6 +4731,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -4761,8 +4769,7 @@
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/upath": {
|
||||
"version": "2.0.1",
|
||||
@ -4798,6 +4805,7 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
Reference in New Issue
Block a user