├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.pdf ├── logo512.png ├── images │ ├── 0.png │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── data.csv ├── manifest.json ├── arweave-images.json └── index.html ├── src ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── App.css ├── App.js └── logo.svg ├── .gitignore ├── package.json ├── README.md └── uploader.js /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/logo512.pdf -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/images/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/0.png -------------------------------------------------------------------------------- /public/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/1.png -------------------------------------------------------------------------------- /public/images/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/10.png -------------------------------------------------------------------------------- /public/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/2.png -------------------------------------------------------------------------------- /public/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/3.png -------------------------------------------------------------------------------- /public/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/4.png -------------------------------------------------------------------------------- /public/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/5.png -------------------------------------------------------------------------------- /public/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/6.png -------------------------------------------------------------------------------- /public/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/7.png -------------------------------------------------------------------------------- /public/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/8.png -------------------------------------------------------------------------------- /public/images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuglabs/arweave-image-uploader/HEAD/public/images/9.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /public/data.csv: -------------------------------------------------------------------------------- 1 | Name,Background Color,Head Color,Neck Color 2 | 0000,palegreen,lightblue,lightslategray 3 | 0001,coral,lightgoldenrodyellow,gray 4 | 0002,coral,wheat,darkseagreen 5 | 0003,khaki,silver,lightslategray 6 | 0004,palegreen,silver,darkkhaki 7 | 0005,aquamarine,paleturquoise,cadetblue 8 | 0006,khaki,ivory,gray 9 | 0007,thistle,wheat,slategray 10 | 0008,khaki,lightcyan,lightcyan 11 | 0009,thistle,lightsteelblue,dimgray 12 | 0010,thistle,lightsteelblue,dimgray 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/arweave-images.json: -------------------------------------------------------------------------------- 1 | {"0":{"name":"ART #0000","uri":"https://arweave.net/eR4wgSnWusIG-xF2BZzsiOwVehQsvfCT8VAUC4NHQ5Y"},"1":{"name":"ART #0001","uri":"https://arweave.net/NOvV7akJDBFZogZOKxDMwIhOauiDNhVqnIfUqJmmPR8"},"2":{"name":"ART #0002","uri":"https://arweave.net/q6RS0m0cdoieJbbXI4H1A4yJcDeFi97YF3fHVhn-h9M"},"3":{"name":"ART #0003","uri":"https://arweave.net/NqvUrE0PWdSsDbpP2AVN_MUKzJbIaBqUJfkfF_UV0lU"},"4":{"name":"ART #0004","uri":"https://arweave.net/9GoRp1mfrW7YgXENLPApBZKi4aYRRPEn1tkjRGvdhfk"},"5":{"name":"ART #0005","uri":"https://arweave.net/D3ezjB4KVZWajYmwjcW59cxx3AOqKdbRbqp9IC8pXiM"},"6":{"name":"ART #0006","uri":"https://arweave.net/uY9DkPSmECy98Cj08mOZQ8TpUcjQXW1FWdGxmf_cauo"},"7":{"name":"ART #0007","uri":"https://arweave.net/OrH9NYAPJigomrpkEKyQ2HNoiOVjRWNurngDB9R_6NA"},"8":{"name":"ART #0008","uri":"https://arweave.net/u7c65U5qJe9vovkZZ9GVuJNe80KPtEZrjIAlkRWlcGY"},"9":{"name":"ART #0009","uri":"https://arweave.net/gfb0feghhE0VgyDwFGo6_Vp_LKwbKSGDORHFc9-g314"},"10":{"name":"ART #0010","uri":"https://arweave.net/M5EXxYfp2UJAYnTP-83ZT8ReXS9pRNLeMARuNj0kOF4"}} -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | 6 | @media (prefers-reduced-motion: no-preference) { 7 | .App-logo { 8 | animation: App-logo-spin infinite 20s linear; 9 | } 10 | } 11 | 12 | .App-header { 13 | background-color: #282c34; 14 | min-height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | font-size: calc(10px + 2vmin); 20 | color: white; 21 | } 22 | 23 | .App-link { 24 | color: #61dafb; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | 36 | .container { 37 | display: flex; 38 | flex-wrap: wrap; 39 | } 40 | 41 | .img { 42 | width: 100px; 43 | height: 100px; 44 | margin: 4px 5px; 45 | } 46 | 47 | .img-link { 48 | position: relative; 49 | width: 100px; 50 | height: 100px; 51 | border: 1px solid white; 52 | margin: 4px 5px; 53 | } 54 | 55 | .img-link > span { 56 | position: absolute; 57 | left: 10px; 58 | color: white; 59 | font-weight: bold; 60 | top: 0px; 61 | font-size: 1em; 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arweave-js", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "arweave": "^1.10.16", 11 | "buffer-image": "^1.0.0", 12 | "csv-parser": "^3.0.0", 13 | "fs": "0.0.1-security", 14 | "path": "^0.12.7", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-scripts": "4.0.3", 18 | "web-vitals": "^1.0.1" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "upload": "node ./uploader.js", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import logo from "./logo.svg"; 3 | import "./App.css"; 4 | 5 | function App() { 6 | const [imgJson, setImgJson] = useState({}); 7 | 8 | useEffect(() => { 9 | const fetchJson = async () => { 10 | const result = await fetch("./arweave-images.json"); 11 | const json = await result.json(); 12 | setImgJson(json); 13 | }; 14 | 15 | fetchJson(); 16 | }, []); 17 | 18 | console.log("imgJson", imgJson); 19 | 20 | return ( 21 |
22 |
23 |

Here is list of uploaded images

24 | 25 |
26 | {Object.keys(imgJson).map((key) => { 27 | return ( 28 | 34 | {/* */} 35 | {key} 36 | 37 | ); 38 | })} 39 |
40 | 41 | {/* */} 42 |
43 |
44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arweave NFT Metadata Uploaded 2 | 3 | This package can be used to create metadata files on the Arweave network. It first uploads NFT image and then creates a metadata JSON file for this NFT using this image and default fields from file `uploader.js`. 4 | It is created with an idea to be used with Solana blockchain and uses [Metaplex NFT Standard](https://docs.metaplex.com/nft-standard) but it isn't bound to Solana in any case and can be used with any other blockchain as well. 5 | 6 | ## ⚠️ Important note 7 | 8 | I've disabled line which generates [new AR wallet](https://github.com/thuglabs/arweave-image-uploader/blob/main/uploader.js#L175) for the script use. From my understanding, newly generated wallets have 0 balance and shouldn't be able to approve TX on Arweave. Despite of it the script works well 🤷🏻‍♂️. So, to avoid any possible issues I've disabled it. Consider to use local Arweave wallet instead or this tool instead: [solana-nft-uploader by @moshthepitt](https://github.com/moshthepitt/solana-nft-uploader). 9 | 10 | ## Use 11 | 12 | Prerequisites: 13 | 14 | - all images need to be in PNG format 15 | - all images need to be placed in `public/images/` folder 16 | - CSV data need to be placed in `public/data.csv` 17 | 18 | Then run: 19 | 20 | ``` 21 | node ./uploader.js 22 | 23 | # or 24 | 25 | node run upload 26 | ``` 27 | 28 | The result json file will be saved to `./public/arweave-images.json`. This files consists of arrays of NFT objects with name / uri fields 29 | 30 | ## Result Sample 31 | 32 | ``` 33 | { 34 | "0": { 35 | "name": "ART #0000", 36 | "uri": "https://arweave.net/eR4wgSnWusIG-xF2BZzsiOwVehQsvfCT8VAUC4NHQ5Y" 37 | }, 38 | "1": { 39 | "name": "ART #0001", 40 | "uri": "https://arweave.net/NOvV7akJDBFZogZOKxDMwIhOauiDNhVqnIfUqJmmPR8" 41 | }, 42 | "2": { 43 | "name": "ART #0002", 44 | "uri": "https://arweave.net/q6RS0m0cdoieJbbXI4H1A4yJcDeFi97YF3fHVhn-h9M" 45 | } 46 | } 47 | ``` 48 | 49 | 50 | # TODO: More 51 | 52 | There is React app to manually validate items. But it needs to be updated for better use. 53 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /uploader.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path, { dirname } from "path"; 3 | import { fileURLToPath } from "url"; 4 | import Arweave from "arweave"; 5 | import csv from "csv-parser"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | const results = []; 10 | 11 | const initOptions = { 12 | host: "arweave.net", // Hostname or IP address for a Arweave host 13 | port: 443, // Port 14 | protocol: "https", // Network protocol http or https 15 | timeout: 20000, // Network request timeouts in milliseconds 16 | logging: false, // Enable network request logging 17 | }; 18 | 19 | const getNftName = (name) => `ART #${name}`; 20 | 21 | const getMetadata = (name, imageUrl, attributes) => ({ 22 | name: getNftName(name), 23 | symbol: "", 24 | description: 25 | "You hold in your possession an OG thugbird. It was created with love for the Solana community by 0x_thug", 26 | seller_fee_basis_points: 500, 27 | external_url: "https://www.thugbirdz.com/", 28 | attributes, 29 | collection: { 30 | name: "Test Collection", 31 | family: "thugbirdz", 32 | }, 33 | properties: { 34 | files: [ 35 | { 36 | uri: imageUrl, 37 | type: "image/png", 38 | }, 39 | ], 40 | category: "image", 41 | maxSupply: 0, 42 | creators: [ 43 | { 44 | address: "CBBUMHRmbVUck99mTCip5sHP16kzGj3QTYB8K3XxwmQx", 45 | share: 100, 46 | }, 47 | ], 48 | }, 49 | image: imageUrl, 50 | }); 51 | 52 | // run localy 53 | // npx @textury/arlocal 54 | const initOptionsLocal = { 55 | host: "localhost", // Hostname or IP address for a Arweave host 56 | port: 1984, // Port 57 | protocol: "http", // Network protocol http or https 58 | timeout: 20000, // Network request timeouts in milliseconds 59 | // logging: false, // Enable network request logging 60 | }; 61 | 62 | const arweave = Arweave.init(initOptions); 63 | let key = null; 64 | 65 | const runUpload = async (data, contentType, isUploadByChunk = false) => { 66 | const tx = await arweave.createTransaction({ data: data }, key); 67 | 68 | tx.addTag(...contentType); 69 | 70 | await arweave.transactions.sign(tx, key); 71 | 72 | if (isUploadByChunk) { 73 | const uploader = await arweave.transactions.getUploader(tx); 74 | 75 | while (!uploader.isComplete) { 76 | await uploader.uploadChunk(); 77 | console.log( 78 | `${uploader.pctComplete}% complete, ${uploader.uploadedChunks}/${uploader.totalChunks}` 79 | ); 80 | } 81 | } 82 | 83 | // Do we need to post with uploader? 84 | await arweave.transactions.post(tx); 85 | 86 | // console.log("url", `http://localhost:1984/${tx.id}`); 87 | // console.log("url", `https://arweave.net/${tx.id}`); 88 | return tx; 89 | }; 90 | 91 | const folder = "./public/images/"; 92 | let metadataCollection = {}; 93 | 94 | const getAttributes = (props) => { 95 | // map attributes to the proper key/value objects 96 | const attrs = Object.keys(props).map((key) => { 97 | return { 98 | trait_type: key, 99 | value: props[key], 100 | }; 101 | }); 102 | 103 | return attrs; 104 | }; 105 | 106 | const iterateOverItems = async () => { 107 | try { 108 | for (const row of results) { 109 | // get separately name and props 110 | const { Name: name, ...props } = row; 111 | console.log("name", name); 112 | const nameByNumber = Number.parseInt(name); 113 | 114 | const filePath = folder + nameByNumber + ".png"; 115 | console.log("filePath", filePath); 116 | 117 | let newItem = {}; 118 | 119 | try { 120 | const data = fs.readFileSync(filePath); 121 | // if (!data) console.warn(`Can't find file: ${filePath}`); 122 | 123 | const contentType = ["Content-Type", "image/png"]; 124 | const { id } = await runUpload(data, contentType, true); 125 | const imageUrl = id ? `https://arweave.net/${id}` : undefined; 126 | console.log("imageUrl", imageUrl); 127 | 128 | const attributes = getAttributes(props); 129 | 130 | const metadata = getMetadata(name, imageUrl, attributes); 131 | // console.log(metadata); 132 | const metaContentType = ["Content-Type", "application/json"]; 133 | const metadataString = JSON.stringify(metadata); 134 | const { id: metadataId } = await runUpload( 135 | metadataString, 136 | metaContentType 137 | ); 138 | const metadataUrl = id 139 | ? `https://arweave.net/${metadataId}` 140 | : undefined; 141 | 142 | console.log("metadataUrl", metadataUrl); 143 | newItem = { 144 | [nameByNumber]: { 145 | name: getNftName(name), 146 | uri: metadataUrl, 147 | }, 148 | }; 149 | } catch (error) { 150 | newItem = { 151 | [nameByNumber]: undefined, 152 | }; 153 | } 154 | 155 | // update collection with new item 156 | metadataCollection = { ...metadataCollection, ...newItem }; 157 | } 158 | 159 | // All images iterated 160 | console.log(metadataCollection); 161 | 162 | // Save data to json in /public/ 163 | const data = JSON.stringify(metadataCollection); 164 | fs.writeFileSync("./public/arweave-images.json", data); 165 | } catch (e) { 166 | // Catch anything bad that happens 167 | console.error("We've thrown! Whoops!", e); 168 | } 169 | }; 170 | 171 | const readCsv = async () => { 172 | // Consider to use local wallet instead of generated one. 173 | //. I'm not sure how this works, since newly generated wallet have 0 balance. 🤷🏻‍♂️ 174 | //. So, I comment out this line for now. 175 | // key = await arweave.wallets.generate(); 176 | 177 | fs.createReadStream(path.resolve(__dirname, "public", "data.csv")) 178 | .pipe(csv()) 179 | .on("data", (data) => results.push(data)) 180 | .on("end", () => { 181 | // console.log(results); 182 | // { 183 | // Name: '0000', 184 | // 'Background Color': 'palegreen', 185 | // 'Head Color': 'lightblue', 186 | // 'Neck Color': 'lightslategray', 187 | // ... 188 | // }, 189 | 190 | iterateOverItems(); 191 | }); 192 | }; 193 | 194 | readCsv(); 195 | --------------------------------------------------------------------------------