Somnia 백서

Somnia

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 23쪽

-ignition @nomicfoundation/hardhat-ignition-ethers ``` \ Create `.env`: ``` PRIVATE_KEY=0xYourPrivateKey SOMNIA_RPC_HTTPS=https://dream-rpc.somnia.network ``` <details> <summary><code>hardhat.config.ts</code></summary> ```typescript import "dotenv/config"; import "@nomicfoundation/hardhat-verify"; import "@nomicfoundation/hardhat-ignition-ethers"; import { HardhatUserConfig } from "hardhat/config"; const config: HardhatUserConfig = { solidity: "0.8.28", networks: { somnia: { url: process.env.SOMNIA_RPC_HTTPS!, accounts: [process.env.PRIVATE_KEY!], }, }, sourcify: { enabled: false }, etherscan: { apiKey: { somnia: process.env.SOMNIA_EXPLORER_API_KEY || "" }, customChains: [ { network: "somnia", chainId: 50312, urls: { apiURL: "https://shannon-explorer.somnia.network/api", browserURL: "https://shannon-explorer.somnia.network", }, }, ], }, }; export default config; ``` </details> Add the `Smart Contract` to the Contract directory ## Deploy with Ignition Create `ignition/modules/NFTTest.ts`: ```ts import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; const NFTTestModule = buildModule("NFTTestModule", (m) => { const initialOwner = m.getParameter("initialOwner", "0xYourOwnerAddress"); const nft = m.contract("NFTTest", [initialOwner]); return { nft }; }); export default NFTTestModule; ``` Deploy: ```bash npx hardhat ignition deploy ignition/modules/NFTTest.ts --network somnia ``` Copy the **deployed address** from the output. *** ## Verify Smart Contract ```bash npx hardhat verify --network somnia <DEPLOYED_ADDRESS> 0xYourOwnerAddress ``` > Ensure compiler version, optimizer, and constructor args match. *** ### Mint your collection We’ll mint by calling `safeMint(to, uri)` **10 times**, matching your uploaded metadata files. <details> <summary><code>scripts/mint.ts</code></summary> ```ts import { ethers } from "hardhat"; async function main() { const contractAddr = "<DEPLOYED_ADDRESS>"; const nft = await ethers.getContractAt("NFTTest", contractAddr); const owner = (await ethers.getSigners())[0]; // Example: 10 tokens at /ipfs/<CID>/{0..9}.json const CID = "<YOUR_METADATA_CID>"; for (let i = 0; i < 10; i++) { const uri = `/ipfs/${CID}/${i}.json`; const tx = await nft.safeMint(owner.address, uri); console.log(`Mint tx ${i}:`, tx.hash); await tx.wait(); } const lastId = await nft.callStatic.safeMint(owner.address, `/ipfs/${CID}/999.json`).catch(()=>null); console.log("Minted 10 tokens. Next simulated ID (no state change):", lastId ?? "N/A"); } main().catch((e) => { console.error(e); process.exit(1); }); ``` </details> Run the project: ```bash npx hardhat run scripts/mint.ts --network somnia ``` *** ## Inspect Token metadata Read a token’s URI (e.g., tokenId `0`) and open it in your browser. `scripts/read-uri.ts`: ```ts import { ethers } from "hardhat"; async function main() { const contractAddr = "<DEPLOYED_ADDRESS>"; const nft = await ethers.getContractAt("NFTTest", contractAddr); const uri = await nft.tokenURI(0); console.log("tokenURI(0):", uri); } main().catch(console.error); ``` ```bash npx hardhat run scripts/read-uri.ts --network somnia ``` Open the printed URL (it should be `https://ipfs.io/ipfs/<CID>/0.json`) in your browser to confirm JSON and `image` render correctly. Congratulations. 🎉 You have deployed your first ERC721 Smart Contract to the Somnia Network. 🎉 # Managing NFT Metadata with IPFS In this guide, you will rely on an IPFS workflow for ERC721 collections on Somnia. This guide will walk you through how\ \- You will prepare and upload artwork to IPFS via Pinata.\ \- Generate clean, wallet/marketplace-friendly metadata JSON\ \- Upload the metadata to IPFS via Pinata Cloud\ \- Deploy a Solidity contract that stores the metadata URIs onchain (for each token)\ \- Mint your NFTs. All metadata resides on IPFS, while only the URIs (pointers) are stored onchain. Please note that this guide provides a Smart Contract option for fully onchain metadata. ## Concepts You Should Know * [IPFS](https://ipfs.tech/) (InterPlanetary File System) is a “Content Addressed Storage Network”. Files are addressed by their CID (content identifier). If any bit changes, the CID changes, and it is great for integrity and verifiability. * [Pinata](https://pinata.cloud/): Pinata pins your files to IPFS and keeps them available. You’ll get CIDs (content identifiers) that never change for the same content. * TokenURI: an ERC721 method that returns a URL (often an ipfs\:// URI) pointing to a JSON file describing the NFT token (name, description, image, attributes, etc.). * Per token URI vs `baseURI`: * Per token URI (this guide): store the full JSON URI onchain for each token with ERC721URIStorage. Easiest and most flexible. * `baseURI` pattern: compute tokenURI = baseURI + tokenId (no per token storage). Cheaper for large drops, but requires sequential naming. ## Prerequisites * MetaMask is configured for a Somnia RPC and has sufficient funds for gas * Node.js 20+ (only if you’ll run the helper scripts below) * Pinata account and a JWT (recommended): Get your JWT on Pinata * `Pinata Dashboard → API Keys → New Key → choose Admin/Scoped as needed → copy JWT` ## Prepare Images If your art varies wildly in size or format, normalize once for a consistent user experience. Create an `assets` directory for your images. Run the node script below, targeting the `assets` directory to give uniformity to your images. Install the dependencies: ```bash npm i sharp fast-glob ``` Run the script: ```typescript // scripts/resize.js import fg from "fast-glob"; import sharp from "sharp"; import { mkdirSync } from "fs"; import path from "path"; const INPUT = "assets/raw-images"; // put your original images here const OUTPUT = "assets/images"; mkdirSync(OUTPUT, { recursive: true }); const files = (await fg(`${INPUT}/*.{png,jpg,jpeg,webp}`)).sort(); for (let i = 0; i < files.length; i++) { const out = path.join(OUTPUT, `${i}.png`); await sharp(files[i]).resize(1024, 1024, { fit: "cover" }).toFile(out); } console.log("Normalized images written to", OUTPUT); ``` ## Recommended Folder Structure ```markup project/ assets/ images/ 0.png 1.png 2.png ... metadata/ 0.json 1.json 2.json ... scripts/ resize.js pinata.ts (Pinata SDK init) upload-images.ts (pin images folder → IMAGES_CID) make-metadata.ts (generate JSON → uses IMAGES_CID) upload-metadata.ts (pin metadata folder → METADATA_CID) ``` * `images/` contain consistent dimensions and formats (e.g., 1024×1024 PNG). * `metadata/` contain one JSON per token ID, matching the filename (e.g., 7.json for tokenId 7). ## Upload Images to IPFS via Pinata Install Pinata SDK: ```bash npm i pinata dotenv ``` Initialise Pinata using your JWT credentials: ```typescript // pinata.ts import 'dotenv/config' import { PinataSDK } from 'pinata' export const pinata = new PinataSDK({ pinataJwt: process.env.PINATA_JWT!, // from Pinata “API Keys” pinataGateway: process.env.PINATA_GATEWAY!, // e.g. myxyz.mypinata.cloud }) ``` Create a `.env` file in your project root: ``` PINATA_JWT=eyJhbGciOi... # your Pinata JWT (keep secret) PINATA_GATEWAY=myxyz.mypinata.cloud IMAGES_CID= # leave blank until you upload images ``` \ Upload the images directory and get the root CID: ```typescript // scripts/upload-images.ts import 'dotenv/config' import { pinata } from './pinata' import fs from 'node:fs/promises' import path from 'node:path' import { File } from 'node:buffer' const DIR = 'assets/images' async function main() { const names = (await fs.readdir(DIR)) .filter(n => n.match(/\.(png|jpg|jpeg|webp)$/i)) .sort((a, b) => Number(a.split('.')[0]) - Number(b.split('.')[0])) const files: File[] = [] for (const name of names) { const bytes = await fs.readFile(path.join(DIR, name)) files.push(new File([bytes], name)) // uploaded as a single folder } const res = await pinata.upload.public.fileArray(files) // res.cid is the directory root CID console.log('Images CID:', res.cid) } main().catch(console.error) ``` Run the following command to upload the images: ```bash node dist/scripts/upload-images.js # => Images CID: bafybe... ``` Copy the CID into `.env` as `IMAGES_CID=....`<br> ## Generate Metadata JSON Each \*.json should follow the de facto standards: ```json { "name": "NFTTest #0", "description": "A clean ERC-721 on Somnia with per-token metadata.", "image": "ipfs://IMAGES_CID/0.png", "external_url": "https://your-site.example", "attributes": [ { "trait_type": "Edition", "value": 0 } ] } ``` Install `fast-glob` library: ```bash npm i fs-extra fast-glob ``` Run the script below to generate a file per image: ```typescript / scripts/make-metadata.js import fg from "fast-glob"; import { writeJSON, mkdirs } from "fs-extra"; import path from "path"; const IMAGES_CID = process.env.IMAGES_CID || "bafy..."; // paste from Step 2 const OUT_DIR = "assets/metadata"; sync function main() { await mkdirs(OUT_DIR) const imgs = (await fg('assets/images/*.{png,jpg,jpeg,webp}')).sort((a, b) => Number(path.basename(a).split('.')[0]) - Number(path.basename(b).split('.')[0]) ) for (const p of imgs) { const id = Number(path.basename(p).split('.')[0]) const json = { name: `NFTTest #${id}`, description: 'ERC-721 on Somnia with Pinata-hosted IPFS metadata.', image: `ipfs://${IMAGES_CID}/${id}.png`, attributes: [{ trait_type: 'Edition', value: id }] } await writeJSON(path.join(OUT_DIR, `${id}.json`), json, { spaces: 2 }) } console.log('Metadata written to', OUT_DIR) } main().catch(console.error) ``` To create the metadata files, run the command: ```bash node dist/scripts/make-metadata.js ``` ## Upload Metadata to IPFS Upload the metadata folder to generate the `METADATA_CID` ```typescript // scripts/upload-metadata.ts import { pinata } from './pinata' import fs from 'node:fs/promises' import path from 'node:path' import { File } from 'node:buffer' const DIR = 'assets/metadata' async function main() { const names = (await fs.readdir(DIR)) .filter(n => n.endsWith('.json')) .sort((a, b) => Number(a.split('.')[0]) - Number(b.split('.')[0])) const files: File[] = [] for (const name of names) { const bytes = await fs.readFile(path.join(DIR, name)) files.push(new File([bytes], name, { type: 'application/json' })) } const res = await pinata.upload.public.fileArray(files) console.log('Metadata CID:', res.cid) } main().catch(console.error) ``` Run the command: ``` node dist/scripts/upload-metadata.js # => Metadata CID: bafybe... ``` Your tokenURI format is now: `ipfs://<METADATA_CID>/<tokenId>.json` Make sure the script uploads `assets/metadata` and records the printed Metadata CID; you’ll mint with: `ipfs://<METADATA_CID>/<tokenId>.json` ## NFT Smart Contract We’ll use `ERC721URIStorage` Smart Contract and mint with full URIs. Paste this into Remix as `NFTTest.sol` <details> <summary>NFTTest.sol</summary> ```solidity // SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts ^5.4.0 pragma solidity ^0.8.27; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract NFTTest is ERC721, ERC721URIStorage, Ownable { uint256 private _nextTokenId; constructor(address initialOwner) ERC721("NFTTest", "NFTT") Ownable(initialOwner) {} function _baseURI() internal pure override returns (string memory) { return "https://ipfs.io"; // only affects relative paths } function safeMint(address to, string memory uri) public onlyOwner returns (uint256) { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); // store full URI (e.g., ipfs://CID/0.json) return tokenId; } function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { return super.tokenURI(tokenId); } function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) { return super.supportsInterface(interfaceId); } } ``` </details> ## Contract Walkthrough ### Imports * `ERC721` is the core NFT logic (ownership, transfers, approvals). * `ERC721URIStorage` adds per-token storage for URIs via `_setTokenURI`. * `Ownable` simple admin model; exposes onlyOwner for minting control. `ERC721URIStorage` is the most straightforward way to store a token’s exact URI. For large drops, consider using a baseURI pattern to save storage gas; otherwise, this is perfect for flexible, explicit URIs. State (\_nextTokenId) is the sequential ID counter starting at 0. First mint → tokenId = 0, then 1, 2, … etc ### Constructor Initializes collection name/symbol and sets the owner to `initialOwner` (the only account allowed to mint). ### \_baseURI() Returns <https://ipfs.io>. This is only used if you mint with relative paths (e.g., /ipfs/CID/7.json). If you mint with absolute ipfs\://… URIs (recommended), this value is ignored. ### safeMint(to, uri): Owner-only mint. Creates a new token ID, mints safely (checking receiver contracts implement IERC721Receiver), and stores the exact uri string for that token via \_setTokenURI. ### Compile and Deploy using Remix. Follow this [guide](/developer/building-dapps/tokens-and-nfts/create-erc20-tokens) Copy the deployed contract address.<br> ## How To Mint NFTs (Per Token URIs) n Remix (Deployed Contracts): * Call `safeMint(to, uri)` where: * `to` is the recipient address (can be yours). * `uri` is `ipfs://<METADATA_CID>/<id>.json` (e.g., `ipfs://bafy.../0.json`). Then call `tokenURI(0)` to confirm the stored URI.<br> If you prefer scripts for multiple mints, you can write a script to Batch Mint using Hardhat, for example: ```typescript scripts/mint.ts (Hardhat) import { ethers } from "hardhat"; const CONTRACT = "0xYourDeployedAddress"; const RECEIVER = "0xReceiver"; const METADATA_CID = "bafy..."; async function main() { const nft = await ethers.getContractAt("NFTTest", CONTRACT); for (let id = 0; id < 10; id++) { const uri = `ipfs://${METADATA_CID}/${id}.json`; const tx = await nft.safeMint(RECEIVER, uri); await tx.wait(); console.log(`Minted #${id} → ${uri}`); } } main().catch(console.error); ``` ## Validation To validate everything end to end, first do quick gateway smoke tests by opening `https://ipfs.io/ipfs/<IMAGES_CID>/0.png` and `https://ipfs.io/ipfs/<METADATA_CID>/0.json` in a browser to confirm the image and JSON are reachable. Next, call `tokenURI(0)` on your contract and verify it returns `ipfs://<METADATA_CID>/0.json`. Finally, check in a wallet or marketplace UI that the NFT renders correctly using the `ipfs://` image URI embedded in the JSON.<br> ## MetadataOptional Fully Onchain Metadata <details> <summary>Smart Contract</summary> If your art is small (SVG) or you only need text metadata, you can **encode metadata JSON on-chain**: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; contract OnchainMeta is ERC721, Ownable { using Strings for uint256; uint256 private _id; constructor(address owner_) ERC721("Onchain", "ONC") Ownable(owner_) {} function mint(address to) external onlyOwner returns (uint256) { uint256 tokenId = _id++; _safeMint(to, tokenId); return tokenId; } function tokenURI(uint256 tokenId) public view override returns (string memory) { _requireOwned(tokenId); // Example: trivial, static image via SVG data URI string memory image = string( abi.encodePacked( "data:image/svg+xml;base64,", Base64.encode( bytes( string( abi.encodePacked( "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>", "<rect width='512' height='512' fill='black'/>", "<text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='48'>#", tokenId.toString(), "</text></svg>" ) ) ) ) ) ); bytes memory json = abi.encodePacked( '{"name":"Onchain #', tokenId.toString(), '","description":"Fully on-chain metadata","image":"', image, '"}' ); return string(abi.encodePacked( "data:application/json;base64,", Base64.encode(json) )); } } ``` **Trade-offs** * No external dependencies; URIs are immutable, always available. * Higher gas (long strings), limited to compact media (SVG/text). For large images, prefer IPFS. </details> # Using Native Coin (SOMI/STT) SOMI is the native coin of the Somnia Network, similar to ETH on Ethereum. Unlike ERC20 tokens, SOMI is built into the protocol itself and does not have a contract address. {% hint style="warning" %} Kindly note that the Native Coin for Somnia Testnet is STT. {% endhint %} {% embed url="<https://www.youtube.com/watch?v=pq7sARCSTSk>" %} This multi-part guide shows how to use SOMI for: * Payments * Escrow * Donations & Tipping * Sponsored gas via Account Abstraction ## Use SOMI for Payments in Smart Contracts A simple contract that accepts exact SOMI payments: ```solidity function payToAccess() external payable { require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI"); } ``` Use `msg.value` to access the native coin sent in a transaction. No ERC20 functions are needed. To withdraw collected SOMI: ```solidity function withdraw() external onlyOwner { payable(owner).transfer(address(this).balance); } ``` Deploy using Hardhat Ignition or Viem and test with a `sendTransaction` call. <details> <summary>Example.sol</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract SOMIPayment { address public owner; constructor() { owner = msg.sender; } // Modifier to restrict access to the contract owner modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this"); _; } // User must send exactly 0.01 SOMI to access this feature function payToAccess() external payable { require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI"); // Logic for access: mint token, grant download, emit event, etc. } // Withdraw collected SOMI to owner function withdraw() external onlyOwner { payable(owner).transfer(address(this).balance); } } ``` </details> ## Build an SOMI Escrow Contract A secure escrow contract allows a buyer to deposit SOMI and later release or refund: ```solidity constructor(address payable _seller) payable { buyer = msg.sender; seller = _seller; amount = msg.value; } ``` Release funds to the seller: ```solidity function release() external onlyBuyer { seller.transfer(amount); } ``` Deploy with Hardhat Ignition and pass SOMI as `value` during deployment. <details> <summary>Example.sol</summary> ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract SOMIEscrow { address public buyer; address payable public seller; uint256 public amount; bool public isDeposited; constructor(address payable _seller) payable { buyer = msg.sender; seller = _seller; amount = msg.value; require(amount > 0, "Must deposit SOMI"); isDeposited = true; } modifier onlyBuyer() { require(msg.sender == buyer, "Only buyer can call this"); _; } function release() external onlyBuyer { require(isDeposited, "No funds to release"); isDeposited = false; seller.transfer(amount); } function refund() external onlyBuyer { require(isDeposited, "No funds to refund"); isDeposited = false; payable(buyer).transfer(amount); } } ``` </details> ## SOMI Tip Jar Allow any wallet to send tips directly: ```solidity receive() external payable { emit Tipped(msg.sender, msg.value); } ``` Withdraw all tips: ```solidity function withdraw() external onlyOwner { payable(owner).transfer(addr
프로젝트로 돌아가기