[solana] 개발환경 구성하기
안녕하세요 멍개입니다.
이번 시간은 솔라나를 위한 개발환경 구성 방법을 다뤄보려고 합니다.
solana는 rust 기반으로 개발환경이 구성되어 있습니다.
solana 환경을 이용하기 위해 필요한 도구는 다음과 같습니다.
☞ solana cli
☞ anchor
☞ solana scan
☞ solana explorer
☞ online IDE
☞ @solana/web.js
여기서는 솔라나의 동작 메커니즘을 자세히 다루지 않습니다. 이더리움과 완전히 다른 개념으로 동작하기 때문에 중간중간 솔라나에서 새롭게 등장한 개념을 간략하게는 소개하지만 이들은 추후 별도로 다룹니다.
● solana cli
solana cli는 이더리움의 geth와 같은 프로그램입니다. 자체적으로 블록체인 네트워크(솔라나에선 클러스터라고 부름)를 구성할 수 있습니다. 이 외에도 지갑을 생성하거나 하는 여러 유틸리티 프로그램을 제공하며 솔라나 네트워크와 RPC 통신을 할 수 있습니다.
solana cli는 다음과 같은 바이너리를 제공합니다.
각각의 명령어 옵션은 다음 링크에서 확인할 수 있습니다.
· 지갑생성
solana-keygen을 이용하면 지갑 파일을 생성할 수 있습니다. 생성된 지갑 파일은 secretkey 정보를 가지고 있습니다.
해당 명령어를 수행하면 BIP39로 생성하기를 시도합니다.
엔터를 눌러주면 public key와 니모닉을 출력합니다.
니모닉과 BIP39를 이해하고 싶다면 다음 포스트를 참고하세요
생성된 지갑의 publickey(address)를 조회하기 위해서는 다음과 같이 수행합니다.
솔라나의 어카운트는 Native Account, Program Account, Program Data Account 개념이 존재합니다. 여기서는 각각의 차이를 다루지 않습니다.
· solana cli
solana cli는 솔라나 네트워크(클러스터)와 RPC 통신을 수행합니다.
다음은 최신 slot 번호를 조회합니다.
url로 전달한 값은 개발 서버의 솔라나 네트워크로 접속하기 위한 엔드포인트 입니다.
· 설정구성
solana cli가 동작될 때 연결될 네트워크 및 기본 지갑 주소를 설정할 수 있습니다.
설정된 정보를 변경하기 전에 어떻게 설정이 되어있는지 확인할 수 있습니다.
설정된 정보는 ~/.config/solana/cli/config.yml에 정의되어 있습니다. 해당 파일은 다음과 같이 구성되어 있습니다.
websocket_url은 비어있으면 websocker_url의 도메인을 사용합니다.
keypair_path는 앞에서 생성한 지갑 경로를 넣어주면 됩니다.
참고로 commitment는 processed, confirmed, finalized가 존재합니다.
▶ cli에서 설정정보 변경
설정 파일에서 직접 수정하지 않더라도 CLI를 통해 각각 변경할 수 있습니다.
· 로컬 테스트 클러스터 구성
solana-test-validator를 이용하여 로컬 환경에서 테스트용 클러스터를 구성할 수 있습니다.
클러스터가 구성되면 네트워크 구성을 위한 데이터가 저장될 경로를 미리 만들어줍니다.
복잡한 설정 없이 클러스터가 동작합니다. 이런저런 설정은 할 수 있지만 여기서는 복잡한 설정을 다루진 않습니다.
config로 설정된 주소는 테스트용 클러스터가 동작될 때 500000000 SOL을 제공합니다.
메인넷을 제외하고는 에어드랍을 통해 5SOL을 받을 수 있습니다. 참고로 devnet은 하루에 한 번 받을 수 있습니다.
· 솔라나 토큰 전송
앞에서 생성한 주소로 5SOL을 전송하는 트랜잭션을 발생합니다.
· 트랜잭션 확인
여기서 주의깊게 봐야하는 항목은 Account [숫자], Insturction [숫자], Status, Log Messages, version입니다.
Account [숫자]는 해당 트랜잭션에 존재하는 Account(Address or public key)를 목록으로 가지고 Account가 필요한 곳에서는 Account 인덱스를 사용합니다.
Instruction은 [숫자]는 해당 트랜잭션에서 수행해야 하는 작업을 의미합니다. 하나의 트랜잭션은 여러개의 Instruction을 가질 수 있으며, 하나의 Instruction이라도 실패하면 해당 트랜잭션은 실패합니다. 이 부분이 유용한 부분인데, 예를들어 컨트랙트를 배포하고 초기화 하는 과정을 하나의 트랜잭션으로 처리할 수 있습니다. 컨트랙트 배포 -> 컨트랙트 초기화 -> 솔라나 전송을 하나의 트랜잭션에서 처리할 수 있습니다.
솔라나의 트랜잭션은 Legacy 버전과 0 버전이 존재합니다. 여기서는 이 둘의 차이점을 다루진 않습니다.
· RPC 통신하기
CLI로도 RPC 통신 결과를 받을 수 있지만 네트워크 통신을 직접 해보겠습니다.
RPC 통신은 공식문서에서 목록을 확인해 볼 수 있습니다.
● anchor
anchor는 솔라나 개발 프레임워크 입니다. 이더리움의 truffle or hardhat과 같은 프레임워크입니다.
anchor를 설치하기 위해서는 rust 설치가 필요합니다.
해당 문서에 나와있는 것처럼 anchor는 avm으로 버전 관리를 수행합니다. node로 치면 n, python으로 치면 pyenv와 유사합니다.
· 프로젝트 생성
컨트랙트 코드는 programs/src 아래에 lib.rs가 존재합니다. 솔라나는 스마트 컨트랙트 대신 프로그램이라고 표현합니다.
Anchor.toml에서 RPC 노드 주소, wallet을 지정할 수 있습니다.
솔라나의 프로그램 주소는 내부적으로 생성하지 않고 프로그램이 배포될 때 지정해주어야 합니다.
· 프로그램 빌드
프로그램에는 declare_id!()를 통해 프로그램 주소를 전달해야 합니다.
anchor build를 통해 작성된 프로그램 코드를 빌드할 수 있습니다.
빌드가 성공적으로 끝나면 target/deploy와 target/idl, target/types 경로에서 프로그램 어카운트 주소 정보, 인터페이스 정의, 타입을 확인 할 수 있습니다.
· 프로그램 테스트
프로그램 테스트 코드는 tests/ 아래에 작성할 수 있습니다. 해당 경로에 작성된 테스트 코드는 anchor test로 실행 할 수 있습니다.
참고로 test를 수행할 때 Anchor.toml의 provider를 이용합니다. cluster가 Localnet이라고 되어있는데 이는 solana-test-validator로 실행중인 클러스터를 이용하지 않고 test 수행 시 anchor가 자체적으로 클러스터를 띄운 후 테스트를 수행합니다. 해당 명령어를 수행할 땐 solana-test-validator가 실행중이라면 정상적으로 동작하지 않습니다.
· 배포
배포된 프로그램 아이디(주소)를 조회할 수 있습니다.
· 프로그램 업그레이드
solana의 프로그램은 이더리움의 스마트컨트랙트와 다르게 업그레이드가 가능합니다.
· 어플리케이션 연동
여기서는 react, vue와 같은것들은 사용하지 않고 타입스크립트 수준에서 해당 프로그램을 연동하여 트랜잭션을 발생해보겠습니다.
이제 코드는 app 아래에서 작성합니다.
만약, 웹 또는 모바일 환경이라면 provider는 다른 지갑 프로그램을 연결할 수 있습니다.
작성된 코드를 실행하기 전에 solana에서 logs를 실행하여 생성된 트랜잭션 로그를 확인할 수 있습니다.
msg!로 출력한 Hello world가 출력되는 모습을 확인할 수 있습니다.
solana confirm을 이용하여 생성된 트랜잭션 signature를 이용하여 상세하게 확인할 수 있습니다.
▶ instruction 만들기
솔라나에서는 트랜잭션을 직접 발생하는 것은 항상 좋지 않습니다. 왜냐하면 앞에서 언급했지만 솔라나는 한 트랜잭션에 여러 인스트럭션을 포함할 수 있기 때문입니다.
solana logs를 통해 생성된 트랜잭션을 확인하면 다음과 같이 Log Messages가 포함된것을 볼 수 있습니다.
트랜잭션 Signature을 확인하면 다음과 같이 Instruction이 목록형태로 포함된 것을 확인할 수 있습니다.
트랜잭션에는 어떤 Instruction이든 상관없이 포함될 수 있습니다.
다음과 같이 컨트랙트 호출과 솔라나 전송을 하나의 트랜잭션으로 만들 수 있습니다.
실행후 생성된 트랜잭션을 조회하면 다음과 같이 4개의 instruction이 포함된 것을 확인할 수 있습니다.
우측 상단에서 네트워크를 선택할 수 있습니다. 하지만 아쉽게도 Custom RPC는 사용할 수 없습니다.
● solana explorer
솔라나 탐색기는 솔라나 스캐너와 유사하지만 좀 더 심플한 UI를 가집니다.
우측의 Mainnet Beta 버튼을 누르면 다른 네트워크로 연결할 수 있습니다. 솔라나 탐색기는 로컬에 구동된 클러스터로 연결이 가능합니다.
그리고 앞에서 생성된 트랜잭션을 조회하면 다음과 같이 확인할 수 있습니다.
· 프로젝트 생성
네이티브를 선택해도 되지만 Anchor를 선택합니다. Native를 선택할 경우 프로그램에서 메서드를 분기하는것이 매우 귀찮습니다. 추후에 이 둘의 차이를 다루겠지만 여기서는 일단 anchor를 선택합니다.
우리가 앞에서 작성한 코드와 유사합니다. 하지만 처음보는 메크로들이 존재합니다. #[account] 입니다. 프로그램에 의해 파생될 주소가 가지는 데이터 공간을 의미합니다. 솔라나 프로그램은 상태를 저장하지 않습니다. PDA가 상태를 저장하며 PDA(Program Data Account)는 Program Account를 참조하고 있습니다.
#[derive(Accounts)]는 프로그램에서 PDA를 접근하기 위한 어댑터 역할을 합니다. #[derive(Accounts)]로 정의된 구조체가 어떻게 정의되었느냐에 따라서 PDA를 만들거나 PDA의 상태값을 변경할 수 있습니다.
이때 #[account(init, payer = signer, space = 8+ 8)]의init은 account를 만들라는 의미입니다. 즉 PDA가 생성됩니다. 이때 해당 어카운트의 rent 비용을 지불할 대상을 payer로 선택할 수 있습니다. 이렇게 생성된 PDA는 제너릭으로 전달된 NewAccount로 접근할 수 있습니다. NewAccount 구조의 크기는 space로 정의합니다. space는 해당 PDA의 데이터 스토리지 크기를 의미합니다. solana는 Account가 계속 유지되기 위해 rent 비용이 1epoch 단위로 지불됩니다.
데이터 스토리지에 저장되는 데이터는 #[account]로 정의된 NewAccount 구조체입니다.
u64는 8byte를 의미합니다. PDA의 공간은 사용되는 크기 + 8을 해줍니다.
그리고 #[program]이 의미하는 것은 Program Account로 호출할 수 있는 메서드가 정의됩니다.
여기에 정의된 메서드들은 #[derive(Accounts)]로 정의된 구조체를 첫 번째 인자의 타입이 되어야 합니다.
#[derive(Accounts]는 복수개 존재할 수 있습니다.
· 빌드
두 번째 탭에서 빌드 및 업그레이드를 수행할 수 있습니다.
빌드가 완료되면 다음과 같이 콘솔에 빌드가 성공되었다고 출력됩니다.
그리고 여러가지가 바뀌는데 먼저, 지갑을 연결하라고 뜹니다.
· 지갑연결
Connected to Playground Wallet을 통해 지갑을 연결할 수 있습니다.
Import Keypair를 통해 solana-keygen으로 생성된 json 파일의 지갑을 선택합니다.
그리고 Program ID가 111111 에서 특정 public key가 생성되어 정해집니다.
· 새로운 Program ID 생성(Program Account)
만약 Program Account를 새롭게 다시 배포를 하려면 New 버튼을 누르면 됩니다. 새로운 Program Account를 생성하여 새롭게 배포할 수 있습니다.
· 네트워크 선택
지갑이 연결되면 네트워크를 선택할 수 있습니다. 기본으로는 devnet에 연결되어 있습니다. 좌측 하단을 살펴보면 어떤 네트워크에 연결되어 있는지 확인할 수 있습니다.
· 배포
deploy 버튼을 누르면 프로그램이 배포됩니다.
· 프로그램 주소 조회
solana explorer에서 배포된 Program Account(ID)를 조회해보겠습니다.
transaction history의 해당 트랜잭션을 확인해봅니다. 그러면 어떤 과정으로 Program Account가 배포되었는지 알 수 있습니다.
또한 트랜잭션을 조회해도 동일하게 어떤 과정으로 배포되었는지 알 수 있습니다.
참고로 account inputs은 해당 트랜잭션에서 사용되는 모든 account입니다. 이것 때문에 Legacy, 0 버전이 존재합니다.
· 프로그램 초기화 호출
Program Account를 배포하였으니 #[program]에 정의된 initialize를 호출합니다. 이때 #[derive(Accounts)]로 정의된 Initialize 구조체에 의해 호출이 되므로 #[account(init, payer = signer, space = 8 + 8)]로 정의된 new_account가 존재하므로 이는 새로운 Program Data Account가 생성됩니다.
playground에서는 provider와 program을 주입하기 때문에 편하게 클라이언트 코드를 작성해볼 수 있습니다.
run 버튼을 누르면 작성된 client.ts가 실행됩니다.
여기서 중요한 점은 .accounts는 생성되는 PDA(Program Data Account)의 주소를 전달해야 합니다.
initialize는 #[program]정의된 메서드 입니다. 이때 전달되는 값은 정의된 메서드의 첫 번째 인자 이후의 파라미터 수 만큼 전달해야 합니다.
PDA의 중요한 점은 Owner입니다. Program Account가 할당됩니다. 즉 해당 PDA는 Owner(Program Account)에 의해 생성되어졌음을 의미합니다.
앞에서 우리는 16 바이트 크기의 space를 할당해주었습니다. 따라서 해당 어카운트의 크기인 Length는 16 바이트가 됩니다.
· PDA 조회
이제 해당 어카운트에 저장된 데이터를 읽어와보겠습니다. 이때 PDA의 account 정보를 저장합니다.