Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2
3/**
4 * Rebake $air token metadata on v4 staging contract
5 * This will regenerate the bundle and update on-chain metadata
6 */
7
8import { TezosToolkit } from '@taquito/taquito';
9import { InMemorySigner } from '@taquito/signer';
10import fs from 'fs';
11import path from 'path';
12import { fileURLToPath } from 'url';
13import https from 'https';
14
15const __filename = fileURLToPath(import.meta.url);
16const __dirname = path.dirname(__filename);
17
18// V4 staging contract
19const V4_CONTRACT = 'KT1ER1GyoeRNhkv6E57yKbBbEKi5ynKbaH3W';
20const TOKEN_ID = 3; // $air
21const PIECE_CODE = 'air';
22
23// Load staging wallet credentials
24const stagingEnvPath = path.join(__dirname, 'staging/.env');
25const envContent = fs.readFileSync(stagingEnvPath, 'utf8');
26let stagingAddress, stagingKey;
27for (const line of envContent.split('\n')) {
28 if (line.startsWith('STAGING_ADDRESS=') || line.startsWith('ADDRESS=')) {
29 stagingAddress = line.split('=')[1].trim().replace(/"/g, '');
30 } else if (line.startsWith('STAGING_KEY=') || line.startsWith('KEY=') || line.startsWith('SECRET_KEY=')) {
31 stagingKey = line.split('=')[1].trim().replace(/"/g, '');
32 }
33}
34
35if (!stagingAddress || !stagingKey) {
36 console.error('❌ Could not load staging wallet credentials');
37 process.exit(1);
38}
39
40const tezos = new TezosToolkit('https://mainnet.api.tez.ie');
41tezos.setProvider({ signer: new InMemorySigner(stagingKey) });
42
43console.log('\n╔══════════════════════════════════════════════════════════════╗');
44console.log('║ 🔄 Rebake $air Metadata ║');
45console.log('╚══════════════════════════════════════════════════════════════╝\n');
46
47console.log(`📍 Contract: ${V4_CONTRACT}`);
48console.log(`🎨 Token: #${TOKEN_ID} ($${PIECE_CODE})`);
49console.log(`👤 Admin: ${stagingAddress}\n`);
50
51// Fetch new bundle from bundle-html API
52console.log('📦 Generating fresh bundle...');
53const bundleUrl = `https://aesthetic.computer/api/bundle-html?code=${PIECE_CODE}&format=json`;
54
55function fetchUrl(url) {
56 return new Promise((resolve, reject) => {
57 https.get(url, (res) => {
58 let data = '';
59 res.on('data', chunk => data += chunk);
60 res.on('end', () => {
61 if (res.statusCode === 200) {
62 resolve(JSON.parse(data));
63 } else {
64 reject(new Error(`HTTP ${res.statusCode}: ${data}`));
65 }
66 });
67 }).on('error', reject);
68 });
69}
70
71const bundleData = await fetchUrl(bundleUrl);
72console.log(` ✓ Bundle generated: ${bundleData.sizeKB} KB`);
73console.log(` ✓ Filename: ${bundleData.filename}\n`);
74
75// Upload to IPFS via Pinata
76console.log('📤 Uploading to IPFS...');
77
78// Load Pinata credentials
79const pinataEnvPath = path.join(__dirname, '..', 'aesthetic-computer-vault', '.env.pinata');
80const pinataContent = fs.readFileSync(pinataEnvPath, 'utf8');
81let pinataKey, pinataSecret;
82for (const line of pinataContent.split('\n')) {
83 if (line.startsWith('PINATA_API_KEY=')) {
84 pinataKey = line.split('=')[1].trim().replace(/"/g, '');
85 } else if (line.startsWith('PINATA_API_SECRET=')) {
86 pinataSecret = line.split('=')[1].trim().replace(/"/g, '');
87 }
88}
89
90function uploadToPinata(content, filename, pinataApiKey, pinataApiSecret) {
91 return new Promise((resolve, reject) => {
92 const boundary = '----WebKitFormBoundary' + Math.random().toString(36);
93 const bodyParts = [
94 `--${boundary}\r\n`,
95 `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n`,
96 'Content-Type: text/html\r\n\r\n',
97 Buffer.from(content, 'base64').toString('utf8'),
98 `\r\n--${boundary}--\r\n`
99 ];
100 const body = bodyParts.join('');
101
102 const options = {
103 hostname: 'api.pinata.cloud',
104 path: '/pinning/pinFileToIPFS',
105 method: 'POST',
106 headers: {
107 'Content-Type': `multipart/form-data; boundary=${boundary}`,
108 'Content-Length': Buffer.byteLength(body),
109 'pinata_api_key': pinataApiKey,
110 'pinata_secret_api_key': pinataApiSecret
111 }
112 };
113
114 const req = https.request(options, (res) => {
115 let data = '';
116 res.on('data', chunk => data += chunk);
117 res.on('end', () => {
118 if (res.statusCode === 200) {
119 resolve(JSON.parse(data));
120 } else {
121 reject(new Error(`Pinata upload failed: ${res.statusCode} ${data}`));
122 }
123 });
124 });
125
126 req.on('error', reject);
127 req.write(body);
128 req.end();
129 });
130}
131
132const pinataResult = await uploadToPinata(bundleData.content, bundleData.filename, pinataKey, pinataSecret);
133const ipfsHash = pinataResult.IpfsHash;
134const ipfsUri = `ipfs://${ipfsHash}`;
135
136console.log(` ✓ Uploaded: ${ipfsUri}\n`);
137
138// Build updated token_info with new artifact URI
139console.log('📝 Building updated metadata...');
140
141function stringToBytes(str) {
142 return Buffer.from(str, 'utf8').toString('hex');
143}
144
145const tokenInfo = {
146 'name': stringToBytes(`$${PIECE_CODE}`),
147 'description': stringToBytes(bundleData.sourceCode || ''),
148 'artifactUri': stringToBytes(ipfsUri),
149 'displayUri': stringToBytes(ipfsUri),
150 'thumbnailUri': stringToBytes(ipfsUri), // Will use existing thumbnail
151 'decimals': stringToBytes('0'),
152 'symbol': stringToBytes('KEEP'),
153 'isBooleanAmount': stringToBytes('true'),
154 'shouldPreferSymbol': stringToBytes('false'),
155 '': stringToBytes(ipfsUri) // metadata URI
156};
157
158console.log('📤 Calling edit_metadata...');
159
160try {
161 const contract = await tezos.contract.at(V4_CONTRACT);
162
163 const op = await contract.methods.edit_metadata(TOKEN_ID, tokenInfo).send();
164
165 console.log(` ⏳ Operation hash: ${op.hash}`);
166 console.log(' ⏳ Waiting for confirmation...');
167
168 await op.confirmation(1);
169
170 console.log('\n✅ Metadata updated!');
171 console.log(` 🔗 Explorer: https://tzkt.io/${op.hash}`);
172 console.log(` 🎨 View on objkt: https://objkt.com/tokens/${V4_CONTRACT}/${TOKEN_ID}\n`);
173
174} catch (error) {
175 console.error('\n❌ Update failed!');
176 console.error(` Error: ${error.message}\n`);
177 process.exit(1);
178}