https://learn.figment.io/protocols/solana
Solana 체인을 이용한 NFT Marketplace 프로젝트를 진행하게 되어서, Solana에 대한 스터디를 진행하게 되었다.
우선 Figment Learn 사이트에서 제공하는 Pathway를 따라가면서 익힐 예정이다.
위의 링크로 들어가서 Solana 101 을 누르면 깃허브 레포가 나온다. 해당 레포에서 설명하는대로 프로젝트를 시작하고 - 나는
Gitpod를 이용하였다
. - 그 다음은 설명해주는 대로 차근차근 단계를 밟아가면 된다.
💣💣💣💣
8단계 deploy에서 계속.. 계속 오류가 났었는데... 니모닉을 넣고 지갑에서 해도 에러가 났었는데...
로컬에서 클론을 해서 하니까 너무 잘 되는 것이다ㅠㅠㅠㅠㅠㅠㅠㅠ
여러분 꼭 gitpod 사용하지 마시고 로컬에서 클론해서 하세요....
저는 윈도우에서 wsl2 상요해서 우분투 환경에서 진행했습니다ㅠㅠㅠㅠ
https://github.com/figment-networks/learn-web3-dapp
1. Welcome to the Solana Pathway
Pathway의 각 단계는 특정 블록체인 또는 프로토콜을 사용하는 다양한 측면을 다룬다.
각 Topic에 대한 설명 후, 다음 단계로 넘어가기 전에 coding challenge가 준비되어 있다. 과제마다 힌트도 주어지는데 관련된 documentation page를 링크로 걸어둔다. 링크를 타고 넘어가서 해당 내용까지 공부하면 꽤나 시간이 많이 드는 과제가 될 것 같다. 물론 정답도 제공한다.
코드를 완성한 후에는 페이지의 오른쪽을 통해 UI와 상호작용이 잘 되는지 확인할 수 있다.
2. Setup the project
💻Requirements
https://docs.solana.com/cli/install-solana-cli-tools#macos--linux
Solana Pathway를 따라가기 위해선 Solana CLI 를 설치해야 한다. Use Solana's Install Tool(Simplest option)을 선택해서 Windows 설명을 따라서 설치했다.
설치 완료후 cmd를 열어서 solana --version 을 입력해서 설치를 확인할 수 있다.
C:\Users\SAMSUNG>solana --version
solana-cli 1.9.5 (src:39a4cc95; feat:3125401026)
이후에 업데이트가 필요한 경우에는 solana-install update 명령을 이용해서 간단하게 업데이트를 진행할 수 있다.
💣💣 혹시 여기부터 잘못되었나 싶어서.. local cmd창이 아니라 gitpod에서 bash 터미널을 만들어서 진행하였다. MAC과 동일하게 진행하면 된다.
💣💣💣💣 로컬에서 진행해야 되는 거였다... (이유는 아직도 모르겠지만..) 그래서 우분투 환경에서 설치를 진행했다.
🧩DataHub API keys
DataHub 계정과 API key 도 필요하다. 계정을 만들어서 이메일 인증 후, API key를 복사해서 .env.example 파일을 복사해서 만든 .env.local 파일의 DATAHUB_SOLANA_API_KEY 에 붙여넣으면 끝!
🤖Using a Test Validator
Test Validator 도 설치해 준다. 로컬에서 Test Validator 를 사용하려면 Solana CLI 가 설치되어 있어야 한다고 하는데 우리는 방금 설치를 했으니 cmd를 관리자 권한으로 열어서 solana-test-validator 를 입력해준다.
이때 나는 Error: failed to start validator: Failed to create ledger at test-ledger: blockstore error 가 떠서 제대로 실행이 안 된 줄 알고 찾아봤는데 bzip2 가 없어서 생기는 에러라고 한다. 그렇지만 지금 당장은 큰 상관이 없는 듯 하다.
solana config set 명령을 이용해서 특정 클러스터를 타겟할 수 있다. 한번 설정해놓으면 이후에는 solana config get 명령으로 확인할 수 있다.
C:\WINDOWS\system32>solana config set --url https://localhost:8899
Config File: C:\Users\SAMSUNG\.config\solana\cli\config.yml
RPC URL: https://localhost:8899
WebSocket URL: wss://localhost:8900/ (computed)
Keypair Path: C:\Users\SAMSUNG\.config\solana\id.json
Commitment: confirmed
C:\WINDOWS\system32>solana config get
Config File: C:\Users\SAMSUNG\.config\solana\cli\config.yml
RPC URL: https://localhost:8899
WebSocket URL: wss://localhost:8900/ (computed)
Keypair Path: C:\Users\SAMSUNG\.config\solana\id.json
Commitment: confirmed
💣💣 solana-test-validator를 gitpod 터미널에 입력하면 터미널이 새로 생성된다. 여기서 가만히 기다리고 있지 말고 bash 터미널로 돌아가자,,
-> deploy 오류와는 관계없다..
🔐 Keypair storage
후에 튜토리얼을 진행하려면 keypair 가 필요하다고 한다. 이 keypair는 웹 브라우저의 localstorage에 보관되고 Solana CLI에서 생성된 다른 keypair와 구별된다. generate key를 누르면, 화면 상당에서 카피할 수 있는 버튼이 보여진다.
그런데.. 어디서 만든다는 거지...?? 일단 패쓰...
3. Connect to Solana
이제 본격적으로 시작인 것 같다. 우리는 Solana blockchain과 @solana/web3.js 라이브러리를 이용해서 interact할 것이다.
Javascript 어플리케이션을 build 할 때 RPC API와 interface하는 것이 편리한 방법이라고 한다. Under the hood 에서 Solana의 RPC method를 구현하고 이를 Javascript 객체로 보여준다고 한다.
👉 여기서 Under the hood 가 무슨 뜻인가 했더니 말 그대로 자동차 후드 아래에서 벌어지는 일이라고 하는데, 몰라도 운전을 할 수 있지만 제대로 하려면 원리를 알아야 할 필요가 있다.. 그런 뜻이라고 한다.
RPC API : Remote Procedure Call | 웹 개발을 할 때는 RESTful API 를 주로 사용했는데, RPC API는 생소했다.
- 원격에 위치한 프로그램을 로컬에 있는 프로그램처럼 사용할 수 있다.
- 개발자는 network 통신과 관련된 작업은 신경쓰지 않아도 된다.
- 실행 인자와 실행할코드를 명확하게 해야 한다.
- IDL(Interface Definition Language)를 사용해 서버의 호출 규약을 정의한다.고유 프로그램 개발에 집중이 가능하고, 프로세스간 통신 기능을 비교적 쉽게 구현 가능, 다양한 언어를 가진 환경에서 쉽게 확장 가능하다는 장점이 있고, 단점으로는 호출 실행과 반환 시간이 보장되지 않는다고 한다.
Reference : https://mindock.github.io/network/rest-rpc/
🏋️ Challenge
https://solana-labs.github.io/solana-web3.js/classes/Connection.html
Solana에 연결하고 싶을 때마다 우리는 connection instance를 사용한다. 코드는 다음과 같다.
//...
try {
const {network} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const version = await connection.getVersion();
res.status(200).json(version['solana-core']);
}
//...
Connection class 의 constructor는 다음과 같다.
new Connection(endpoint: string, commitmentOrConfig?: Commitment | ConnectionConfig): Connection
getVersion() 함수를 통해 API version을 가져오는데, 이 함수는 Promise 객체를 return 하므로 await 를 붙여야 한다.
👉 await 참고자료: https://elvanov.com/2597
4. Create an account
대다수의 Web3 프로토콜과 마찬가지로, Solana의 Transaction도 Account 사이에서 일어난다. Account를 만들기 위해선 public key와 secret key로 이루어진 keypair를 만들어야 한다. (public key: or address, used to identify and lookup an account / secret key used to sign transactions)
🏋️ Challenge
[https://solana-labs.github.io/solana-web3.js/classes/Keypair.html]
[https://solana-labs.github.io/solana-web3.js/classes/PublicKey.html]
const keypair = new Keypair();
const address = keypair.publicKey.toString();
const secret = JSON.stringify(Array.from(keypair.secretKey));
//...
//Solution
const keypair = Keypair.generate();
const address = keypair?.publicKey.toString();
const secret = JSON.stringify(Array.from(keypair.secretKey));
나는 위와 같이 풀었고.. 코드는 잘 실행이 되었는데 Solution의 답은 나와 조금 달랐다.
일단 Keypair를 만들 때 constructor를 사용하지 않고 Keypair.generate() function을 사용했다. - 전체코드를 보니 import Keypair가 되어 있더라 - 그리고 keypair에서 public key를 추출할 때 nullish coalescing operater와 optional chaining operator인 '?' 를 사용했다.. undefined된 값을 클라이언트에게 리턴하지 않게 하기 위함이라고 한다.
👉 optional chaining operator 참고자료: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Optional_chaining
secret key는 array format으로 저장되어 있기 때문에 Array.from 을 사용한다. 이를 클라이언트에게 사용가능한 포맷으로 보내려면 JSON.stringify 를 사용해야 한다.
👉 JSON.stringify() 메서드는 JavaScript 값이나 객체를 JSON 문자열로 변환 https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
👉 Array.from() 메서드는 유사 배열 객체(array-like object)나 반복 가능한 객체(iterable object)를 얕게 복사해 새로운Array 객체를 만듭니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from
Generate Keypair 버튼을 누를 때마다 랜덤으로 키페어가 생성되는데, 주소의 영역이 매우 넓기 때문에 두 개의 동일한 주소가 생성될 확률은 매~우 적다고 한다.
5. Fund the account with SOL
일부 프로토콜의 경우, 네트워크마다(테스트넷, 메인넷) 토큰 이름이 다르다. 예를 들어 Plkadot의 경우, mainnet token은 DOT이고 testnet token은 WND이다. Solana의 경우, 어느 네트워크에 있든지 항상 토큰은 SOL 이라고 불린다.
그리고 당연한 얘기지만 devnet에서 무료로 얻는 토큰은 Solana mainnet에서 사용할 수 없으니 너무 흥분하지 말라고 한다..ㅎ
🪂 Airdropping
우리는 Account에 자금을 제공하기 위해 Airdrop(토큰이 마법처럼 하늘에서 지갑으로 떨어지는 것) 이라고 하는 일을 할 것이다. 클러스터는 우리가 전송을 테스트하고 block explorer에서 transaction 세부 정보를 볼 수 있도록 일부 SOL을 제공해준다.
1 SOL = 1,000,000,000 lamports | Leslie Lamport를 기려서 만든 것.
🏋️ Challenge
https://solana-labs.github.io/solana-web3.js/classes/PublicKey.html
https://solana-labs.github.io/solana-web3.js/classes/Connection.html#requestairdrop
try {
const {network, address} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const publicKey = new PublicKey(address);
const hash = await connection.requestAirdrop(publicKey, LAMPORTS_PER_SOL);
await connection.confirmTransaction(hash);
res.status(200).json(hash);
}
string 형식의 address를 이용해서 PublicKey를 만들었고, 이렇게 만든 publicKey를 requestAirdrop의 인자로 1 SOL을 나타내는 LAMPORTS_PER_SOL constant와 함께 전달했다.
그리고 transaction hash를 confirmTransaction 메소드에 인자로 전달함으로써 이 transaction을 검증했다.
그러면 이제 transaction의 hash를 클라이언트에게 JSON 형태로 return한다.
🧐 Anatomy of an Explorer page
Solana Explorer를 이용해서 Transaction의 세부 정보를 확인할 때
- You can click on the Cluster name in the top right corner to choose which Solana Cluster to view.
- The Overview panel displays information such as the transaction signature, which block it was included in and what the fee amount for the transaction was.
- The Account Input(s) panel displays the accounts involved in the transaction, including the change in their SOL balance and details like which account paid the transaction fee, and the account responsible for signing the transaction.
- The Instruction(s) panel displays which Program instructions were used in the transaction. Most of the time, this will be the Transfer instruction.
- The Program Log contains logging output from the execution of the Program. Log output can be useful for developers to assist in debugging their programs.
참고자료 : [https://docs.solana.com/terminology]
6. Get the balance
전송하기 전에, account에 balance가 얼마나 있는지 check해서 transfer를 할 수 있을 만큼 충분한 SOL이 있는지 확인할 필요가 있다. getBalance() function은 publicKey를 input으로 가지고 해당 publicKey에 연결된 balance를 리턴해줄 것이다.
🏋️ Challenge
[https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getBalance]
try {
const {network, address} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const publicKey = new PublicKey(address);
const balance = await connection.getBalance(publicKey);
if (balance === 0 || balance === undefined) {
throw new Error('Account not funded');
}
res.status(200).json(balance);
}
이전 실습에서 했던 것처럼 string 형태의 address를 가지고 publicKey를 만든 후, connection.getBalance 의 인자로 만든 publicKey를 전달해서 balance를 가져온다.
잔액은 LAMPORTS로 표기된다고 한다.
7. Transfer some SOL
일부 값을 다른 계정으로 전송하려면 서명된 transaction을 생성하여 클러스터로 보내야 한다. 이 작업을 수행하는 방법을 이해한다면 Solana API의 다른 부분과 interact 할 수 있는 견고한 기반을 갖게 된다.
transaction이 클러스터에 submit 되면 Solana runtime은 transaction에 포함된 각 명령을 순서대로 원자적으로 처리하는 프로그램을 실행한다. 즉, 어떤 이유로든 명령 중 하나라도 실패하면 전체 transaction이 되돌려진다.
🏋️ Challenge
[https://solana-labs.github.io/solana-web3.js/modules.html#sendAndConfirmTransaction]
[https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#add]
[https://docs.solana.com/developing/programming-model/transactions]
const instructions = SystemProgram.transfer({
fromPubkey,
toPubkey,
lamports,
});
const signers = [
{
publicKey: fromPubkey,
secretKey,
},
];
const transaction = new Transaction().add(instructions);
const hash = await sendAndConfirmTransaction(connection, transaction, signers);
res.status(200).json(hash);
우리는 transfer를 위한 instructions을 만들고, sender와 receiver와 amount를 제공했다.
또한 우리는 transaction을 보내는 account에 해당하는 signer의 signers array가 필요하다. 이것은 signer의 publicKey와 secretKey를 포함하는데, secretKey는 transaction을 sign하는 데 사용된다.
그리고 새로운 Transaction 객체를 만들고 instruction을 add 한다.
sendAndConfirmTransaction 을 사용해서 signed transaction 을 보내고 confirm한다.
마지막으로, transaction hash를 JSON 형태로 클라이언트에게 보낸다.
🧐 Anatomy of an Explorer page
- The Account Overview panel displays information about the account, including its address, balance, and if there is a Program deployed at that address.
- The History tab displays the Transaction history for the selected account, which is a list of previous transactions that account has been involved in.
- The Tokens tab displays information regarding any tokens held by the account.
8. Deploy a program
A program is to Solana what a smart contract is to other protocols.
프로그램이 배포되면, 모든 앱은 program instruction이 포함된 trnasaction을 Solana cluster에 전송하여 프로그램과 interact할 수 있다.
Solana program 참고자료 : [https://docs.solana.com/developing/on-chain-programs/overview]
🧐 Smart contract review
우리가 살펴볼 프로그램은 단순히 호출될 때마다 숫자를 증가하는 간단한 프로그램이다. 분석해보자..
// contracts/solana/program/src/lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
Rust에서 use declaration은 다른 코드에 대한 편리한 단축키(?) 역할을 하는데, 여기서는 borsh crate의 직렬화(serialize)와 역직렬화(de-serialize) function이 수행된다.. borsh는 Binary Object Representation Serializer for Hashing 을 나타낸다.
crate는 배포 및 컴파일을 함께 할 수 있는 소스 코드 모음이다. Cargo, Crates, basic project structure 에 대해 더 알아보고 싶다면 아래 링크 참조..
👉 참고자료:
[https://doc.rust-lang.org/reference/items/use-declarations.html]
[https://borsh.io/]
[https://learning-rust.github.io/docs/a4.cargo,crates_and_basic_project_structure.html]
또한, 우리는 solana program crate를 use 한다.
- next AccountInfo와 AccountInfo의 구조를 return하는 함수
- The entrypoint (진입점) macro and related entrypoint::ProgramResult
- The msg macro, for low-impact logging on the blockchain
- program_error::ProgramError 은 온체인 프로그램이 program-specific error types 을 구현하고 Solana runtime에서 반환된 오류 타입을 볼 수 있도록 한다. program-specific error는 u32 integer로 표시되거나 직렬화되는 모든 유형의 정수일 수 있다.
- pubkey::Pubkey struct
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
pub counter: u32,
}
다음으로, 우리는 derive macro를 이용하여 GreetingAccount 구조체를 wrap 하는 데 필요한 모든 boilerplate code를 생성한다. 이는 컴파일 시간동안 일어난다, Rust macro는 다루기에 다소 큰 주제이지만 이해하려고 노력할 가치가 있다. 지금은 이것이 compile time에 삽입되는 boilerplate code의 shortcut(바로가기) 라는 것만 알아두자.
boilerplate code : 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드
[https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros]
구조체 선언 자체는 간단하다. pub 키워드를 사용하여 구조체를 공개적으로 access 할 수 있도록 선언한다. - 즉, 다른 프로그램과 함수에서 사용할 수 있다. - struct 키워드는 단일 필드가 있는 GreetingAccount라는 구조체를 정의하고 있음을 컴파일러에 알리는 역할을 한다. 이 구조체에는 부호가 없는 32bit 정수인 u32 유형의 counter가 있다. 즉, counter는 4,294,967,295 보다 클 수 없다.
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Hello World Rust program entrypoint");
let accounts_iter = &mut accounts.iter();
let account = next_account_info(accounts_iter)?;
다음으로, 우리는 entry point를 선언한다. - the process_instruction function
process_instruction entry point의 리턴값은 ProgramResult이다. Result는 std crate에서 왔으며 possibility of error를 표현하는 데 사용된다.
디버깅을 위해 네트워크의 계산 비용 측면에서 엄청나게 많은 println!()을 사용하는 대신 msg!() macro를 사용하여 프로그램 로그에 메시지를 프린트 할 수 있다.
Debugging : [https://docs.solana.com/developing/on-chain-programs/debugging]
msg!() macro : [https://docs.rs/solana-program/1.7.3/solana_program/macro.msg.html]
Rust의 let 키워드는 value를 variable(변수)에 바인딩한다. iterator를 사용하여 accounts를 반복함으로써 accounts_iter는 계정의 각 값에 대한 변경 가능한 reference를 가져온다.
그러면 next_account_info(accounts_iter)? 는 다음 AccountInfo 또는 NotEnoughAccountKeys 에러를 반환한다.
코드의 마지막에 있는 ? 는 error propagation을 위한 Rust의 shortcut expression이다.
iterator : [https://doc.rust-lang.org/book/ch13-02-iterators.html]
mutable reference : [https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references]
shortcut expression : [https://doc.rust-lang.org/std/result/#the-question-mark-operator-]
error propagation : [https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors]
if account.owner != program_id {
msg!("Greeted account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}
account owner가 prmission을 가지고 있는지 security check를 수행한다. 만약 account.owner 의 public key가 program_id와 일치하지 않는다면 에러를 리턴한다.
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
msg!("Greeted {} time(s)!", greeting_account.counter);
Ok(())
마침내..! existing account data를 "borrow"한 good stuff를 얻게 되고 (?) counter 값을 1 증가하고 다시 storage에 쓴다.
- GreetingAccount 구조체는 오직 하나의 field만 가진다 - counter. 이것을 수정하려면 & borrow 연산자를 사용하여 account.data에 대한 참조를 빌려야 한다.
- BorshDeserialize의 try_from_slice() 함수는 account.data를 변경 가능하게 참조하고 deserialize한다.
- borrow() 함수는 Rust core library에서 제공되며, wrapped value를 변경할 수 없도록 borrow하기 위해 존재한다..(?)
종합하면, 이것은 account data를 borrow해서 이를 deserialize하고 에러가 발생하면 에러를 반환하는 함수에 전달한다는 의미이다. '?' 가 error propagation을 위한 것이라는 걸 떠올려보자..
다음으로, +=를 이용하여 counter 값을 1씩 증가시킨다.
BorshSerialize의 serialize() 함수를 사용하면 새 counter 값이 올바른 형식으로 Solana로 다시 전송된다. 이 메커니즘은 std::io crate의 Write trait이다..
그 후, 우리는 msg!() macro를 사용하여 카운트가 몇 번 증가했는지 프로그램 로그에 표시할 수 있다.
💻 Set up the Solana CLI
Rust도 js도 잘 모르는 상태여서 지금 약간 멘붕이 오기 시작했지만 마음을 다잡아보자...
Install Rust and Solana CLI
지금까지 우리는 Solana의 JS API를 사용하여 blockchain과 interact 했다. Solana 프로그램을 배포하기 위해 다른 Solana 개발자 도구인 CLI를 사용한다. Solana CLI를 설치하고 터미널을 통해 사용해보자..
Gitpod를 사용하는 경우에는 Rust toolchain이 이미 설치되어 있다고 한다.. 그러나 Solana CLI는 설치해야 한다고..
Rust는 Window도 설명해주면서 Solana CLI는 Window에서 설치하는 방법 설명 안해준다.. 링크로 이동해서 직접 보고 설치하자.
💣💣💣 위에서도 언급했지만, 로컬에서 클론해서 wsl2 우분투 환경에서 진행했기 때문에 Rust 도 설치를 진행해주었다.
Set up Solana CLI
프로그램을 배포하기 전에 Solana cluster를 구성하고, 계정을 만들고, 에어드랍을 요청하고, 모든 것이 제대로 작동하는지 확인해야 한다.
먼저 CLI config URL을 devnet cluster로 세팅한다.
solana config set --url https://api.devnet.solana.com
다음으로, CLI를 사용해서 새로운 keypair를 만들 것이다. /learn-web3-dapp/ 에서 실행해야 한다. - 니모닉도 혹시 몰라서 넣었다.
mkdir solana-wallet
solana-keygen new --outfile solana-wallet/keypair.json
그리고 airdrop도.. 해주자..
solana airdrop 1 $(solana-keygen pubkey solana-wallet/keypair.json)
여기서 Error: airdrop request failed. This can happen when the rate limit is reached. 가 나와서 당황했는데.. 조금 시간 지난 후에 다시 하니 된다(??)
solana config get
solana account $(solana-keygen pubkey solana-wallet/keypair.json)
이렇게 configuration 모두 verify 하고 1 SOL이 account에 fund 된 것도 확인할 수 있다.. 휴...
🧩 Deploy a Solana program
우리가 배포할 프로그램은 계정에서 greeting instruction을 보낸 횟수를 추적한다. 이것은 storage가 Solana에서 어떻게 작동하는 지 알 수 있는 효과적인 예제이다.
🧱 Building the program
첫번째로 할 일은 Rust 프로그램을 compile 하는 것이다. 이를 위해 package.json에 정의된 사용자 정의 스크립트를 사용할 것이다. 스크립트를 실행하고 터미널(프로젝트 루트 디렉토리에서)에서 다음 명령을 실행해서 프로그램을 빌드한다.
yarn run solana:build:program
성공하면 helloworld.so 라는 컴파일된 프로그램의 경로를 사용하여 배포 명령을 실행하는 instruction을 볼 수 있다. 이것이 실행되는 동안 우리는 이 목적을 위해 생성한 keypair를 지정하고 싶다.
.so 확장자는 Solana를 위미하지 않고 shared object를 의미한다.
⛓ Deploying the program
이제 프로그램을 devnet cluster에 배포할 것이다. CLI는 solana deploy를 위한 간단한 인터페이스를 제공한다.
solana deploy -v --keypair solana-wallet/keypair.json dist/solana/program/helloworld.so
-v Verbose 플래그는 선택사항이지만, default signer keypair에 대한 경로 및 RPC URL과 같은 정보와 예상 commitment level을 표시해준다. 프로세스가 완료되면 프로그램 id가 표시된다.
💣아.. 에러가 났다..ㅠㅠ
Deployment failures will print an error message specifying the seed phrase needed to recover the generated intermediate buffer's keypair:
이런 에러인데 [https://docs.solana.com/cli/deploy-a-program] 를 보니 keypair를 recover 해야 한다고 한다.. recover를 했지만 또 다시 에러가 발생한다... keypair를 다시 만들어보고 해봐도 똑같은 에러가 난다.. 나는.. 여기까지인 것 같다.... 😇
💣💣💣
로컬에서 클론을 해서 진행하면 해결되는 문제였다.. 이렇게 간단히.. gitpod에서는 뭐가 문제였던 걸까ㅠㅠ
성공하면 CLI가 배포된 계약의 programId 를 출력한다.
🏋️ Challenge
https://solana-labs.github.io/solana-web3.js/classes/Connection.html#getAccountInfo
https://solana-labs.github.io/solana-web3.js/modules.html#AccountInfo
다음 단계로 넘어가기 전에, 프로그램이 제대로 배포되었는지 확인할 필요가 있다. 이를 위해서는 프로그램의 programId가 필요하다.
try {
const {network, programId} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const publicKey = new PublicKey(programId);
const programInfo = await connection.getAccountInfo(publicKey);
if (programInfo === null) {
if (fs.existsSync(PROGRAM_SO_PATH)) {
throw new Error(
'Program needs to be deployed with `solana deploy`',
);
} else {
throw new Error('Program needs to be built and deployed');
}
} else if (!programInfo.executable) {
throw new Error(`Program is not executable`);
}
res.status(200).json(true);
}
- programId(string formatted address) 를 이용해서 새로운 PublicKey 객체를 만든다.
- connection.getAccountInfo 메소드를 호출해서 이 주소가 사용가능한지 체크한다. 만약 accountInfo가 없다면 이 주소에 연결된 계정이 없을 것이고, 이는 프로그램이 아직 deployed 되지 않았다는 걸 의미한다.
- 그리고 account's executable property가 true인지 확인한다. true라면 해당 계정 안에 로드된 프로그램이 있다는 것이다.
- 마지막으로 client에게 true 값을 JSON 형식으로 보낸다.
이렇게 우리의 프로그램을 Solana's devnet cluster에 배포 후 잘 배포가 되었는지 확인하는 과정까지 마쳤다. 이제 Solana 클러스터에 데이터를 저장하기 위해 프로그램이 소유한 계정을 생성해야 한다.
9. Create storage for the program
Solana program은 stateless하기 때문에, 작동하고 있는 동안에 값을 저장하지 않는다. 그렇다면 어떻게 호출된 count를 저장하고 있을까?
👉 데이터를 저장하려면 다른 계정에 의존해야 한다. 그래서 counter 정보를 저장하기 위한 새 계정인 Greetinger 계정(owned by our program)을 만들 것이다.
🏋️ Challenge
https://solana-labs.github.io/solana-web3.js/classes/PublicKey.html#createWithSeed
https://solana-labs.github.io/solana-web3.js/classes/SystemProgram.html#createAccountWithSeed
const greetedPubkey = await PublicKey.createWithSeed(
payer.publicKey,
GREETING_SEED,
programId,
);
const lamports = await connection.getMinimumBalanceForRentExemption(
GREETING_SIZE,
);
const transaction = new Transaction().add(
SystemProgram.createAccountWithSeed({
fromPubkey: payer.publicKey,
basePubkey: payer.publicKey,
seed: GREETING_SEED,
newAccountPubkey: greetedPubkey,
lamports,
space: GREETING_SIZE,
programId,
}),
);
const hash = await sendAndConfirmTransaction(connection, transaction, [payer]);
- 우리는 트랜잭션의 payer, random seed, programId 이렇게 세 값으로부터 PublicKey를 얻는다.
- 다음으로 계정을 생성하기 위해 SystemProgram.createAccountWithSeed 를 호출한다.
- fromPubkey: 새로 생성된 계정으로 lamports를 전달할 계정이다.
- basePubkey: 새로 생성된 계정의 주소를 얻기 위해 사용되는 Base public key이다. newAccountPubkey를 만들 때 사용한 baes key와 동일해야 한다.
- seed: 새로 생성된 계정의 주소를 얻기 위해 사용된다. GREETING_SEED에 정의되어 있다.
- newAccountPubKey: 새로 생성된 계정의 Public key이다. PublicKey.createWithSeed()로 미리 계산되어야 한다.
- lamports: 생성된 계정으로 보낼 lamports의 양이다. GREETING_SIZE를 기준으로 rent exemption(?) 을 위한 최소 잔액을 계산했다.
- space: 생성된 계정을 할당할 byte 공간이다.
- programId: 프로그램의 Public key이다. 생성된 계정의 owner를 할당하기 위해서다.
- 마지막으로 sendAndConfirmTransaction 을 통해 트랜젝션을 보내고 confirmation을 기다린다. 여기서 쓰이는 [payer] 가 두번째 단계에서 생성한 계정이다.
rent exemption: https://docs.solana.com/developing/programming-model/accounts#rent-exemption
SystemProgram: https://docs.solana.com/developing/runtime-facilities/programs#system-program
이제 프로그램의 데이터를 저장하는 전용 계정을 만들었다. 이제 데이터를 다뤄보자..
10. Get data from the program
데이터는 계정에 buffer로 저장된다. 데이터에 접근하기 위해, 먼저 이 데이터 덩어리를 잘 정의된 구조로 unpack해야 한다. 아래의 코드는 TypeScript 프로그램이다. 이 코드를 사용하면 greeter의 buffer를 TypeScript class로 deserializing(역직렬화) 한다.
// The state of a greeting account managed by the hello world program
class GreetingAccount {
counter = 0;
constructor(fields: {counter: number} | undefined = undefined) {
if (fields) {
this.counter = fields.counter;
}
}
}
// Borsh schema definition for greeting accounts
const GreetingSchema = new Map([
[GreetingAccount, {kind: 'struct', fields: [['counter', 'u32']]}],
]);
🏋️ Challenge
if (accountInfo === null) {
throw new Error('Error: cannot find the greeted account');
}
const greeting = borsh.deserialize(
GreetingSchema,
GreetingAccount,
accountInfo.data,
);
res.status(200).json(greeting.counter);
- deserialize() 함수는 schema, class type, buffer 를 input으로 가진다.
- Schema와 Account의 key(여기서 key는 public이나 private key가 아니라 map 구조체의 키를 말한다.) , 그리고 binary data는 greeter에 저장된다.
- 마지막으로 greeting의 counter 속성을 불러서 JSON 값으로 client에게 넘겨주기만 하면 된다.
단순히 greeting counter 값을 가져오는 것만으로는 충분하지 않다. 우리는 우리 프로그램에 greeting을 보내고 싶다.
11. Send data to the program
먼저 우리는 greeter에 저장된 data를 수정해야 한다. 그렇게 하면 블록체인의 상태가 변경되므로 트랜잭션을 생성해야 한다.
🏋️ Challenge
https://solana-labs.github.io/solana-web3.js/classes/TransactionInstruction.html
https://solana-labs.github.io/solana-web3.js/modules.html#sendAndConfirmTransaction
const instruction = new TransactionInstruction({
keys: [{pubkey: greeterPublicKey, isSigner: false, isWritable: true}],
programId: programKey,
data: Buffer.alloc(0), // All instructions are hellos
});
const hash = await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[payerKeypair],
);
res.status(200).json(hash);
- 먼저, 새로운 TransactionInstruction 클래스 객체를 생성한다.
- greeter의 public key와 함께, isWritable flag를 true로 바꾼다.
- programKey: programId 또는 호출하려는 프로그램의 주소
- data: 우리가 전달하려는 데이터. 이 경우에는 우리가 보낼 수 있는 instruction이 오직 한 종류뿐이며 Buffer.alloc(0)은 배열의 0 인덱스를 참조하는 것과 같다. 여러 명령이 있는 경우 이 값을 변경한다.
- 그리고 sendAndConfirmTransaction 을 한다. [payerKeypair]는 두 번째 단계에서 만든 계정이다.
클러스터 연결부터 프로그램 배포 및 interacting에 이르기까지 Solana 사용의 모든 기본 사항을 다루었다! 마지막 단계는 간단한 요약 및 추가 학습 리소스에 대한 링크이다..
12. Pathway complete
Quick recap of what we covered:
- 🔌 Connecting to a Solana cluster using the web3.js library
- 🏦 Generating a new keypair, then funding the resulting address with an airdrop
- 💸 Transferring tokens between accounts
- ⛓ Deploying and interacting with a Solana program, written in RustQ
🧐 Keep learning with these resources:
'블록체인🔗 > Solana' 카테고리의 다른 글
Create a Solana NFT marketplace and mint NFTs using Metaplex (0) | 2022.02.03 |
---|