NFT 컬렉션 만들기 단계별 가이드
👋 소개
대체불가능 토큰(NFT)은 디지털 아트와 수집품 세계에서 가장 뜨거운 주제 중 하나가 되었습니다. NFT는 블록체인 기술을 사용하여 소유권과 진위성을 검증하는 고유한 디지털 자산입니다. NFT는 창작자와 수집가들이 디지털 아트, 음악, 비디오 및 기타 디지털 콘텐츠를 수익화하고 거래할 수 있는 새로운 가능성을 열었습니다. 최근 몇 년간 NFT 시장이 급성장하여 일부 유명 작품은 수백만 달러에 거래되고 있습니다. 이 글에서는 TON에서 단계별로 NFT 컬렉션을 만들어보겠습니다.
이 튜토리얼이 끝나면 만들게 될 아름다운 오리 컬렉션입니다:
🦄 배울 내용
- TON에서 NFT 컬렉션을 발행합니다.
- TON의 NFT 작동 방식을 이해합니다.
- NFT를 판매합니다.
- 메타데이터를 pinata.cloud에 업로드합니다.
💡 전제 조건
최소 2 TON이 있는 테스트넷 지갑이 필요합니다. @testgiver_ton_bot에서 테스트넷 코인을 받을 수 있습니다.
:::info[Tonkeeper 지갑의 테스트넷 버전을 여는 방법?] Tonkeeper의 테스트넷을 열려면 설정으로 가서 하단의 Tonkeeper 로고를 5번 클릭하세요. 그런 다음 "mainnet" 대신 "testnet"을 선택하세요. :::
IPFS 스토리지 시스템으로 Pinata를 사용할 것이므로 pinata.cloud에 계정을 만들고 api_key와 api_secret을 받아야 합니다. 공식 Pinata 문서 튜토리얼이 도움이 될 수 있습니다. API 토큰을 받았다면, 여기서 계속하시죠!
💎 TON의 NFT란 무엇인가요?
튜토리얼의 메인 파트를 시작하기 전에 TON의 NFT가 일반적으로 어떻게 작동하는지 이해해야 합니다. 의외로 TON의 NFT 구현이 업계의 다른 블록체인과 비교하여 어떻게 독특한지 이해하기 위해 Ethereum(ETH)의 NFT 작동 방식부터 설명하겠습니다.
ETH의 NFT 구현
ETH의 NFT 구현은 매우 단순합니다 - 컬렉션의 메인 컨트랙트 하나가 있고, 이 컨트랙트는 해당 컬렉션의 NFT 데이터를 저장하는 간단한 해시맵을 가지고 있습니다. 이 컬렉션과 관련된 모든 요청(사용자가 NFT를 전송하거나 판매하려는 경우 등)은 특별히 이 단일 컬렉션 컨트랙트로 보내집니다.
TON에서 이러한 구현의 발생 가능한 문제점
TON의 NFT 표준은 이러한 구현의 문제점을 완벽하게 설명합니다:
-
예측할 수 없는 가스 소비. TON에서는 딕셔너리 작업의 가스 소비가 정확한 키 집합에 따라 달라집니다. 또한 TON은 비동기 블록체인입니다. 이는 스마트 컨트랙트에 메시지를 보내면 다른 사용자의 메시지가 얼마나 많이 당신의 메시지보다 먼저 스마트 컨트랙 트에 도달할지 모른다는 의미입니다. 따라서 당신의 메시지가 스마트 컨트랙트에 도달할 때 딕셔너리의 크기가 어떨지 알 수 없습니다. 이는 단순한 지갑 -> NFT 스마트 컨트랙트 상호작용에서는 괜찮지만, 지갑 -> NFT 스마트 컨트랙트 -> 경매 -> NFT 스마트 컨트랙트와 같은 스마트 컨트랙트 체인에서는 받아들일 수 없습니다. 가스 소비를 예측할 수 없다면, NFT 스마트 컨트랙트에서 소유자가 변경되었지만 경매 작업을 위한 Toncoin이 충분하지 않은 상황이 발생할 수 있습니다. 딕셔너리가 없는 스마트 컨트랙트를 사용하면 가스 소비를 결정적으로 만들 수 있습니다.
-
확장이 안 됨(병목현상이 됨). TON의 확장성은 샤딩 개념을 기반으로 합니다. 즉, 부하 시 네트워크가 자동으로 샤드체인으로 분할됩니다. 인기 있는 NFT의 단일 대형 스마트 컨트랙트는 이 개념과 모순됩니다. 이 경우 많은 트랜잭션이 하나의 단일 스마트 컨트랙트를 참조하게 됩니다. TON 아키텍처는 샤드된 스마트 컨트랙트(화이트페이퍼 참조)를 제공하지만, 현재는 구현되어 있지 않습니다.
TL;DR ETH 솔루션은 확장성이 없고 TON과 같은 비동기 블록체인에는 적합하지 않습니다.
TON NFT 구현
TON에서는 마스터 컨트랙트 하나가 있습니다 - 우리 컬렉션의 스마트 컨트랙트로, 메타데이터와 소유자 주소를 저장하고 가장 중요한 점은 새로운 NFT 아이템을 만들고("mint") 싶을 때 이 컬렉션 컨트랙트에 메시지를 보내기만 하면 된다는 것입니다. 이 컬렉션 컨트랙트는 우리가 제공하는 데이터를 사용하여 새로운 NFT 아이템 컨트랙트를 배포할 것입니다.
이 주제에 대해 더 자세히 알고 싶다면 TON의 NFT 처리 글을 확인하거나 NFT 표준을 읽어보세요.
⚙ 개발 환경 설정
빈 프로젝트를 만드는 것부터 시작해보겠습니다:
- 새 폴더 만들기
mkdir MintyTON
- 폴더 열기
cd MintyTON
- 프로젝트 초기화
yarn init -y
- typescript 설치
yarn add typescript @types/node -D
- TypeScript 프로젝트 초기화
tsc --init
- 이 설정을 tsconfig.json에 복사
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": ["ES2022"],
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": "src",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strict": true,
"esModuleInterop": true,
"strictPropertyInitialization": false
},
"include": ["src/**/*"]
}
package.json
에 앱을 빌드하고 시작하는 스크립트 추가
"scripts": {
"start": "tsc --skipLibCheck && node dist/app.js"
},
- 필요한 라이브러리 설치
yarn add @pinata/sdk dotenv @ton/ton @ton/crypto @ton/core buffer
.env
파일을 만들고 이 템플릿을 기반으로 자신의 데이터 추가
PINATA_API_KEY=your_api_key
PINATA_API_SECRET=your_secret_api_key
MNEMONIC=word1 word2 word3 word4
TONCENTER_API_KEY=aslfjaskdfjasasfas
@tonapibot에서 toncenter api 키를 받을 수 있으며 메인넷이나 테스트넷을 선택할 수 있습니다. MNEMONIC
변수에는 컬렉션 소유자 지갑의 24단어 시드 구문을 저장합니다.
좋습니다! 이제 프로젝트의 코드를 작성할 준비가 되었습니다.
헬퍼 함수 작성
먼저 src/utils.ts
에 openWallet
함수를 만들어 니모닉으로 지갑을 열고 publicKey/secretKey를 반환하도록 하겠습니다.
24단어(시드 구문)를 기반으로 키 쌍을 얻습니다:
import { KeyPair, mnemonicToPrivateKey } from "@ton/crypto";
import { beginCell, Cell, OpenedContract} from "@ton/core";
import { TonClient, WalletContractV4 } from "@ton/ton";
export type OpenedWallet = {
contract: OpenedContract<WalletContractV4>;
keyPair: KeyPair;
};
export async function openWallet(mnemonic: string[], testnet: boolean) {
const keyPair = await mnemonicToPrivateKey(mnemonic);
toncenter와 상호작용하기 위한 클래스 인스턴스를 만듭니다:
const toncenterBaseEndpoint: string = testnet
? "https://testnet.toncenter.com"
: "https://toncenter.com";
const client = new TonClient({
endpoint: `${toncenterBaseEndpoint}/api/v2/jsonRPC`,
apiKey: process.env.TONCENTER_API_KEY,
});
마지막으로 지갑을 엽니다:
const wallet = WalletContractV4.create({
workchain: 0,
publicKey: keyPair.publicKey,
});
const contract = client.open(wallet);
return { contract, keyPair };
}
좋습니다. 그 다음 프로젝트의 메인 엔트리포인트인 src/app.ts
를 만듭니다.
여기서는 방금 만든 openWallet
함수를 사용하고 메인 함수 init
을 호출합니다.
지금은 이 정도면 충분합니다.
import * as dotenv from "dotenv";
import { openWallet } from "./utils";
import { readdir } from "fs/promises";
dotenv.config();
async function init() {
const wallet = await openWallet(process.env.MNEMONIC!.split(" "), true);
}
void init();
마지막으로 src
디렉토리에 delay.ts
파일을 만들어 seqno
가 증가할 때까지 기다리는 함수를 만듭니다.
import { OpenedWallet } from "./utils";
export async function waitSeqno(seqno: number, wallet: OpenedWallet) {
for (let attempt = 0; attempt < 10; attempt++) {
await sleep(2000);
const seqnoAfter = await wallet.contract.getSeqno();
if (seqnoAfter == seqno + 1) break;
}
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
간단히 말해서, seqno는 지갑이 보낸 나가는 트랜잭션의 카운터입니다. seqno는 재생 공격을 방지하는 데 사용됩니다. 트랜잭션이 지갑 스마트 컨트랙트로 전송되면, 트랜잭션의 seqno 필드와 저장소 내부의 seqno를 비교합니다. 일치하면 수락되고 저장된 seqno가 1 증가합니다. 일치하지 않으면 트랜잭션이 폐기됩니다. 이것이 모든 나가는 트랜잭션 후에 잠시 기다려야 하는 이유입니다.