|
| 1 | +/** |
| 2 | + * upload-arweave.ts |
| 3 | + * |
| 4 | + * Uploads the MACH token image and metadata JSON to Arweave via Irys. |
| 5 | + * Prints the metadata URI to paste into setup-mach-token.ts. |
| 6 | + * |
| 7 | + * Run: |
| 8 | + * SOLANA_CLUSTER=devnet npx tsx scripts/upload-arweave.ts |
| 9 | + * |
| 10 | + * Prerequisites: |
| 11 | + * - pnpm add -D @irys/sdk |
| 12 | + * - Keypair at ~/.config/solana/id.json (or ANCHOR_WALLET) |
| 13 | + * - Devnet: funded automatically via airdrop |
| 14 | + * - Mainnet: wallet needs real SOL (upload costs ~$0.01 total) |
| 15 | + * |
| 16 | + * Output: |
| 17 | + * Image URI — paste into assets/mach-token-metadata.json |
| 18 | + * Metadata URI — paste into scripts/setup-mach-token.ts uri field |
| 19 | + */ |
| 20 | + |
| 21 | +import fs from "node:fs"; |
| 22 | +import os from "node:os"; |
| 23 | +import path from "node:path"; |
| 24 | +import Irys from "@irys/sdk"; |
| 25 | + |
| 26 | +const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet"; |
| 27 | + |
| 28 | +const ASSETS_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "assets"); |
| 29 | +const IMAGE_PATH = path.join(ASSETS_DIR, "mach-token.png"); |
| 30 | +const METADATA_PATH = path.join(ASSETS_DIR, "mach-token-metadata.json"); |
| 31 | + |
| 32 | +function getIrysUrl(): string { |
| 33 | + return CLUSTER === "mainnet-beta" ? "https://node1.irys.xyz" : "https://devnet.irys.xyz"; |
| 34 | +} |
| 35 | + |
| 36 | +function getRpcUrl(): string { |
| 37 | + if (CLUSTER === "localnet") { |
| 38 | + console.error("Arweave uploads are not supported on localnet. Use devnet or mainnet-beta."); |
| 39 | + process.exit(1); |
| 40 | + } |
| 41 | + const apiKey = process.env.HELIUS_API_KEY; |
| 42 | + if (apiKey) { |
| 43 | + return CLUSTER === "mainnet-beta" |
| 44 | + ? `https://mainnet.helius-rpc.com/?api-key=${apiKey}` |
| 45 | + : `https://devnet.helius-rpc.com/?api-key=${apiKey}`; |
| 46 | + } |
| 47 | + return CLUSTER === "mainnet-beta" |
| 48 | + ? "https://api.mainnet-beta.solana.com" |
| 49 | + : "https://api.devnet.solana.com"; |
| 50 | +} |
| 51 | + |
| 52 | +function loadKeypairPath(): string { |
| 53 | + return process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json"); |
| 54 | +} |
| 55 | + |
| 56 | +async function main() { |
| 57 | + console.log(`Cluster: ${CLUSTER}`); |
| 58 | + console.log(`Irys node: ${getIrysUrl()}\n`); |
| 59 | + |
| 60 | + if (!fs.existsSync(IMAGE_PATH)) { |
| 61 | + console.error(`Image not found: ${IMAGE_PATH}`); |
| 62 | + process.exit(1); |
| 63 | + } |
| 64 | + if (!fs.existsSync(METADATA_PATH)) { |
| 65 | + console.error(`Metadata not found: ${METADATA_PATH}`); |
| 66 | + process.exit(1); |
| 67 | + } |
| 68 | + |
| 69 | + const keypairPath = loadKeypairPath(); |
| 70 | + const keypairBytes = JSON.parse(fs.readFileSync(keypairPath, "utf-8")) as number[]; |
| 71 | + const secretKey = Uint8Array.from(keypairBytes); |
| 72 | + |
| 73 | + const irys = new Irys({ |
| 74 | + url: getIrysUrl(), |
| 75 | + token: "solana", |
| 76 | + key: secretKey, |
| 77 | + config: { providerUrl: getRpcUrl() }, |
| 78 | + }); |
| 79 | + |
| 80 | + // Fund if on devnet (devnet Irys will airdrop automatically) |
| 81 | + if (CLUSTER !== "mainnet-beta") { |
| 82 | + console.log("Funding Irys node (devnet)..."); |
| 83 | + try { |
| 84 | + await irys.fund(irys.utils.toAtomic(0.05)); |
| 85 | + console.log(" Funded 0.05 SOL\n"); |
| 86 | + } catch { |
| 87 | + // May already be funded — continue |
| 88 | + console.log(" Already funded or airdrop pending — continuing\n"); |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + // ── Upload image ───────────────────────────────────────────────────────────── |
| 93 | + console.log("Uploading mach-token.png..."); |
| 94 | + const imageData = fs.readFileSync(IMAGE_PATH); |
| 95 | + const imageReceipt = await irys.upload(imageData, { |
| 96 | + tags: [ |
| 97 | + { name: "Content-Type", value: "image/png" }, |
| 98 | + { name: "App-Name", value: "Maschina" }, |
| 99 | + ], |
| 100 | + }); |
| 101 | + const imageUri = `https://arweave.net/${imageReceipt.id}`; |
| 102 | + console.log(` Image URI: ${imageUri}\n`); |
| 103 | + |
| 104 | + // ── Patch metadata with real image URI ─────────────────────────────────────── |
| 105 | + const metadataRaw = fs.readFileSync(METADATA_PATH, "utf-8"); |
| 106 | + const metadata = JSON.parse(metadataRaw); |
| 107 | + metadata.image = imageUri; |
| 108 | + metadata.properties.files[0].uri = imageUri; |
| 109 | + const metadataPatched = JSON.stringify(metadata, null, 2); |
| 110 | + |
| 111 | + // ── Upload metadata ────────────────────────────────────────────────────────── |
| 112 | + console.log("Uploading mach-token-metadata.json..."); |
| 113 | + const metaReceipt = await irys.upload(Buffer.from(metadataPatched, "utf-8"), { |
| 114 | + tags: [ |
| 115 | + { name: "Content-Type", value: "application/json" }, |
| 116 | + { name: "App-Name", value: "Maschina" }, |
| 117 | + ], |
| 118 | + }); |
| 119 | + const metadataUri = `https://arweave.net/${metaReceipt.id}`; |
| 120 | + console.log(` Metadata URI: ${metadataUri}\n`); |
| 121 | + |
| 122 | + // ── Write patched metadata back to disk ────────────────────────────────────── |
| 123 | + fs.writeFileSync(METADATA_PATH, metadataPatched); |
| 124 | + console.log(" Updated assets/mach-token-metadata.json with real image URI\n"); |
| 125 | + |
| 126 | + // ── Summary ────────────────────────────────────────────────────────────────── |
| 127 | + console.log("─────────────────────────────────────────────"); |
| 128 | + console.log("Next steps:"); |
| 129 | + console.log(" 1. In scripts/setup-mach-token.ts, set:"); |
| 130 | + console.log(` uri: "${metadataUri}",`); |
| 131 | + console.log(" 2. Run: SOLANA_CLUSTER=devnet npx tsx scripts/setup-mach-token.ts"); |
| 132 | + console.log("─────────────────────────────────────────────"); |
| 133 | +} |
| 134 | + |
| 135 | +main().catch((err) => { |
| 136 | + console.error(err); |
| 137 | + process.exit(1); |
| 138 | +}); |
0 commit comments