FVM Hyperspace Testnet์์ AI ์์ฑ ์ํธ NFT๋ฅผ ์์ฑํ๊ธฐ ์ํด ์์ ๋ง์ Text-to Image ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํ์ฌ DApp์ ๊ตฌ์ถ, ์คํ ๋ฐ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์๋ฒฝํ ๊ฐ์ด๋์ ๋๋ค!
์ด ๋ธ๋ก๊ทธ์์๋ ๋ค์ ๋ฐฉ๋ฒ์ ์๋ดํด ๋๋ฆฝ๋๋ค.
Tensorflow๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์คํ์์ค Python ๊ธฐ๋ฐ ํ ์คํธ-์ด๋ฏธ์ง ์คํฌ๋ฆฝํธ๋ฅผ ๊ตฌ์ถํฉ๋๋ค(๊ด์ฌ์ด ์๋ ๊ฒฝ์ฐ Bacalhau HTTP ์๋ํฌ์ธํธ๋ฅผ ์ฌ์ฉํ ์๋ ์์).
Bacalhau(๊ฐ๋ฐฉํ P2P ์คํ์ฒด์ธ ์ปดํจํ ํ๋ซํผ)์์ ์ด ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ์ธ์.
Solidity์์ NFT ๊ณ์ฝ ์์ฑ(Open Zeppelin ERC721 ๊ณ์ฝ ๊ธฐ๋ฐ)
Hardhat์ ์ฌ์ฉํ์ฌ FVM(Filecoin Virtual Machine) ํ์ดํผ์คํ์ด์ค ํ ์คํธ๋ท์ NFT ๊ณ์ฝ ๋ฐฐํฌ
ํ๋ฐํธ ์๋ ์ํธ ์์ฉ - Bacalhau ํ ์คํธ-์ด๋ฏธ์ง ์คํฌ๋ฆฝํธ ๋ฐ React์์ NFT ๊ณ์ฝ๊ณผ ์ํธ ์์ฉํ๋ ๋ฐฉ๋ฒ
NFT ๋ฉํ๋ฐ์ดํฐ๋ฅผ NFT.Storage์ ์ ์ฅํ๋ ๋ฐฉ๋ฒ
Fleek์ ํ๋ฐํธ์๋ DApp์ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ
์ ๋ ์๋์ ์ผ๋ก ์ด ์คํ์์ ์ฌ์ฉํ ์ ์๋ ์คํ ์์ค ๋ฐ ๋ถ์ฐ ๊ธฐ์ ์ ์ต๋ํ ๋ง์ด ์ฌ์ฉํ๊ธฐ๋ก ์ ํํ์ต๋๋ค.
์ด ๋ธ๋ก๊ทธ๋ ๊ฝค ๊ธธ์ด์ง ๊ฒ์ ๋๋ค. (๋ชจ๋ ์ ๋ณด๋ฅผ ์ ๊ณตํ๊ณ ์ด๋ณด์ ์นํ์ ์ด๊ณ ํฌ๊ด์ ์ธ์ง ํ์ธํ๊ณ ์ถ์ต๋๋ค!) - ํ์์ ์ ์ฉํ ๋ถ๋ถ์ผ๋ก ๊ฑด๋๋ฐ์ ๋ ๋ฉ๋๋ค. ๋ด์ฉ <3
(์๊ฒ ์ต๋๋ค - ํฌ์ผ์ดํฌ ๋๋ฏธ์ ๋๋ค #๋ฏธ์ํด์์ฃ์กํฉ๋๋ค)
์คํ ์์ค ๋ฐ Web3์ ๊ฐ์น๋ ์ฒ์๋ถํฐ ๋๊น์ง :)
๐กTLDR ํ ๐ก
์ด ์คํฌ๋ฆฝํธ๋ ์ด๋ฏธ CLI ๋ฐ HTTP ์๋ํฌ์ธํธ๋ฅผ ํตํด Bacalhau๋ฅผ ํตํด ์ฌ์ฉํ ์ ์์ผ๋ฏ๋ก ์ด ๋ถ๋ถ์ ๊ฑด๋๋ฐ์ด๋ ๋ฉ๋๋ค.
์์ ํ์ฐ์ ๋ํ ๋น ๋ฅธ ์๊ฐ
Stable Diffusion์ ํ์ฌ ํ ์คํธ-์ด๋ฏธ์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ์ต๊ณ ์ ๊ธฐ๊ณ ํ์ต ๋ชจ๋ธ์ ๋๋ค(&๋ Dall-E๊ฐ ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๋์ผํ ๋ชจ๋ธ์ ๋๋ค). ์ด๋ ์ผ์ข ์ ๋ฅ ๋ฌ๋์ ๋๋ค. ํน์ ์์ ์ ์ํํ๋๋ก ์์ฒด์ ์ผ๋ก ํ์ตํ๋ ๋จธ์ ๋ฌ๋์ ํ์ ์งํฉ์ ๋๋ค. ์ด ๊ฒฝ์ฐ์๋ ํ ์คํธ ์ ๋ ฅ์ ์ด๋ฏธ์ง ์ถ๋ ฅ์ผ๋ก ๋ณํํฉ๋๋ค.
์ด ์์์๋ ๋ณํ๊ธฐ๋ฅผ ์ฌ์ฉํ์ฌ ํ ์คํธ์์ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ ํ์ฐ ํ๋ฅ ๋ชจ๋ธ์ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
ํ์ง๋ง ๊ฑฑ์ ํ์ง ๋ง์ญ์์ค. ์ด๋ฅผ ์ํด ๊ธฐ๊ณ ํ์ต ๋ชจ๋ธ์ ๊ต์กํ ํ์๋ ์์ต๋๋ค. (ํ์ง๋ง, ๊ทธ๊ฒ ๋น์ ์ ์ผ์ด๋ผ๋ฉด ์ ์ ์ผ๋ก ๊ฐ๋ฅํฉ๋๋ค!)
๋์ ML ๊ฐ์ค์น๊ฐ ๋ฏธ๋ฆฌ ๊ณ์ฐ๋์๊ธฐ ๋๋ฌธ์ Python ์คํฌ๋ฆฝํธ์์ Google์ TensorFlow ์คํ ์์ค ๊ธฐ๊ณ ํ์ต ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฌ์ ํ๋ จ๋ ๋ชจ๋ธ์ ์ฌ์ฉํ ๊ฒ์ ๋๋ค.
๋ ์ ํํ๊ฒ๋ ์๋ ML ๋ชจ๋ธ์ ์ต์ ํ๋ Keras/TensorFlow ๊ตฌํ ํฌํฌ๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
ํ์ด์ฌ ์คํฌ๋ฆฝํธ
๐ฆ Bacalhau ๋ฌธ์ ์ ์ด @BacalhauProject YouTube ๋์์ ์์ ์ด ํ ์คํธ-์ด๋ฏธ์ง ์คํฌ๋ฆฝํธ๋ฅผ ๋น๋ํ๊ณ Dockeriseํ๊ณ Bacalhau์์ ์คํํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์ ์ฒด ์ฐ์ต์ ์ฐพ์ ์ ์์ต๋๋ค.๐ฆ ๋ํ ์ด Google Collabs Notebook ์์ ์คํํ ์๋ ์์ต๋๋ค.
์ ์ฒด Python ์คํฌ๋ฆฝํธ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค!
import argparse from stable_diffusion_tf.stable_diffusion import Text2Image from PIL import Image import os parser = argparse.ArgumentParser(description="Stable Diffusion") parser.add_argument("--h",dest="height", type=int,help="height of the image",default=512) parser.add_argument("--w",dest="width", type=int,help="width of the image",default=512) parser.add_argument("--p",dest="prompt", type=str,help="Description of the image you want to generate",default="cat") parser.add_argument("--n",dest="numSteps", type=int,help="Number of Steps",default=50) parser.add_argument("--u",dest="unconditionalGuidanceScale", type=float,help="Number of Steps",default=7.5) parser.add_argument("--t",dest="temperature", type=int,help="Number of Steps",default=1) parser.add_argument("--b",dest="batchSize", type=int,help="Number of Images",default=1) parser.add_argument("--o",dest="output", type=str,help="Output Folder where to store the Image",default="./") args=parser.parse_args() height=args.height width=args.width prompt=args.prompt numSteps=args.numSteps unconditionalGuidanceScale=args.unconditionalGuidanceScale temperature=args.temperature batchSize=args.batchSize output=args.output generator = Text2Image( img_height=height, img_width=width, jit_compile=False, # You can try True as well (different performance profile) ) img = generator.generate( prompt, num_steps=numSteps, unconditional_guidance_scale=unconditionalGuidanceScale, temperature=temperature, batch_size=batchSize, ) for i in range(0,batchSize): pil_img = Image.fromarray(img[i]) image = pil_img.save(f"{output}/image{i}.png")
์์ ์คํฌ๋ฆฝํธ๋ ๋จ์ํ ํ ์คํธ ํ๋กฌํํธ ์ ๋ ฅ ์ธ์์ ๊ธฐํ ์ ํ์ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ์ ธ์จ ๋ค์ ๋ถ๊ธฐ๋ TensorFlow ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํธ์ถํ์ฌ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์ถ๋ ฅ ํ์ผ์ ์ ์ฅํฉ๋๋ค.
์ฌ๊ธฐ์์ ์ํ๋๋ ๋ชจ๋ ์ด๋ ค์ด ์์ ์ ์๋ ์น์ ์์ ๋ฐ์ํฉ๋๋ค. ์ฌ๊ธฐ๊ฐ ๊ธฐ๊ณ ํ์ต ๋ชจ๋ธ์ด ๋ง๋ฒ์ ๋ฐํํ๋ ๊ณณ์ ๋๋ค. ๐ช
generator = Text2Image( img_height=height, img_width=width, jit_compile=False, ) img = generator.generate( prompt, num_steps=numSteps, unconditional_guidance_scale=unconditionalGuidanceScale, temperature=temperature, batch_size=batchSize, )
์ข์ต๋๋ค. ํ ์คํธ ํ๋กฌํํธ์์ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ ์ ์์ง๋ง ์... ์ด GPU ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ ์์น๋..... ๐ค๐ค
๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ด ๋ณธ์ง์ ์ผ๋ก ์ ํ์ง ๋ชปํ๋ ๊ฒ์ด ์๋ค๋ฉด ๋ฐ๋ก ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ๋๋ค. ์ด๋ ๋ฌด์ ๋ขฐ ๋ฐ ๊ฒ์ด ์ ํญ๊ณผ ๊ฐ์ ๋ค๋ฅธ ๊ฐ๋ ฅํ ์์ฑ์ ์ ๊ณตํ๊ธฐ ์ํด ๋ถ์ฐ ์์คํ ์ ํตํ ์ปดํจํ ๋น์ฉ ๋๋ฌธ์ ๋๋ค.
์์ ์์ ๋ฅผ ์ํด ๋ก์ปฌ ์ปดํจํฐ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๊ฐ๋ฅํฉ๋๋ค. ์ฌ์ค ๋๋ ์ด ํน์ ์์ ๋ฅผ ๋ด Mac M1์์ ์๋ํ๋๋ก ๊ด๋ฆฌํ์ง๋ง(๋งค์ฐ ๋ถํํจ) ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ๋ฐ ๋งค์ฐ ์ค๋ ์๊ฐ์ด ๊ฑธ๋ ธ์ต๋๋ค(ํ๊ตฌ ๊ฒ์์ ํ๋ ์ฌ๋์ด ์์ต๋๊น?). ๋ฐ๋ผ์ ๋ ํฐ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์์ํ๋ฉด ๋ ๋ง์ ๊ฐ์ค๊ฐ ํ์ํ๊ฒ ๋๋ฉฐ(๋ง์ฅ๋ ์๋) ์ง ์ฃผ๋ณ์ ์ ์ฉ ์๋ฒ๊ฐ ์์ผ๋ฉด ๊ฐ์ ๋จธ์ ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค. ํด๋ผ์ฐ๋ ์ปดํจํ ํ๋ซํผ.
์ค์ ์ง์คํ๋์ด ์์ ๋ฟ๋ง ์๋๋ผ ๋ฐ์ดํฐ๊ฐ ๊ณ์ฐ ๊ธฐ๊ณ๋ก๋ถํฐ ๋ฉ๋ฆฌ ๋จ์ด์ ธ ์๊ธฐ ๋๋ฌธ์ ๋นํจ์จ์ ์ด๋ฉฐ ๋น์ฉ์ด ๋ง์ด ๋ค ์ ์์ต๋๋ค. ์ด๋ฅผ ์ํด GPU ์ฒ๋ฆฌ๋ฅผ ์ ๊ณตํ๋ ๋ฌด๋ฃ ๊ณ์ธต ํด๋ผ์ฐ๋ ์ปดํจํ ์๋น์ค๋ฅผ ์ฐพ์ง ๋ชปํ๊ณ (๋๊ตฐ๊ฐ ์ํธํํ ์ฑ๊ตด ๊ธ์ง๋ผ๊ณ ๋งํ๋์..?) ํ ๋ฌ์ US$400๊ฐ ๋๋ ๊ณํ์ด ๋์์ต๋๋ค(๊ณ ๋ง์์).
๋ฐ์นผ๋ผ์ฐ!
๋คํํ๋ ์ด๋ฌํ ๋ฌธ์ ๋ Bacalhau๊ฐ ํด๊ฒฐํ๋ ค๊ณ ํ๋ ๋ฌธ์ ์ค ์ผ๋ถ์ ๋๋ค. Bacalhau์์๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฐ ๊ณ์ฐ์ ๋ชจ๋ ์ฌ๋์ด ์ฌ์ฉํ ์ ์๋๋ก ๊ฐ๋ฐฉํ๊ณ ์ฒ๋ฆฌ ์๊ฐ์ ๋จ์ถํ๋ ๊ฒ์ด ๊ฐ๋ฅํฉ๋๋ค. ์ฒซ์งธ, ์ฌ๋ฌ ๋ ธ๋์ ๊ฑธ์ณ ์ผ๊ด ์ฒ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ , ๋์งธ, ๋ฐ์ดํฐ๊ฐ ์๋ ๊ณณ์ ์ฒ๋ฆฌ ๋ ธ๋๋ฅผ ๋ฐฐ์นํจ์ผ๋ก์จ ๊ฐ๋ฅํฉ๋๋ค!
Bacalhau๋ IPFS, Filecoin ๋ฐ Web3์ ๋ด์ฌ๋ ๋ถ์ฐ ๊ฐ์น๋ฅผ ๋ณด๋ค ๊ด๋ฒ์ํ๊ฒ ํฌ๊ธฐํ์ง ์๊ณ ๋ฐ์ดํฐ์ ๋ํ ์คํ์ฒด์ธ ๊ณ์ฐ์ ๊ฐ๋ฅํ๊ฒ ํ์ฌ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ๋ฏธ๋๋ฅผ ๋ฏผ์ฃผํํ๋ ๋ฐ ๋์์ ์ฃผ๋ ๊ฒ์ ๋ชฉํ๋ก ํ๊ณ ์์ต๋๋ค.
Bacalhau ๋ ์ฌ์ฉ์๊ฐ IPFS(๋ฐ ๊ณง Filecoin)์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ํฌํจํ ๋ชจ๋ ๋ฐ์ดํฐ์ ๋ํด Docker ์ปจํ ์ด๋ ๋๋ ์น ์ด์ ๋ธ๋ฆฌ ์ด๋ฏธ์ง๋ฅผ ์์ ์ผ๋ก ์คํํ ์ ์๋ ๊ณต๊ฐ์ ์ด๊ณ ํฌ๋ช ํ๋ฉฐ ์ ํ์ ์ผ๋ก ๊ฒ์ฆ ๊ฐ๋ฅํ ๊ณ์ฐ ํ๋ก์ธ์ค๋ฅผ ์ํ ํ๋ซํผ์ ์ ๊ณตํ๋ P2P ๊ฐ๋ฐฉํ ๊ณ์ฐ ๋คํธ์ํฌ์ ๋๋ค. US$400 ์ด์์ด ์๋ GPU ์์ ๋ ์ง์ํฉ๋๋ค!
Bacalhau์์ ์คํฌ๋ฆฝํธ ์คํ
์ด ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋ ค๋ฉด Bacalhau์์ ์ฌ์ฉํ ์ ์๋๋ก Dockerise๋ฅผ ์ํํ๋ฉด ๋ฉ๋๋ค. ๊ทธ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด ์ฌ๊ธฐ ํํ ๋ฆฌ์ผ์ ๋ฐ๋ผ๊ฐ ์ ์์ต๋๋ค. ๊ทธ๋ฐ ๋ค์ ๋จ ํ ์ค์ ์ฝ๋๋ก Bacalhau CLI๋ฅผ ์ฌ์ฉํ์ฌ ์คํํ ์ ์์ต๋๋ค(๋ค๋ฅธ ํ ์ค๋ก Bacalhau๋ฅผ ์ค์นํ ํ).
bacalhau docker run --gpu 1 ghcr.io/bacalhau-project/examples/stable-diffusion-gpu:0.0.1 -- python main.py --o ./outputs --p "Rainbow Unicorn"
ํ์ง๋ง ์ด ์์์๋ ํตํฉ ์น์ ์์ ๋ณด์ฌ๋๋ฆด ์ด ๊ณ ์ ๋ ์์ ์ ์ธ ํ์ฐ ์คํฌ๋ฆฝํธ์ ์ฐ๊ฒฐํ๋ HTTP ์๋ํฌ์ธํธ๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค!
ํ์ง๋ง ์ฌ๊ธฐ์๋ ์ด๊ฒ์ด ์น3 ์นํ์ ์ธ ๋ฐ์ดํฐ ๊ณ์ฐ ํ๋ก์ธ์ค๋ฅผ ์คํํ๋ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ ๋ฐฉ๋ฒ์ด๋ผ๋ ์ ์ ์ฃผ๋ชฉํ๊ฒ ์ต๋๋ค. ์ฐ๋ฆฌ๋ ์ด ํ๋์ ์์ ๋ชจ๋ธ์๋ง ๊ตญํ๋์ง ์์ต๋๋ค.
๊ทธ๋๋ NFT ์คํฌ๋ฆฝํธ๋ก ๋์ด๊ฐ๊ฒ ์ต๋๋ค! :)
์ค๋งํธ ๊ณ์ฝ
NFT ์ค๋งํธ ๊ณ์ฝ์ Open Zeppelin์ ERC721 ๊ตฌํ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ง๋ง ๋ฉํ๋ฐ์ดํฐ ํ์ค ํ์ฅ์ด ํฌํจ๋ ERC721URIStorage ๋ฒ์ ์ ์ฌ์ฉํฉ๋๋ค(๋ฐ๋ผ์ NFT.Storage์ ์ ์ฅํ IPFS ์ฃผ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ณ์ฝ์ ์ ๋ฌํ ์ ์์ต๋๋ค). .
์ด ๊ธฐ๋ณธ ๊ณ์ฝ์ mint() ๋ฐ transfer()์ ๊ฐ์ ๊ธฐ๋ฅ์ด ์ด๋ฏธ ๊ตฌํ๋ NFT ๊ณ์ฝ์ ์ผ๋ฐ ๊ธฐ๋ฅ์ ์ถ๊ฐ๋ก ์ ๊ณตํฉ๋๋ค.
๋ํ ์๋ก์ด NFT๊ฐ ์์ฑ๋ ๋๋ง๋ค ์ฒด์ธ์์ ๋ฐฉ์ถ๋ ์ด๋ฒคํธ๋ฟ๋ง ์๋๋ผ ํ๋ฐํธ ์๋์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ ๋ช ๊ฐ์ง getter ํจ์๋ ์ถ๊ฐํ์์ ์ ์ ์์ต๋๋ค. ์ด๋ DApp์์ ์จ์ฒด์ธ ์ด๋ฒคํธ๋ฅผ ์์ ํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
BacalhauFRC721.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "@hardhat/console.sol"; contract BacalhauFRC721 is ERC721URIStorage { /** @notice Counter keeps track of the token ID number for each unique NFT minted in the NFT collection */ using Counters for Counters.Counter; Counters.Counter private _tokenIds; /** @notice This struct stores information about each NFT minted */ struct bacalhauFRC721NFT { address owner; string tokenURI; uint256 tokenId; } /** @notice Keeping an array for each of the NFT's minted on this contract allows me to get information on them all with a read-only front end call */ bacalhauFRC721NFT[] public nftCollection; /** @notice The mapping allows me to find NFT's owned by a particular wallet address. I'm only handling the case where an NFT is minted to an owner in this contract - but you'd need to handle others in a mainnet contract like sending to other wallets */ mapping(address => bacalhauFRC721NFT[]) public nftCollectionByOwner; /** @notice This event will be triggered (emitted) each time a new NFT is minted - which I will watch for on my front end in order to load new information that comes in about the collection as it happens */ event NewBacalhauFRC721NFTMinted( address indexed sender, uint256 indexed tokenId, string tokenURI ); /** @notice Creates the NFT Collection Contract with a Name and Symbol */ constructor() ERC721("Bacalhau NFTs", "BAC") { console.log("Hello Fil-ders! Now creating Bacalhau FRC721 NFT contract!"); } /** @notice The main function which will mint each NFT. The ipfsURI is a link to the ipfs content identifier hash of the NFT metadata stored on NFT.Storage. This data minimally includes name, description and the image in a JSON. */ function mintBacalhauNFT(address owner, string memory ipfsURI) public returns (uint256) { // get the tokenID for this new NFT uint256 newItemId = _tokenIds.current(); // Format info for saving to our array bacalhauFRC721NFT memory newNFT = bacalhauFRC721NFT({ owner: msg.sender, tokenURI: ipfsURI, tokenId: newItemId }); //mint the NFT to the chain _mint(owner, newItemId); //Set the NFT Metadata for this NFT _setTokenURI(newItemId, ipfsURI); _tokenIds.increment(); //Add it to our collection array & owner mapping nftCollection.push(newNFT); nftCollectionByOwner[owner].push(newNFT); // Emit an event on-chain to say we've minted an NFT emit NewBacalhauFRC721NFTMinted( msg.sender, newItemId, ipfsURI ); return newItemId; } /** * @notice helper function to display NFTs for frontends */ function getNFTCollection() public view returns (bacalhauFRC721NFT[] memory) { return nftCollection; } /** * @notice helper function to fetch NFT's by owner */ function getNFTCollectionByOwner(address owner) public view returns (bacalhauFRC721NFT[] memory){ return nftCollectionByOwner[owner]; }
์๊ตฌ์ฌํญ
์ ๋ ์ด ๊ณ์ฝ์ Filecoin Virtual Machine Hyperspace Testnet ์ ๋ฐฐํฌํ ์์ ์ด์ง๋ง ์ด ๊ณ์ฝ์ Polygon, BSC, Optimism, Arbitrum, Avalanche ๋ฑ์ ํฌํจํ ๋ชจ๋ EVM ํธํ ์ฒด์ธ์ ๋ฐฐํฌํ ์ ์์ต๋๋ค. ๋ฉํฐ ์ฒด์ธ NFT๋ฅผ ๋ง๋ค๊ธฐ ์ํด ํ๋ฐํธ ์๋๋ฅผ ์กฐ์ ํ ์๋ ์์ต๋๋ค(ํํธ: ์ด ์ ์ฅ์ )!
Hyperspace Testnet์ ๋ฐฐํฌํ๋ ค๋ฉด ๋ค์์ด ํ์ํฉ๋๋ค.
Hardhat๊ณผ ํจ๊ป ์ค๋งํธ ๊ณ์ฝ ๋ฐฐํฌ
์ ๋ hardhat์ ์ฌ์ฉํ์ฌ ์ด ๊ณ์ฝ์ Hyperspace ํ ์คํธ๋ท์ ๋ฐฐํฌํ๊ณ ์์ต๋๋ค.
๐ธ ์ด๊ณต๊ฐ RPC ๋ฐ BlockExplorer ์ต์ :
๊ณต๊ฐ RPC ๋์
๋ธ๋ก์ต์คํ๋ก๋ฌ์
https://filecoin-hyperspace.chainstacklabs.com/rpc/v0
https://hyperspace.filfox.info/rpc/v0
https://fvm.starboard.ventures/contracts/
https://rpc.ankr.com/filecoin_testnet
https://explorer.glf.io/?network=hyperspacenet
์คํ API : beryx.zondax.ch
๊ตฌ์ฑ ์ค์ ์ ์ํด ์ฌ์ฉ ๊ฐ๋ฅํ ๊ณต๊ฐ RPC ์๋ํฌ์ธํธ ์ค ํ๋๋ฅผ ์ ํํ ์ ์์ต๋๋ค.
hardhat.config.ts
import '@nomicfoundation/hardhat-toolbox'; import { config as dotenvConfig } from 'dotenv'; import { HardhatUserConfig } from 'hardhat/config'; import { resolve } from 'path'; //Import our customised tasks // import './pages/api/hardhat/tasks'; const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || './.env'; dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) }); // Ensure that we have all the environment variables we need. const walletPrivateKey: string | undefined = process.env.WALLET_PRIVATE_KEY; if (!walletPrivateKey) { throw new Error('Please set your Wallet private key in a .env file'); } const config: HardhatUserConfig = { solidity: '0.8.17', defaultNetwork: 'filecoinHyperspace', networks: { hardhat: {}, filecoinHyperspace: { url: 'https://api.hyperspace.node.glif.io/rpc/v1', chainId: 3141, accounts: [process.env.WALLET_PRIVATE_KEY ?? 'undefined'], }, // bleeding edge often-reset FVM testnet filecoinWallaby: { url: 'https://wallaby.node.glif.io/rpc/v0', chainId: 31415, accounts: [process.env.WALLET_PRIVATE_KEY ?? 'undefined'], //explorer: https://wallaby.filscan.io/ and starboard }, }, // I am using the path mapping so I can keep my hardhat deployment within the /pages folder of my DApp and therefore access the contract ABI for use on my frontend paths: { root: './pages/api/hardhat', tests: './pages/api/hardhat/tests', //who names a directory in the singular?!!! Grammarly would not be happy cache: './pages/api/hardhat/cache', }, }; export default config;
๊ทธ๋ฆฌ๊ณ ์ค๋งํธ ๊ณ์ฝ์ ๋ฐฐํฌํ๊ธฐ ์ํด ๋ฐฐํฌ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํฉ๋๋ค. ์ฌ๊ธฐ์๋ ์๋ช ์(์์ ์)๋ก ์ง๊ฐ ์ฃผ์๋ฅผ ๊ตฌ์ฒด์ ์ผ๋ก ์ค์ ํ๊ณ ์์ต๋๋ค. ์์ฑํ๋ ์์ ์ FEVM์์ ์ฌ์ ํ ์๋ ์ค์ธ ๋ช ๊ฐ์ง ๋งคํ ์ค๋ฅ๊ฐ ์์ต๋๋ค. ๋ญ๊ฐ ์ด์ํ ํ๋.
deploy/deployBacalhauFRC721.ts
import hre from 'hardhat'; import type { BacalhauFRC721 } from '../typechain-types/contracts/BacalhauFRC721'; import type { BacalhauFRC721__factory } from '../typechain-types/factories/contracts/BacalhauFRC721__factory'; async function main() { console.log('Bacalhau721 deploying....'); // !!!needed as hardhat's default does not map correctly to the FEVM const owner = new hre.ethers.Wallet( process.env.WALLET_PRIVATE_KEY || 'undefined', hre.ethers.provider ); const bacalhauFRC721Factory: BacalhauFRC721__factory = < BacalhauFRC721__factory > await hre.ethers.getContractFactory('BacalhauFRC721', owner); const bacalhauFRC721: BacalhauFRC721 = <BacalhauFRC721>( await bacalhauFRC721Factory.deploy() ); await bacalhauFRC721.deployed(); console.log('bacalhauFRC721 deployed to ', bacalhauFRC721.address); // optionally log to a file here } main().catch((error) => { console.error(error); process.exitCode = 1; });
๋ฐฐํฌํ๋ ค๋ฉด ๋ค์ ์ฝ๋๋ฅผ ์ฌ์ฉํ์ฌ ํฐ๋ฏธ๋์์ ์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํฉ๋๋ค. (์ฃผ์: ๊ตฌ์ฑ์์ ๊ธฐ๋ณธ ๋คํธ์ํฌ๋ฅผ filecoinHyperspace๋ก ์ค์ ํ๊ธฐ ๋๋ฌธ์ ์๋์ ํ์๋์ด ์์ง๋ง ๋คํธ์ํฌ์ ๋ํ ํ๋๊ทธ๋ฅผ ์ ๋ฌํ ํ์๋ ์์ต๋๋ค.)
> cd ./pages/hardhat/deploy/
npx hardhat run ./deployBacalhauFRC721.ts --network filecoinHyperspace
์ถํํ๋ค! ๋ฐฉ๊ธ Filecoin ์ด๊ณต๊ฐ ํ ์คํธ๋ท์ NFT ๊ณ์ฝ์ ๋ฐฐํฌํ์ต๋๋ค!
์ฐ์~ ์์ ๋ถ๋ถ์ด๊ตฐ์... ๊ทธ๋ฆฌ๊ณ ์ฌ๊ธฐ ๋ชจ๋๋ฅผ ํ๋๋ก ๋ฌถ์ด์ฃผ๋ ์ ์ฐฉ์ ๋ ์์ด์ :)
ํ๋ฐํธ ์๋๋ฅผ ๊ตฌ์ถํ๊ธฐ ์ํด NextJS์ Typescript๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ํ์ง๋ง ์์งํ ๋งํด์ ์ ๋ NextJS์ SSR(์๋ฒ ์ธก ๋ ๋๋ง) ๊ธฐ๋ฅ์ ํ์ฉํ์ง ์๊ณ ์์ผ๋ฉฐ ํ์ด์ง ๋ผ์ฐํ ๋ ์ฌ์ฉํ์ง ์์ต๋๋ค(๋จ์ผ ํ์ด์ง Dapp์ด๊ธฐ ๋๋ฌธ์). ๋ฐ๋๋ผ React ์ค์ (๋๋ ๋ฌผ๋ก ์ ํํ ํ๋ ์์ํฌ!)์ ์ฌ์ฉํฉ๋๋ค.
Typescript์ ๊ดํด์๋... ์, ์ ๋ ์ด๊ฒ์ ์ฝ๊ฐ ๊ธํ๊ฒ ๋ง๋ค์๊ณ ์ด๊ฒ์ด Typescript์ ์์ฃผ ์ข์ ์๋ ์๋๋ผ๋ ์ ์ ์ธ์ ํด์ผ ํฉ๋๋ค. ํ์ง๋ง vars๋ ํ๋ณตํด ๋ณด์ ๋๋ค... ;)
Anyhoo - ์ด ์น์ ์ ์ฃผ์ ์์ ์ ํ๋ฐํธ ์๋ ์ฝ๋ฉ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ด ์๋๋ผ ์ค๋งํธ ๊ณ์ฝ์ธ Bacalhau(์์ ์ ์ธ ํ์ฐ ML ๋ชจ๋ธ ์ฌ์ฉ) ๋ฐ ๋ฌผ๋ก NFT.Storage์ ์ํธ ์์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ๋๋ค. NotOnIPFSNotYourNFT.
[todo: ์์๋ ๋ค์ด์ด๊ทธ๋จ ์์ฑ]
์ข์ต๋๋ค. ์ด๋ฅผ ์ฝ๋์์ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค!
Bacalhau์ฉ ํ๋ฐํธ ์๋ API ์๋ํฌ์ธํธ ์์ฑ์ ์์ง๋์ด Luke Marsden ์ด ์์ฑํ ์ด ํ๋ก์ ํธ ๋ณด๊ณ ์ ์ ๋ฌธ์ํ๋์ด ์์ต๋๋ค.
API๋ ํ์ฌ ์ด ๋ธ๋ก๊ทธ์ ๋ฌธ์ํ๋ ์์ ์ ์ธ ํ์ฐ ์คํฌ๋ฆฝํธ ์๋ง ์ง์ ์ ์ผ๋ก ์ ์ฉ๋์ง๋ง ํ์์๋ HTTP์์ ์์ ์ ์์ฒด ๋ฐฐํฌ ์คํฌ๋ฆฝํธ๋ฅผ ํธ์ถํ ์ ์๋๋ก ์ด๋ฅผ ๋ณด๋ค ์ผ๋ฐ์ ์ธ API๋ก ํ์ฅํ๋ ๊ณผ์ ์ ์์ต๋๋ค. REST API. ์ฌ๊ธฐ ๋๋ FilecoinProject Slack์ #bacalhau ์ฑ๋์์ ์ด ๋ด์ฉ์ ๊ณ์ ์ง์ผ๋ณด์ธ์.
>run/test in terminal
curl -XPOST -d '{"prompt": "rainbow unicorn"}' 'http://dashboard.bacalhau.org:1000/api/v1/stablediffusion';
>react / typescript code
import { CID } from 'multiformats/cid'; export const callBacalhauJob = async (promptInput: string) => { //Bacalahau HTTP Stable Diffusion Endpoint const url = 'http://dashboard.bacalhau.org:1000/api/v1/stablediffusion'; const headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; const data = { prompt: promptInput, //The user text prompt! }; /* FETCH FROM BACALHAU ENDPOINT */ const cid = await fetch(url, { method: 'POST', body: JSON.stringify(data), headers: headers, }) .then(async (res) => { let body = await res.json(); if (body.cid) { /* Bacalhau returns a V0 CID which we want to convert to a V1 CID for easier usage with http gateways (ie. displaying the image on web), so I'm using the IPFS multiformats package to convert it here */ return CID.parse(body.cid).toV1().toString(); } }) .catch((err) => { console.log('error in bac job', err); }); return cid; };
์ด ํจ์๋ ์๋์ ๊ฐ์ ํด๋ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง IPFS CID(์ฝํ
์ธ ์๋ณ์)๋ฅผ ๋ฐํํฉ๋๋ค. ๊ทธ๋ฌ๋ฉด ์ด๋ฏธ์ง๋ /outputs/image0.png
์๋์์ ์ฐพ์ ์ ์์ต๋๋ค.
๐ก ์ฌ๊ธฐ๋ฅผ ํด๋ฆญํ์ฌ ์ง์ ํ์ธํด๋ณด์ธ์ ! ๐ก
์ ๋ฌด์ง๊ฐ ์ ๋์ฝ... ๋ง์์ ์ ๋๋ ๊ฒ ๋ญ์ฃ !
NFT.Storage๋ ์๋ฐ์คํฌ๋ฆฝํธ ๋๋ HTTP SDK๋ฅผ ์ฌ์ฉํ์ฌ NFT ๋ฉํ๋ฐ์ดํฐ๋ฅผ IPFS ๋ฐ Filecoin์ ์๊ตฌ์ ์ผ๋ก ์ฝ๊ฒ ์ ์ฅํ ์ ์๊ฒ ํด์ฃผ๋ ๊ณต๊ณต์ฌ(๋ฌด๋ฃ)์ ๋๋ค.
NFT ๋ฉํ๋ฐ์ดํฐ๋ Open Zeppelin ๋ฌธ์์์ ์ง์ ๊ฐ์ ธ์จ ์๋ ์์ ์ ์ฌํ JSON ๋ฌธ์์ ๋๋ค.
NFT๋ฅผ ์์ฑํ ๋ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ฒด์ธ์ ์ ์ฅํ์ง ์๋ ํ(๋์ฉ๋ ํ์ผ์ ๊ฒฝ์ฐ ์์ฒญ๋๊ฒ ๋น์ฉ์ด ๋ง์ด ๋ค ์ ์์) ํ ํฐ์ '๋์ฒด ๋ถ๊ฐ๋ฅ์ฑ'์ ์ค์ํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ ์ฅ์๊ฐ ํ์ํ๋ค๋ ์ ์ ์ ์ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์ง์์ ์ด๊ณ ์์ ์ ์ด๋ฉฐ ๋ถ๋ณํฉ๋๋ค.
NFT์ ์์ ์์ ๊ฐ์ ์์น ๊ธฐ๋ฐ ์ฃผ์๊ฐ ์๋ ๊ฒฝ์ฐ ํ๋งค ํ ์ด ์์น ๊ฒฝ๋ก๋ฅผ ์ ํํ๋ ๊ฒ์ ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค. ์ฆ, ๊ตฌ๋งคํ๋ค๊ณ ์๊ฐํ NFT๊ฐ ์์ ํ ๋ค๋ฅธ ๊ฒ์ผ๋ก ๋ฐ๋๊ฑฐ๋ ์ด ๊ฒฝ์ฐ ๋ฌธ์ ๊ทธ๋๋ก ์ํ์๊ฐ ๋ฉ๋๋ค. NFT ์ ์์๊ฐ ๋ฌ๊ทธ ์ฌ์ง์ ์ํธ ์ด๋ฏธ์ง๋ฅผ ์ ํํ ๊ณณ์ ์๋์ ๋๋ค.
Open Zeppelin์์๋ ๊ฒฝ๊ณ ํ๋ ๋ด์ฉ์ด ์์ต๋๋ค!
NFT.Storage๋ฅผ ์ฌ์ฉํ๋ค๋ ๊ฒ์ IPFS์ ๊ณ ์ ๋ ๋ฟ๋ง ์๋๋ผ ์ง์์ฑ์ ์ํด Filecoin์ ์ ์ฅ๋๋ ๋ฉํ๋ฐ์ดํฐ์ ๋ํ ๋ณ๊ฒฝ ๋ถ๊ฐ๋ฅํ IPFS ํ์ผ CID( ์ฝํ ์ธ - ์์น๊ฐ ์๋ - ID ์๋ณ์)๋ฅผ ์ป๋๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค. NFT.Storage๋ฅผ ์คํํ๊ณ ์ด์ ๋ํ API ํค (.env ํ์ผ์ ์ ์ฅํ๊ธฐ ์ํด)๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
.env example
NEXT_PUBLIC_NFT_STORAGE_API_KEY=xxx
๋ํ FVM์๋ (์์ง!) NFT ๋ง์ผํ๋ ์ด์ค๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ฌ๋ฐ๋ฅธ ํ์์ ๋ฉํ๋ฐ์ดํฐ JSON์ ์์ฑํ๋์ง ํ์ธํด์ผ ํฉ๋๋ค... ์ฐ๋ฆฌ๋ NFT๊ฐ ์ฑํ๋ ๋ ์ฌ์ ํ ํ์ค์ ์ค์ํ๋์ง ํ์ธํ๊ณ ์ถ์ต๋๋ค. .
import { NFTStorage } from 'nft.storage'; //connect to NFT.Storage Client const NFTStorageClient = new NFTStorage({ token: process.env.NEXT_PUBLIC_NFT_STORAGE_API_KEY, }); const createNFTMetadata = async ( promptInput: string, imageIPFSOrigin: string, //the ipfs path eg. ipfs://[CID] imageHTTPURL: string //an ipfs address fetchable through http for the front end to use (ie. including an ipfs http gateway on it like https://[CID].ipfs.nftstorage.link) ) => { console.log('Creating NFT Metadata...'); let nftJSON; // let's get the image data Blob from the IPFS CID that was returned from Bacalhau earlier... await getImageBlob(status, setStatus, imageHTTPURL).then( async (imageData) => { // Now let's create a unique CID for that image data - since we don't really want the rest of the data returned from the Bacalhau job.. await NFTStorageClient.storeBlob(imageData) .then((imageIPFS) => { console.log(imageIPFS); //Here's the JSON construction - only name, description and image are required fields- but I also want to save some other properties like the ipfs link and perhaps you have other properties that give your NFT's rarity to add as well nftJSON = { name: 'Bacalhau Hyperspace NFTs 2023', description: promptInput, image: imageIPFSOrigin, properties: { prompt: promptInput, type: 'stable-diffusion-image', origins: { ipfs: `ipfs://${imageIPFS}`, bacalhauipfs: imageIPFSOrigin, }, innovation: 100, content: { 'text/markdown': promptInput, }, }, }; }) .catch((err) => console.log('error creating blob cid', err)); } ); return nftJSON; };
์ด์ ์ด ๋ฉํ๋ฐ์ดํฐ๋ฅผ NFT.Storage์ ์ ์ฅํด ๋ณด๊ฒ ์ต๋๋ค!
await NFTStorageClient.store(nftJson) .then((metadata) => { // DONE! - do something with this returned metadata! console.log('NFT Data pinned to IPFS & stored on Filecoin!'); console.log('Metadata URI: ', metadata.url); // once saved we can use it to mint the NFT // mintNFT(metadata); }) .catch((err) => { console.log('error uploading to nft.storage'); });
Woot - Bacalhau์ ์ด๋ฏธ์ง๊ฐ ์๊ณ NFT.Storage๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ณ์ ์ด๊ณ ์ง์์ ์ผ๋ก ์ ์ฅํ์ต๋๋ค. ์ด์ NFT๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค!
๐ก ๋น ๋ฅธ ํ ๐กNFT.Storage๋ ๋ํ storeCar ๋ฐ storeDirectory์ ๊ฐ์ ๋ค์ํ API ํธ์ถ ๊ณผ CID์ IPFS ๊ณ ์ ๋ฐ Filecoin ์ ์ฅ ๊ฑฐ๋๋ฅผ ๋ฐํํ๋ status() ํจ์๋ฅผ ์ ๊ณตํฉ๋๋ค. -> ์ด๋ ๋งค์ฐ ๋ฉ์ง ์ถ๊ฐ ๊ธฐ๋ฅ์ด ๋ ์ ์์ต๋๋ค. NFT ์ํ๋ฅผ ํ์ธํ๊ธฐ ์ํ FEVM DApp(๋๋ FEVM์ด ๋ฉ์ธ๋ท ๋ฆด๋ฆฌ์ค์ ๋๋ฌํ๋ฉด FEVM์ NFT ๊ตฌํ).
์ฌ๊ธฐ์๋ 3๊ฐ์ง ์ ํ์ ์ํธ ์์ฉ์ด ์์ต๋๋ค. (๊ทธ๋ฆฌ๊ณ ๋ช ๊ฐ์ง FEVM ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๋ฒ ํ ๊ธฐ์ ์๋ ํญ์ ๊ธฐ๋ฐํ ๋ฒ๊ทธ ๊ธฐ๋ฅ์ด ์์ต๋๋ค!)
๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ์ง ์๊ณ ์ฒด์ธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๊ธฐ ์ํ ์ฝ๊ธฐ ์ ์ฉ ํธ์ถ
์๋ช ํ๊ณ ๊ฐ์ค๋ฅผ ์ง๋ถํ๊ธฐ ์ํด ์ง๊ฐ์ด ํ์ํ ํธ์ถ์ ์์ฑํฉ๋๋ค. NFT ๋ฐํ๊ณผ ๊ฐ์ด ์ฒด์ธ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๊ธฐ๋ฅ!
์ด๋ฒคํธ ๋ฆฌ์ค๋ - ๊ณ์ฝ์์ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์์ ํฉ๋๋ค.
์ด๋ฌํ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ํด Ethereum API์ฉ ๊ฒฝ๋ ๋ํผ์ธ ethers.js ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ณ์ฝ์ ์ฐ๊ฒฐํ๊ณ ํธ์ถ์ ์ํํฉ๋๋ค.
๊ณต๊ฐ RPC๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๊ธฐ ๋ชจ๋๋ก ๊ณ์ฝ์ ์ฐ๊ฒฐ:
//The compiled contract found in pages/api/hardhat/artifacts/contracts import BacalhauCompiledContract from '@Contracts/BacalhauFRC721.sol/BacalhauFRC721.json'; //On-chain address of the contract const contractAddressHyperspace = '0x773d8856dd7F78857490e5Eea65111D8d466A646'; //A public RPC Endpoint (see table from contract section) const rpc = 'https://api.hyperspace.node.glif.io/rpc/v1'; const provider = new ethers.providers.JsonRpcProvider(rpc); const connectedReadBacalhauContract = new ethers.Contract( contractAddressHyperspace, BacalhauCompiledContract.abi, provider );
๊ณ์ฝ์ ๋ํ ์ด๋ฒคํธ๋ฅผ ์์ ํฉ๋๋ค. ์ด๋ ์ฝ๊ธฐ ์ ์ฉ(๊ฐ์ ธ์ค๊ธฐ) ์ด๋ฒคํธ์ด๋ฏ๋ก ๊ณต๊ฐ RPC๋ฅผ ์ฌ์ฉํ์ฌ ์จ์ฒด์ธ์์ ์ด๋ฒคํธ ๋ฐฉ์ถ์ ์์ ํ ์ ์์ต๋๋ค.
//use the read-only connected Bacalhau Contract connectedReadBacalhauContract.on( // Listen for the specific event we made in our contract 'NewBacalhauFRC721NFTMinted', (sender: string, tokenId: number, tokenURI: string) => { //DO STUFF WHEN AN EVENT COMES IN // eg. re-fetch NFT's, store in state and change page status } );
์ฐ๊ธฐ ๋ชจ๋์์ ๊ณ์ฝ์ ์ฐ๊ฒฐ - ์ด๋ฅผ ์ํด์๋ ์ฌ์ฉ์๊ฐ ๊ฑฐ๋์ ์๋ช ํ๊ณ ๊ฐ์ค ๋น์ฉ์ ์ง๋ถํ ์ ์๋๋ก Ethereum ๊ฐ์ฒด๊ฐ ์ง๊ฐ์ ์ํด ์น ๋ธ๋ผ์ฐ์ ์ ์ฃผ์ ๋์ด์ผ ํฉ๋๋ค. ์ด๊ฒ์ด ์ฐ๋ฆฌ๊ฐ window.ethereum์ ํ์ธํ๋ ์ด์ ์ ๋๋ค. ๋ฌผ์ฒด.
//Typescript needs to know window is an object with potentially and ethereum value. There might be a better way to do this? Open to tips! declare let window: any; //The compiled contract found in pages/api/hardhat/artifacts/contracts import BacalhauCompiledContract from '@Contracts/BacalhauFRC721.sol/BacalhauFRC721.json'; //On-chain address of the contract const contractAddressHyperspace = '0x773d8856dd7F78857490e5Eea65111D8d466A646'; //check for the ethereum object if (!window.ethereum) { //ask user to install a wallet or connect //abort this } // else there's a wallet provider else { // same function - different provider - this one has a signer - the user's connected wallet address const provider = new ethers.providers.Web3Provider(window.ethereum); const contract = new ethers.Contract( contractAddressHyperspace, BacalhauCompiledContract.abi, provider ); const signer = provider.getSigner(); const connectedWriteBacalhauContract = contract.connect(signer); }
์ฐ๊ธฐ ์ฐ๊ฒฐ ๊ณ์ฝ์ ์ฌ์ฉํ์ฌ mint ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
๋จผ์ , ์ฌ์ฉ์์ ์ง๊ฐ ์ฃผ์๊ฐ ์๊ณ FVM Hyperspace ์ฒด์ธ์ ์๋์ง ํ์ธํ์ธ์. chainId๋ฅผ ํ์ธํ๋ ๋ฐฉ๋ฒ, ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก Hyperspace ๋คํธ์ํฌ๋ฅผ Metamask/์ง๊ฐ์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ ๋ฑ ์ฌ๋ฌ๋ถ์ด ์ํ ์ ์๋ ๋ช ๊ฐ์ง ์ ์ฉํ ์ง๊ฐ ๊ธฐ๋ฅ์ด ์์ต๋๋ค. Ethereum ๊ฐ์ฒด๋ฅผ ์ง์ ์ฌ์ฉํ๊ฑฐ๋ ethers.js๋ฅผ ์ฌ์ฉํ์ฌ ์ง๊ฐ๊ณผ ์ํธ ์์ฉํ ์ ์์ต๋๋ค.
declare let window: any; const fetchWalletAccounts = async () => { console.log('Fetching wallet accounts...'); await window.ethereum //use ethers? .request({ method: 'eth_requestAccounts' }) .then((accounts: string[]) => { return accounts; }) .catch((error: any) => { if (error.code === 4001) { // EIP-1193 userRejectedRequest error console.log('Please connect to MetaMask.'); } else { console.error(error); } }); }; const fetchChainId = async () => { console.log('Fetching chainId...'); await window.ethereum .request({ method: 'eth_chainId' }) .then((chainId: string[]) => { return chainId; }) .catch((error: any) => { if (error.code === 4001) { // EIP-1193 userRejectedRequest error console.log('Please connect to MetaMask.'); } else { console.error(error); } }); }; //!! This function checks for a wallet connection WITHOUT being intrusive to to the user or opening their wallet export const checkForWalletConnection = async () => { if (window.ethereum) { console.log('Checking for Wallet Connection...'); await window.ethereum .request({ method: 'eth_accounts' }) .then(async (accounts: String[]) => { console.log('Connected to wallet...'); // Found a user wallet return true; }) .catch((err: Error) => { console.log('Error fetching wallet', err); return false; }); } else { //Handle no wallet connection return false; } }; //Subscribe to changes on a user's wallet export const setWalletListeners = () => { console.log('Setting up wallet event listeners...'); if (window.ethereum) { // subscribe to provider events compatible with EIP-1193 standard. window.ethereum.on('accountsChanged', (accounts: any) => { //logic to check if disconnected accounts[] is empty if (accounts.length < 1) { //handle the locked wallet case } if (userWallet.accounts[0] !== accounts[0]) { //user has changed address } }); // Subscribe to chainId change window.ethereum.on('chainChanged', () => { // handle changed chain case }); } else { //handle the no wallet case } }; export const changeWalletChain = async (newChainId: string) => { console.log('Changing wallet chain...'); const provider = window.ethereum; try { await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: newChainId }], //newChainId }); } catch (error: any) { alert(error.message); } }; //AddHyperspaceChain export const addHyperspaceNetwork = async () => { console.log('Adding the Hyperspace Network to Wallet...'); if (window.ethereum) { window.ethereum .request({ method: 'wallet_addEthereumChain', params: [ { chainId: '0xc45', rpcUrls: [ 'https://hyperspace.filfox.info/rpc/v0', 'https://filecoin-hyperspace.chainstacklabs.com/rpc/v0', ], chainName: 'Filecoin Hyperspace', nativeCurrency: { name: 'tFIL', symbol: 'tFIL', decimals: 18, }, blockExplorerUrls: [ 'https://fvm.starboard.ventures/contracts/', 'https://hyperspace.filscan.io/', 'https://beryx.zondax.chfor', ], }, ], }) .then((res: XMLHttpRequestResponseType) => { console.log('added hyperspace successfully', res); }) .catch((err: ErrorEvent) => { console.log('Error adding hyperspace network', err); }); } };
์ฐ๊ธฐ ๋ชจ๋์์ ๊ณ์ฝ ๋ฐํ ๊ธฐ๋ฅ์ ํธ์ถํฉ๋๋ค....
// Pass in the metadata return from saving to NFT.Storage const mintNFT = async (metadata: any) => { await connectedWriteBacalhauContract // The name of our function in our smart contract .mintBacalhauNFT( userWallet.accounts[0], //users account to use metadata.url //test ipfs address ) .then(async (data: any) => { console.log('CALLED CONTRACT MINT FUNCTION', data); await data .wait() .then(async (tx: any) => { console.log('tx', tx); //CURRENTLY NOT RETURNING TX - (I use event triggering to know when this function is complete) let tokenId = tx.events[1].args.tokenId.toString(); console.log('tokenId args', tokenId); setStatus({ ...INITIAL_TRANSACTION_STATE, success: successMintingNFTmsg(data), }); }) .catch((err: any) => { console.log('ERROR', err); setStatus({ ...status, loading: '', error: errorMsg(err.message, 'Error minting NFT'), }); }); }) .catch((err: any) => { console.log('ERROR1', err); setStatus({ ...status, loading: '', error: errorMsg( err && err.message ? err.message : null, 'Error minting NFT' ), }); }); }
์ฐ์ฐ - NFT ๋ฐํ!! ์ ๋์ฝ ๋์ค ๋ชจ๋ ์๊ฐ!
Bacalhau๋ ๋ฐ์ดํฐ์ ๋ํ ๋ฐ๋ณต์ ์ด๊ณ ๊ฒฐ์ ์ ์ธ ์ฒ๋ฆฌ ์์ ์ ์ํํ๋ ๋ฐ ์ ํฉํฉ๋๋ค.
ETL ํ๋ก์ธ์ค
๋จธ์ ๋ฌ๋ ๋ฐ AI
IOT ๋ฐ์ดํฐ ํตํฉ
๋ค์์ ํฌํจํ ์ผ๊ด ์ฒ๋ฆฌ
๋น๋์ค ๋ฐ ์ด๋ฏธ์ง ์ฒ๋ฆฌ - ๊ด๊ณ ์์ฌ์ ์ ํฉ
Bacalhau ๋ฌธ์ ์๋ ์์ ์ผ๋ถ๋ฅผ ๋ฌ์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์ฌ๋ฌ ์๊ฐ ์์ต๋๋ค.
Bacalhau๊ฐ FEVM ์ค๋งํธ ๊ณ์ฝ์์ Bacalhau๋ฅผ ์ง์ ํธ์ถํ๊ธฐ ์ํ ํตํฉ์ ๊ตฌ์ถํ๋ ๋ฐ ๋ฐ์ ๋์ Bacalhau x FVM ํ์ ์ ๋ํ ๋ช ๊ฐ์ง ์๊ฐ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ฐ๋ฆฌ๋ ํ์ฌ ์ค๋งํธ ๊ณ์ฝ์์ ์ง์ Bacalhau๋ฅผ ์คํํ ์ ์๋ ๋ฐฉ๋ฒ์ ๊ตฌ์ถํ๊ณ ์์ต๋๋ค!!!! ์ด ํ๋ก์ ํธ๋ Project Frog / Project Lilypad๋ผ๊ณ ๋ถ๋ฆฌ๋ฉฐ FEVM ์ค๋งํธ ๊ณ์ฝ์์ Bacalhau ์์ ์ ํธ์ถํ ์ ์๋ ํตํฉ ๊ณ์ธต์ด ๋ ๊ฒ์ ๋๋ค.
๋ด์ค๋ ํฐ์ ๊ฐ์ ํ๊ฑฐ๋ ์๋ ์์ ๋ฏธ๋์ด์ ๊ฐ์ ํ์ฌ ์งํ ์ํฉ์ ๊ณ์ ์ง์ผ๋ณด์ธ์.
๋๊น์ง ์ฝ์ผ์ จ๋ค๋ฉด ์ถํ๋๋ฆฝ๋๋ค!!!
์ด๊ฒ์ด ๋์์ด ๋์๋ค๋ฉด ์ข์์, ๋๊ธ, ํ๋ก์ฐ ๋๋ ๊ณต์ ํด ์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค! <3
๋ฐ์นผ๋ผ์ฐ์ ๊ณ์ ์ฐ๋ฝํ์ธ์!
โฅ๏ธ DeveloperAlly ์ ํจ๊ป
ํ์์๊ฐ ๋์ด Alison Haire๋ฅผ ์ง์ํ์ธ์. ์ด๋ค ๊ธ์ก์ด๋ ๊ฐ์ฌํฉ๋๋ค!
ํด์๋ ธ๋ ํ์์์ ๋ํด ์์ธํ ์์๋ณด์ธ์.
์ฌ๊ธฐ์๋ ๊ฒ์๋์์ต๋๋ค.