├── .gitignore ├── hardhat.config.js ├── contracts ├── SVG721.sol └── Base64.sol ├── package.json ├── README.md ├── test └── MyTokenTest.js └── example └── MyToken.sol /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # debug 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | artifacts/ 12 | cache/ 13 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type import('hardhat/config').HardhatUserConfig 3 | */ 4 | 5 | require("@nomiclabs/hardhat-ethers"); 6 | require("@nomiclabs/hardhat-waffle"); 7 | 8 | module.exports = { 9 | solidity: "0.8.3", 10 | paths: { 11 | sources: "./{contracts,example}" 12 | } 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /contracts/SVG721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./Base64.sol"; 6 | 7 | library SVG721 { 8 | function metadata( 9 | string memory tokenName, 10 | string memory tokenDescription, 11 | string memory svgString 12 | ) internal pure returns (string memory) { 13 | string memory json = string(abi.encodePacked('{"name":"', tokenName, '","description":"', tokenDescription, '","image": "data:image/svg+xml;base64,', Base64.encode(bytes(svgString)), '"}')); 14 | return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json)))); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svgnft", 3 | "version": "0.1.1", 4 | "author": "Mikkel Malmberg ", 5 | "files": [ 6 | "contracts/*.sol" 7 | ], 8 | "scripts": { 9 | "test": "yarn hardhat test", 10 | "release": "np" 11 | }, 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@nomiclabs/hardhat-ethers": "^2.0.2", 15 | "@nomiclabs/hardhat-waffle": "^2.0.1", 16 | "@openzeppelin/contracts": "^4.3.2", 17 | "chai": "^4.3.4", 18 | "ethers": "^5.4.7", 19 | "hardhat": "^2.6.4", 20 | "mocha": "^9.1.2", 21 | "np": "^7.5.0", 22 | "prettier": "^2.4.1", 23 | "prettier-plugin-solidity": "^1.0.0-beta.18" 24 | }, 25 | "prettier": { 26 | "overrides": [ 27 | { 28 | "files": "*.sol", 29 | "options": { 30 | "printWidth": 1000 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svgnft 2 | 3 | A helper library for generating fully on-chain NFTs (ERC721) on Ethereum. 4 | 5 | Being on-chain is fun! 6 | 7 | ## Example 8 | 9 | See `example/`. Override `tokenUri` with something like: 10 | 11 | ```solidity 12 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { 13 | string memory name = string(abi.encodePacked("MyToken #", tokenId.toString())); 14 | string memory description = "An example SVG-based, fully on-chain NFT"; 15 | string memory svg = ''; 16 | 17 | return SVG721.metadata(name, description, svg); 18 | } 19 | ``` 20 | 21 | That's it! 22 | 23 | ## Install 24 | 25 | Using [hardhat](https://hardhat.org). 26 | 27 | ```sh 28 | npm install --save-dev svgnft 29 | # or 30 | yarn add --dev svgnft 31 | ``` 32 | 33 | In your contract 34 | 35 | ```solidity 36 | import "svgnft/contracts/SVG721.sol"; 37 | ``` 38 | 39 | ## License 40 | 41 | MIT 42 | 43 | `Base64.sol` by Brecht Devos. 44 | -------------------------------------------------------------------------------- /test/MyTokenTest.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | 4 | describe("MyToken", function () { 5 | let MyToken; 6 | let myToken; 7 | 8 | before(async function () { 9 | MyToken = await ethers.getContractFactory("MyToken"); 10 | myToken = await MyToken.deploy(); 11 | await myToken.deployed(); 12 | }); 13 | 14 | it("has a tokenUri that returns an inline SVG", async function () { 15 | const uri = await myToken.tokenURI(0); 16 | expect(uri).to.match(/^data:application\/json;base64,.+/); 17 | 18 | let base64; 19 | [_, base64] = uri.split(","); 20 | const json = JSON.parse(Buffer.from(base64, "base64").toString("utf-8")); 21 | 22 | expect(json["name"]).to.eq("MyToken #0"); 23 | expect(json["description"]).to.eq( 24 | "An example SVG-based, fully on-chain NFT" 25 | ); 26 | 27 | const image = json["image"]; 28 | expect(image).to.match(/^data:image\/svg/); 29 | 30 | [_, base64] = image.split(","); 31 | const svg = Buffer.from(base64, "base64").toString("utf-8"); 32 | 33 | expect(svg).to.eq( 34 | '' 35 | ); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /example/MyToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.2; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | import "@openzeppelin/contracts/utils/Strings.sol"; 7 | import "../contracts/SVG721.sol"; 8 | 9 | contract MyToken is ERC721 { 10 | using Strings for uint256; 11 | 12 | constructor() ERC721("MyToken", "MTKN") { 13 | _safeMint(msg.sender, 0); 14 | } 15 | 16 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { 17 | string memory name = string(abi.encodePacked("MyToken #", tokenId.toString())); 18 | string memory description = "An example SVG-based, fully on-chain NFT"; 19 | string memory svg = ''; 20 | 21 | return SVG721.metadata(name, description, svg); 22 | } 23 | 24 | // required overrides 25 | 26 | function _beforeTokenTransfer( 27 | address from, 28 | address to, 29 | uint256 tokenId 30 | ) internal override(ERC721) { 31 | super._beforeTokenTransfer(from, to, tokenId); 32 | } 33 | 34 | function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool) { 35 | return super.supportsInterface(interfaceId); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/Base64.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | /// [MIT License] 5 | /// @title Base64 6 | /// @notice Provides a function for encoding some bytes in base64 7 | /// @author Brecht Devos 8 | library Base64 { 9 | bytes internal constant TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 10 | 11 | /// @notice Encodes some bytes to the base64 representation 12 | function encode(bytes memory data) internal pure returns (string memory) { 13 | uint256 len = data.length; 14 | if (len == 0) return ""; 15 | 16 | // multiply by 4/3 rounded up 17 | uint256 encodedLen = 4 * ((len + 2) / 3); 18 | 19 | // Add some extra buffer at the end 20 | bytes memory result = new bytes(encodedLen + 32); 21 | 22 | bytes memory table = TABLE; 23 | 24 | assembly { 25 | let tablePtr := add(table, 1) 26 | let resultPtr := add(result, 32) 27 | 28 | for { 29 | let i := 0 30 | } lt(i, len) { 31 | 32 | } { 33 | i := add(i, 3) 34 | let input := and(mload(add(data, i)), 0xffffff) 35 | 36 | let out := mload(add(tablePtr, and(shr(18, input), 0x3F))) 37 | out := shl(8, out) 38 | out := add(out, and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)) 39 | out := shl(8, out) 40 | out := add(out, and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)) 41 | out := shl(8, out) 42 | out := add(out, and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)) 43 | out := shl(224, out) 44 | 45 | mstore(resultPtr, out) 46 | 47 | resultPtr := add(resultPtr, 4) 48 | } 49 | 50 | switch mod(len, 3) 51 | case 1 { 52 | mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) 53 | } 54 | case 2 { 55 | mstore(sub(resultPtr, 1), shl(248, 0x3d)) 56 | } 57 | 58 | mstore(result, encodedLen) 59 | } 60 | 61 | return string(result); 62 | } 63 | } 64 | --------------------------------------------------------------------------------