솔라나, Token Extension(Token-2022) 토큰 배포하기(feat. Node.js)
March 01, 2025
배경
EVM 계열에서만 컨트랙트를 발행하고 작업하다가 솔라나(Solana)를 접할 계기가 생겼다. 대부분 인터넷에서 한글로 치면 나오는 컨텐츠들은 작년에 나온 Token Extension 까지는 다루지 않는 것 같았다. 이번 글에서는 Token Extension을 이용하여 솔라나 체인 위에서 사용 가능한 토큰을 만들어 사용해보는 시간을 가지려고 한다.
리눅스 기반 터미널을 사용할 줄 아는 프로그래머를 산정하고 글을 작성하고 있으니 참고하자.
SLP 토큰 모델
솔라나가 특징적으로 EVM과 다른 게 많다. 간단히 두 체인 간의 큰 차이점은 다음과 같이 정리된다. 솔라나는 자산 보유 데이터를 프로그램(컨트랙트)와 별개로 분리해서 저장한다. EVM의 ERC-20은 컨트랙트 자체에서 자산 보유 데이터를 저장한다.
EVM에서도 로직과 데이터를 따로 저장해서 Upgradable Contract를 구현하고는 하는데 기본적으로는 하나의 컨트랙트에서 데이터와 로직을 한꺼번에 다룬다.
이를테면 EVM은 자산 보유 데이터를 컨트랙트 내에서 다음과 같이 글로벌 변수로 선언해서 사용한다.
contract ERC20 {
...
mapping(address=>uint256) balances;
...
}
하지만 솔라나는 각자가 보유하고 있는 자산에 대한 데이터는 각자가 새로 생성한 계정에 보관한다. 해당 계정은 본인의 지갑 주소와 토큰의 주소를 조합해서 고유하게 이끌어 낼 수 있다. 솔라나의 계정 관리 방식은 solidity의 map이나 동적 배열처럼 복잡한 데이터 구조를 포기하고 단순하게 데이터 레이아웃을 짤 수 있다는 것이 장점이라고 한다. 처음 이 방식을 접하면 복잡해보이기는 하지만 실제로 익숙해지면 오히려 이쪽이 편해보이는 것도 같다.
솔라나에서 발행되는 모든 토큰은 한 프로그램의 로직을 통해서 작동한다. 이전 Token 프로그램을 이용해 발행된 토큰은 해당 프로그램을, 이 글에서 설명하는 Token Extension 프로그램을 이용해 발행된 토큰은 해당 프로그램을 이용한다. 로직을 관리하는 해당 프로그램을 통해 연산이 되고 실제로 데이터는 아까 설명했던 계정들에 반영되는 메커니즘이다.
[토큰 계정 관계도, 출처: 솔라나 공식문서]
솔라나 토큰 프로그램을 통해 최소한으로 필요한 계정을 정리하자면 다음과 같다.
- 내 지갑
- Mint Account(프로그램을 통해서 생성된다.)
- Token Account(프로그램을 통해서 생성된다.)
Mint Account
- 전체 발행 개수와 토큰의 메타데이터를 참조할 수 있는 정보를 저장한다.
- 계정을 통해 누군가에게 새로운 토큰을 발행할 수 있다.
- 프로그램을 통해서 특정 계정을 초기화하는 형태로 생성할 수 있다.
Token Account
- 관련된 Mint Account에서 정의하는 토큰에 대해서 이 계정의 Owner가 보유하는 개수를 저장한다.
- 초기화된 특정 Mint Account가 존재해야 생성할 수 있다.
토큰 발행 프로세스 정리
솔라나에서 Token-Extension으로 토큰을 발행하는 프로세스는 다음과 같이 정리된다.
- Mint Account가 될 계정 공간 대여
- Token Extension 추가
- Associated Token Account(ATA) 공간 대여
- ATA에 일정 토큰량 민트(Mint)
토큰 발행 실습
환경 설정
우선 본인이 토큰 발행을 진행한 환경은 'Windows 10'과 'macOS'이다.
모두 Node.js(v20)가 설치되어 있었다.
Windows 같은 경우는 wsl
을 통해 진행했다.
아래에서 설명하는 내용들은 macOS
를 위주로 설명될 것이다.
먼저 Solana가 지원하는 개발툴을 설치해야 한다. 과정 중 Rust 등의 의존적인 패키지들도 같이 설치해야 할 것이다.
솔라나 공식문서: Getting Started 위 페이지에서 Solana CLI(Command Line Interface)를 설치해야 진행이 가능하다.
solana --version
// log: solana-cli x.xx.xx
위 명령어로 제대로 설치되었는지 확인한다.
solana config get
# Config File: /Users/{whoami}/.config/solana/cli/config.yml
# RPC URL: https://api.devnet.solana.com
# WebSocket URL: wss://api.devnet.solana.com/ (computed)
# Keypair Path: /Users/{whoami}/.config/solana/id.json
# Commitment: confirmed
본인은 위와 같이 세팅되는 데, 지금 눈여겨봐야 할 사항은 RPC URL
이다.
솔라나 같은 경우는 블록체인 네트워크를 'mainnet-beta', 'testnet', 'devnet', 이렇게 3개를 지원해준다.
이번의 경우는 'devnet'으로 진행할 것이니, 혹시 위의 예시와 RPC URL
이 다르게 기재된다면 다음의 명령어를 실행시키자.
solana config set --url devnet
(디렉터리 Typescript 세팅)
현재 있는 곳에서 작업을 진행할 디렉터리를 만들고 옮겨가는 것이 관심사 분리를 위해 좋을 것 같다.
mkdir solana-token
cd solana-token
Solana는 토큰 발행을 위해서 필요한 작업을 간편하게 구현할 수 있게 라이브러리를 지원해준다. 이 라이브러리는 여러 프로그래밍 언어로 지원해주고 있는데, 본인은 Node.js 와 TypeScript로 진행했다.
현재 작업이 진행되고 있는 디렉터리를 TypeScript 환경으로 만든다.
# Typescript 세팅
yarn init -y
yarn add -D typescript ts-node @types/node
yarn tsc --init
// tsconfig.json 에 다음 항목 추가
{
"resolveJsonModule": true,
}
라이브러리를 설치해준다.
// 솔라나 라이브러리 의존성 추가
yarn add @solana/spl-token @solana/spl-token-metadata @solana/web3.js
(토큰메타데이터 준비)
블록체인 상에 올라가는 토큰에 대한 정보는 실제로 토큰 이름과, 토큰 단위(symbol)가 있다.
부가적인 데이터는 uri 필드를 두고 참조할 수 있는 uri 값을 저장하게 하고 있다.
보통 uri를 통해 참조하는 데이터는 json
의 형식을 갖는다.
해당 파일은 다음과 같은 예시를 갖는다.
{
"name": "TEST Token",
"symbol": "TT",
"description": "Token for test",
"image": "https://www.412ock.dev/logo.png"
}
위의 예시에서 확인되는 필드들은 거의 만연하게 사용하는 필드들이기 때문에 지켜주는 게 좋다. 특히 image 필드는 해당 토큰의 로고 이미지를 다운로드하거나 참조할 수 있는 또다른 uri 혹은 url을 갖는다. 보통 image는 1000 x 1000 이하 크기의 이미지를 갖는다고 한다.
솔라나 생태계에서 이 토큰을 인지하는 과정은 먼저 블록체인 상의 데이터를 확인하고, uri를 참조해서 확인되는 json파일을 한 번 더 확인하고, 거기서 image 값을 찾아 로고 이미지를 띄우는 형식으로 작동한다. 그렇기 때문에 json파일이나 json 파일 내 image의 url은 인터넷을 통해 어디서나 접근 가능하게 할 필요가 있다.
본인은 이전에 파일과 이미지를 공유하기 위해 IPFS를 이용했었다. AWS의 S3를 이용하는 방법도 있다.
지갑 계정 Keypair 생성 및 에어드랍
우선, 토큰을 만드는 데 필요한 가스비를 내기위해 지갑 계정을 만들어야 한다.
solana-keygen grind --starts-with me:1
위의 명렁어는 me
로 시작하는 지갑 주소 1 개를 만들어 달라는 명령어다.
ls
# log: {base58 인코딩 문자열}.json
지금 있는 디렉터리에서 me로 시작하는 json파일이 생성된 것이 확인될 것이다. 해당 파일의 내용은 개인키이다. 해당 파일의 이름이 솔라나 지갑 주소다.
이 지갑을 Solana CLI가 주로 사용하는 계정으로 변경하기 위해 다음의 명령어를 실행한다.
solana config set ./me{문자열}.json
solana address
# log: {me로 시작하는 지갑 주소}
생성된 파일의 이름과 solana address
를 실행했을 때 출력되는 문자열이 같으면 잘 적용된 것이다.
다음의 명령어를 실행해서 가스비를 내기 위한 테스트용 Solana를 받고 지갑 잔고를 확인한다.
# 에어드랍 명령어
solana airdrop 2
# 잔고 확인
solana balance
# log: 2 SOL
Mint Account Keypair 생성
Mint Account
Keypair를 생성한다.
지갑 계정을 생성하는 방법과 동일하다.
다만 이 계정을 따로 설정에 등록하거나 에어드랍을 받을 필요가 없고, 단지 배포되는 주소로 사용하기 위함 뿐이다.
참고로 이 Mint Account
에 토큰에 대한 권한을 갖게하면 사용도가 높아질 것 같다.
이번에는 토큰에 대한 권한은 모두 지갑 계정이 갖게 하려고 한다.
# 토큰 발행 명령어
solana-keygen grind --starts-with tk:1
# 파일 확인
ls
# log: tk{base58 인코딩 문자열}.json
Mint Account 공간 대여 및 초기화
Mint Account
는 현재 비어있는 상태이다.
이 계정에 토큰을 발행하기 위해서는 데이터를 저장하기 위한 공간을 대여하고 초기화시켜야 한다.
Solana의 Token-2022(Token Extension)는 필요한 기능이 있다면 이미 구현되어 있는 Extension을 추가해 놓을 수 있다. 이를테면 토큰에 대한 정보, Metadata를 저장하는 기능을 필요로 하게 될텐데 이것도 Extension 중 하나다. 이전에는 Metaplex에서 제공하는 라이브러리를 통해 Metadata만 저장하는 계정을 따로 발행해서 Mint Account와 연결하는 방식으로 구현되었다.
이번 실습에서는 MintCloseAuthority와 Metadata Extension만 사용하려고 한다.
MintCloseAuthority는 임의의 Mint Account
토큰 발행량이 0일 때, 해당 Mint Account
에 저장된 정보를 삭제하고 공간 대여에 지불했던 비용을 반환하는 기능을 한다.
Metadata는 토큰의 이름, 심볼, 그리고 토큰을 설명할 수 있는 파일의 uri를 저장하는 기능을 한다.
Mint Account
초기화 작업을 구현한 코드는 다음에서 확인할 수 있다.
// payerKeypair, tokenKeypair는 위에서 생성했던 지갑계정 keypair와 Mint Account Keypair 대응된다.
import payerKeypair from "./{지갑계정 Keypair}.json";
import tokenKeypair from "./{Mint Account Keypair}.json";
const metadata: TokenMetadata = {
mint: tokenKeypair.publicKey,
name: 'TEST_SOL',
symbol: "TTS",
uri: "https://www.412ock.dev/token.json",
additionalMetadata: [],
};
/* Set Connection */
const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');
/* Custom */
const mint = tokenKeypair.publicKey;
const mintAuthority = payerKeypair.publicKey;
const freezeAuthority = payerKeypair.publicKey;
const closeAuthority = payerKeypair.publicKey;
/* a */
const extensions = [ExtensionType.MintCloseAuthority, ExtensionType.MetadataPointer];
const mintLen = getMintLen(extensions);
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen);
/****************/
/* b */
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payerKeypair.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeMintCloseAuthorityInstruction(
mint,
closeAuthority,
TOKEN_2022_PROGRAM_ID,
),
createInitializeMetadataPointerInstruction(
mint,
payerKeypair.publicKey,
mint,
TOKEN_2022_PROGRAM_ID,
),
createInitializeMintInstruction(
mint,
9,
mintAuthority,
freezeAuthority,
TOKEN_2022_PROGRAM_ID,
),
createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID,
mint: mint,
metadata: mint,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
mintAuthority: payerKeypair.publicKey,
updateAuthority: payerKeypair.publicKey,
})
)
/****************/
/* Send Transaction */
await sendAndConfirmTransaction(connection, transaction, [payerKeypair, tokenKeypair]);
/****************/
'Set Connection'에서 확인할 수 있는 코드는, 토큰을 발행하기 위한 트랜잭션을 전송하기 위해 이용할 RPC Endpoint
를 설정하게 되어 있다.
간단하게 솔라나 체인에 연결되어 있는 클러스터(노드)라고 생각하면 된다.
위 구현은 솔라나가 기본적으로 제공하는 Endpoint를 이용하고 있다.
참고로 이전에 환경 설정할 때 확인할 수 있는 Devent URL과 Endpoint는 동일한 주소다.
'Custom'에서는 'Mint Account'와 Extension에서 제공하는 권한들을 누가 소유할 것인지 설정할 수 있다. 간단하게 모든 기능의 권한은 가스 지불에 사용되는 지갑 계정이 갖게 세팅했다.
'a'에서 계정의 공간대여를 위한 필요한 공간 크기와 이를 위한 최소 필요 솔라나 개수를 계산하고 있다. 메타데이터는 토큰의 이름이나 심볼의 데이터는 가변적이기 때문에 여기에서 데이터 크기를 계산해줘야 한다.
본인 같은 경우는 mintLen과 metadataLen의 값을 더한 값이
Mint Account
의 초기화하는 함수SystemProgram.createAccount
의 space 항목에 들어가야 한다고 생각했다. 근데 결과적으로 mintLen 값만 넣어야 트랜잭션이 제출될 수 있다.
'b'에서 이번 토큰 발행에 필요한 트랜잭션을 생성하고 있다.
'createInitialize~Instruction'은 모두 트랜잭션을 만들어주는 함수다.
createInitializeMintInstruction
이 사실 Mint Account
를 초기화하는 가장 기본적인 함수다.
들어가는 순서대로, 'Mint Account의 주소', '소수점 자리수', '민트 기능 권한 소유 주소', 'Freeze 기능 권한 소유 주소', 'Token Program ID'를 파라미터로 넣어야 한다.
트랜잭션 생성 함수를 실행하기 위해 call 할 때, programId가 들어가야 하는 부분에 모두 TOKEN_2022_PROGRAM_ID
를 넣어주고 있다.
기존 토큰 Program을 이용하면 해당 부분이 생략이 가능하다.
'Send Transaction' 구역의 코드를 통해 만들어진 트랜잭션을 모두 한꺼번에 제출한다.
별 문제 없으면 Mint Account
의 초기화가 완료될 것이다.
확인 작업은 solscan에서 하면 된다. 이전에 생성했던 Mint Account Keypair의 파일명(json 확장자 제거해야 한다)을 스캐너의 검색창에 입력한다. 거기서 다음과 같이 토큰이 만들어진 것이 확인되면 된다.
[민트 계정 상태 확인]
[민트 계정 데이터 확인]
토큰 정보가 제대로 기재되어있지 않은 것 같으면, 토큰 정보에 기재된 uri나 url들이 잘 작동하는지 확인할 필요가 있다.
ATA 계정 생성 및 토큰 민트
Associated Token Account(이하 ATA)를 새로 생성하고 초기화하며, 해당 Account에 토큰을 민팅한다. 혹자는 뭔가 이상함을 감지했을 수 있다. 왜 지갑 계정에 발행하지 않고, 새로 생성한 ATA에 토큰을 민팅을 하는 것일까?
앞서 설명했던 SLP 토큰 모델이 비로소 여기에서 설명된다. 솔라나에서 지향하는 계정 모델이 동적으로 추가 가능한 map, array 타입을 지원하지 않고 계정을 새로 생성하는 방식으로 관리를 한다. 그래서 토큰 모델은 임의의 토큰을 보유하고 있다는 정보를 저장하기 위한 새로운 계정을 필요로 한다. 여기서 새로 생성하는 계정이 ATA다.
아래에 예시코드가 있다.
// ATA 생성 및 초기화
import { createAssociatedTokenAccount } from '@solana/spl-token';
const ata = await createAssociatedTokenAccount(
connection,
payerKeypair,
tokenKeypair.publicKey,
payerKeypair.publicKey,
undefined,
TOKEN_2022_PROGRAM_ID,
);
createAssociatedTokenAccount
함수를 call 할 때 들어가는 파라미터는 순서대로, 'RPC Endpoint 연결정보', '가스 지불 계정', 'Mint Account 주소', '민팅할 계정 주소' 이다.
ATA 주소는 'Mint Account'와 '토큰을 소유할 계정의 주소'로 이끌어낼 수 있다.
참고로 ATA는 여러 개를 만들어 관리할 수 있다.
기본적으로 Token Program에서는 기본적으로 이끌어낼 수 있는 ATA를 인식하여 해당 계정에서 토큰의 잔고를 빼고 더하는 작업을 한다.
위 예시 코드는 기본 ATA를 생성하는 코드다.
// 토큰 민팅
import { mintTo } from '@solana/spl-token';
const amount = BigInt("100000000000000000000") // 소수점 9 자리, 백 억
await mintTo(
connection,
payerKeypair,
tokenKeypair.publicKey,
ata, // Token Account 가 입력되어야 한다.
payerKeypair,
amount,
[],
undefined,
TOKEN_2022_PROGRAM_ID
)
위 예시 코드는 토큰을 민팅하는 코드다.
여기서 주의할 것은 솔라나가 Rust 기반으로 작동한다는 것이다.
솔라나 상에서 token의 개수를 다루는 데, u64
데이터 타입을 사용한다.
최대값이 '18446744073709551615'다.
만약 이보다 큰 수를 민팅하려고 하면 에러가 나거나, overflow가 일어나서 이상한 값의 개수로 민팅이 될 수 있다.
위 코드가 제대로 실행되었다면, 지갑 계정으로 토큰이 amount
만큼 발행된다.
[공급 개수 확인]
민팅이 잘되었는지 확인하려면 역시 Solscan을 확인하는 것이 좋다. Solscan에서 토큰 주소를 검색해서 나타난 화면을 확인해보면 총 공급량이 변경이 된 것을 확인할 수 있다.
마무리
토큰을 발행하는 전 과정이 마쳤다. 여기서 생성된 토큰은 또다른 솔라나 라이브러리의 도움이나 솔라나 CLI를 통해 타인에게 전송하거나 수신할 수 있다. 발행된 토큰과의 상호작용은 다음에 기회가 되면 다루도록 하겠다.
위에 설명하기 위해서 제시한 예시코드들은 정리가 되어있고 구동 가능한 상태로 확인할 수 있게 페이지를 공유하도록 하겠다.
위 과정을 따라 토큰을 발행하다가 문제가 생기는 사항을 댓글로 남겨준다면 답변을 할 수 있도록 노력하겠다.