[solana] 개발환경 구성하기

안녕하세요 멍개입니다.

이번 시간은 솔라나를 위한 개발환경 구성 방법을 다뤄보려고 합니다.

solana는 rust 기반으로 개발환경이 구성되어 있습니다.

solana 환경을 이용하기 위해 필요한 도구는 다음과 같습니다.

☞ solana cli

☞ anchor

☞ solana scan

☞ solana explorer

☞ online IDE

☞ @solana/web.js

여기서는 솔라나의 동작 메커니즘을 자세히 다루지 않습니다. 이더리움과 완전히 다른 개념으로 동작하기 때문에 중간중간 솔라나에서 새롭게 등장한 개념을 간략하게는 소개하지만 이들은 추후 별도로 다룹니다.

● solana cli

solana cli는 이더리움의 geth와 같은 프로그램입니다. 자체적으로 블록체인 네트워크(솔라나에선 클러스터라고 부름)를 구성할 수 있습니다. 이 외에도 지갑을 생성하거나 하는 여러 유틸리티 프로그램을 제공하며 솔라나 네트워크와 RPC 통신을 할 수 있습니다.

https://docs.solanalabs.com/cli/install

solana cli는 다음과 같은 바이너리를 제공합니다.

cargo-build-bpf deps solana-bench-tps solana-gossip solana-ledger-tool solana-test-validator solang cargo-build-sbf rbpf-cli solana-dos solana-install solana-log-analyzer solana-tokens spl-token cargo-test-bpf sdk solana-faucet solana-install-init solana-net-shaper solana-validator cargo-test-sbf solana solana-genesis solana-keygen solana-stake-accounts solana-watchtower

각각의 명령어 옵션은 다음 링크에서 확인할 수 있습니다.

https://docs.solanalabs.com/cli/usage#usage

· 지갑생성

solana-keygen을 이용하면 지갑 파일을 생성할 수 있습니다. 생성된 지갑 파일은 secretkey 정보를 가지고 있습니다.

$ solana-keygen new -o account0.json

해당 명령어를 수행하면 BIP39로 생성하기를 시도합니다.

Generating a new keypair For added security, enter a BIP39 passphrase NOTE! This passphrase improves security of the recovery seed phrase NOT the keypair file itself, which is stored as insecure plain text BIP39 Passphrase (empty for none):

엔터를 눌러주면 public key와 니모닉을 출력합니다.

Wrote new keypair to account0.json ============================================================================== pubkey: 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW ============================================================================== Save this seed phrase and your BIP39 passphrase to recover your new keypair: area attract annual erode fruit flame power supreme cart piece utility predict ==============================================================================

니모닉과 BIP39를 이해하고 싶다면 다음 포스트를 참고하세요

https://blog.naver.com/pjt3591oo/221598854549

생성된 지갑의 publickey(address)를 조회하기 위해서는 다음과 같이 수행합니다.

$ solana address -k account0.json 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW

솔라나의 어카운트는 Native Account, Program Account, Program Data Account 개념이 존재합니다. 여기서는 각각의 차이를 다루지 않습니다.

· solana cli

solana cli는 솔라나 네트워크(클러스터)와 RPC 통신을 수행합니다.

https://docs.solanalabs.com/cli/usage#solana-cli

다음은 최신 slot 번호를 조회합니다.

$ solana slot --url https://api.devnet.solana.com 278340555

url로 전달한 값은 개발 서버의 솔라나 네트워크로 접속하기 위한 엔드포인트 입니다.

· 설정구성

solana cli가 동작될 때 연결될 네트워크 및 기본 지갑 주소를 설정할 수 있습니다.

설정된 정보를 변경하기 전에 어떻게 설정이 되어있는지 확인할 수 있습니다.

$ solana config get Config File: /Users/jeongtaepark/.config/solana/cli/config.yml RPC URL: http://localhost:8899 WebSocket URL: ws://localhost:8900/ (computed) Keypair Path: /Users/jeongtaepark/.config/solana/id.json Commitment: confirmed

설정된 정보는 ~/.config/solana/cli/config.yml에 정의되어 있습니다. 해당 파일은 다음과 같이 구성되어 있습니다.

--- json_rpc_url: http://localhost:8899 websocket_url: '' keypair_path: /Users/jeongtaepark/.config/solana/id.json address_labels: '11111111111111111111111111111111': System Program commitment: confirmed

websocket_url은 비어있으면 websocker_url의 도메인을 사용합니다.

keypair_path는 앞에서 생성한 지갑 경로를 넣어주면 됩니다.

참고로 commitment는 processed, confirmed, finalized가 존재합니다.

▶ cli에서 설정정보 변경

설정 파일에서 직접 수정하지 않더라도 CLI를 통해 각각 변경할 수 있습니다.

$ solana config set --url http://localhost:8899
$ solana config set --url devnet
$ solana config set --url mainnet-beta

· 로컬 테스트 클러스터 구성

solana-test-validator를 이용하여 로컬 환경에서 테스트용 클러스터를 구성할 수 있습니다.

클러스터가 구성되면 네트워크 구성을 위한 데이터가 저장될 경로를 미리 만들어줍니다.

$ mkdir solana-my-cluster $ cd solana-my-cluster/
$ solana-test-validator

복잡한 설정 없이 클러스터가 동작합니다. 이런저런 설정은 할 수 있지만 여기서는 복잡한 설정을 다루진 않습니다.

$ solana slot --url http://localhost:8899 228

config로 설정된 주소는 테스트용 클러스터가 동작될 때 500000000 SOL을 제공합니다.

$ solana account 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 Public Key: 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 Balance: 500000000 SOL Owner: 11111111111111111111111111111111 Executable: false Rent Epoch: 0

메인넷을 제외하고는 에어드랍을 통해 5SOL을 받을 수 있습니다. 참고로 devnet은 하루에 한 번 받을 수 있습니다.

$ solana airdrop 5 또는 $ solana airdrop 5 -u [URL] -k [지갑경로]

· 솔라나 토큰 전송

앞에서 생성한 주소로 5SOL을 전송하는 트랜잭션을 발생합니다.

$ solana transfer 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW 5 --allow-unfunded-recipient Signature: 2LzZvgFK2G2XYunj9HLyWdPVNwDLdWENz2X4CZvTHJdy1EUvXyAnFCkdRcxUEca56ttdPpEJNAD7HCLSCA92DLS4
$ solana account 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW Public Key: 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW Balance: 5 SOL Owner: 11111111111111111111111111111111 Executable: false Rent Epoch: 18446744073709551615

· 트랜잭션 확인

$ solana confirm -v 2LzZvgFK2G2XYunj9HLyWdPVNwDLdWENz2X4CZvTHJdy1EUvXyAnFCkdRcxUEca56ttdPpEJNAD7HCLSCA92DLS4 RPC URL: http://localhost:8899 Default Signer Path: /Users/jeongtaepark/.config/solana/id.json Commitment: confirmed Transaction executed in slot 3856: Block Time: 2024-02-10T07:27:02+09:00 Version: legacy Recent Blockhash: EKuGSLoMSYW3ENq76PLaXN9g5WBuMubK8Lw9R2z3nH3R Signature 0: 2LzZvgFK2G2XYunj9HLyWdPVNwDLdWENz2X4CZvTHJdy1EUvXyAnFCkdRcxUEca56ttdPpEJNAD7HCLSCA92DLS4 Account 0: srw- 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (fee payer) Account 1: -rw- 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW Account 2: -r-x 11111111111111111111111111111111 Instruction 0 Program: 11111111111111111111111111111111 (2) Account 0: 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (0) Account 1: 9pXCKUvCcJBzcrpVGAtfbuUhoWxzBj2vT8W74tcqUGiW (1) Transfer { lamports: 5000000000 } Status: Ok Fee:0.000005 Account 0 balance:500000005 ->499999999.999995 Account 1 balance:0 ->5 Account 2 balance:0.000000001 Compute Units Consumed: 150 Log Messages: Program 11111111111111111111111111111111 invoke [1] Program 11111111111111111111111111111111 success Finalized

여기서 주의깊게 봐야하는 항목은 Account [숫자], Insturction [숫자], Status, Log Messages, version입니다.

Account [숫자]는 해당 트랜잭션에 존재하는 Account(Address or public key)를 목록으로 가지고 Account가 필요한 곳에서는 Account 인덱스를 사용합니다.

Instruction은 [숫자]는 해당 트랜잭션에서 수행해야 하는 작업을 의미합니다. 하나의 트랜잭션은 여러개의 Instruction을 가질 수 있으며, 하나의 Instruction이라도 실패하면 해당 트랜잭션은 실패합니다. 이 부분이 유용한 부분인데, 예를들어 컨트랙트를 배포하고 초기화 하는 과정을 하나의 트랜잭션으로 처리할 수 있습니다. 컨트랙트 배포 -> 컨트랙트 초기화 -> 솔라나 전송을 하나의 트랜잭션에서 처리할 수 있습니다.

솔라나의 트랜잭션은 Legacy 버전과 0 버전이 존재합니다. 여기서는 이 둘의 차이점을 다루진 않습니다.

· RPC 통신하기

CLI로도 RPC 통신 결과를 받을 수 있지만 네트워크 통신을 직접 해보겠습니다.

$ curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d ' {"jsonrpc":"2.0","id":1, "method":"getSlot"} ' {"jsonrpc":"2.0","result":2135,"id":1}

RPC 통신은 공식문서에서 목록을 확인해 볼 수 있습니다.

https://solana.com/docs/rpc/http

● anchor

anchor는 솔라나 개발 프레임워크 입니다. 이더리움의 truffle or hardhat과 같은 프레임워크입니다.

anchor를 설치하기 위해서는 rust 설치가 필요합니다.

https://www.anchor-lang.com/docs/installation

해당 문서에 나와있는 것처럼 anchor는 avm으로 버전 관리를 수행합니다. node로 치면 n, python으로 치면 pyenv와 유사합니다.

· 프로젝트 생성

$ anchor init my-counter
$ cd my-counter $ tree . -I "node_modules" -I ".git" -I "dist" -I "__pycache__" -a -I "target" . ├── .gitignore ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── app ├── migrations │ └── deploy.ts ├── package.json ├── programs │ └── my-counter │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ └── lib.rs ├── tests │ └── my-counter.ts ├── tsconfig.json └── yarn.lock 6 directories, 13 files

컨트랙트 코드는 programs/src 아래에 lib.rs가 존재합니다. 솔라나는 스마트 컨트랙트 대신 프로그램이라고 표현합니다.

Anchor.toml에서 RPC 노드 주소, wallet을 지정할 수 있습니다.

[toolchain] [features] seeds = false skip-lint = false [programs.localnet] my_counter = "6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "/Users/jeongtaepark/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

솔라나의 프로그램 주소는 내부적으로 생성하지 않고 프로그램이 배포될 때 지정해주어야 합니다.

· 프로그램 빌드

프로그램에는 declare_id!()를 통해 프로그램 주소를 전달해야 합니다.

use anchor_lang::prelude::*; declare_id!("6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2"); #[program] pub mod my_counter { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { msg!("Hello, world!"); Ok(()) } } #[derive(Accounts)] pub struct Initialize {}

anchor build를 통해 작성된 프로그램 코드를 빌드할 수 있습니다.

$ anchor build

빌드가 성공적으로 끝나면 target/deploy와 target/idl, target/types 경로에서 프로그램 어카운트 주소 정보, 인터페이스 정의, 타입을 확인 할 수 있습니다.

· 프로그램 테스트

프로그램 테스트 코드는 tests/ 아래에 작성할 수 있습니다. 해당 경로에 작성된 테스트 코드는 anchor test로 실행 할 수 있습니다.

import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { MyCounter } from "../target/types/my_counter"; describe("my-counter", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.MyCounter as Program<MyCounter>; it("Is initialized!", async () => { // Add your test here. const tx = await program.methods.initialize().rpc(); console.log("Your transaction signature", tx); }); });
$ anchor test

참고로 test를 수행할 때 Anchor.toml의 provider를 이용합니다. cluster가 Localnet이라고 되어있는데 이는 solana-test-validator로 실행중인 클러스터를 이용하지 않고 test 수행 시 anchor가 자체적으로 클러스터를 띄운 후 테스트를 수행합니다. 해당 명령어를 수행할 땐 solana-test-validator가 실행중이라면 정상적으로 동작하지 않습니다.

· 배포

$ anchor deploy
Deploying cluster: http://localhost:8899 Upgrade authority: /Users/jeongtaepark/.config/solana/id.json Deploying program "my_counter"... Program path: /Users/jeongtaepark/Desktop/solana-contract/my-counter/target/deploy/my_counter.so... Program Id: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 Deploy success

배포된 프로그램 아이디(주소)를 조회할 수 있습니다.

$ solana account 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 Public Key: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 Balance: 0.00114144 SOL Owner: BPFLoaderUpgradeab1e11111111111111111111111 Executable: false Rent Epoch: 18446744073709551615 Length: 36 (0x24) bytes 0000: 02 00 00 00 41 3c e7 be 58 e1 62 aa e2 4b 2b 5a ....A<..X.b..K+Z 0010: ce 96 66 06 1f cc f9 1e 48 87 b1 b6 24 e4 69 bc ..f.....H...$.i. 0020: 1a 20 6b 79 . ky

· 프로그램 업그레이드

solana의 프로그램은 이더리움의 스마트컨트랙트와 다르게 업그레이드가 가능합니다.

$ anchor upgrade --program-id 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 --provider.cluster http://localhost:8899 target/deploy/my_counter.so

· 어플리케이션 연동

여기서는 react, vue와 같은것들은 사용하지 않고 타입스크립트 수준에서 해당 프로그램을 연동하여 트랜잭션을 발생해보겠습니다.

$ npm install @project-serum/anchor @solana/web3.js

이제 코드는 app 아래에서 작성합니다.

// app/test.ts import { Program, Provider } from '@project-serum/anchor' import NodeWallet from '@project-serum/anchor/dist/cjs/nodewallet' import { Connection, Keypair, PublicKey } from '@solana/web3.js' const connection = new Connection('http://localhost:8899', 'confirmed'); function createKeypairFromFile(path: string): Keypair { return Keypair.fromSecretKey( Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) ) }; const IDL = { version: "0.1.0", name: "my_counter", instructions: [ { name: "initialize", accounts: [], args: [] } ], metadata: { address: "6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2" } } const owner = createKeypairFromFile(require('os').homedir() + '/.config/solana/id.json'); const provider = new Provider( connection, (new NodeWallet(owner)), { preflightCommitment: 'confirmed' } ); const program = new Program(IDL, new PublicKey(IDL.metadata.address), provider); (async () => { const tx = await program.rpc.initialize({}) console.log(tx); })();

만약, 웹 또는 모바일 환경이라면 provider는 다른 지갑 프로그램을 연결할 수 있습니다.

npm install @solana/wallet-adapter-base \ @solana/wallet-adapter-wallets \ @solana/wallet-adapter-vue \ @solana/wallet-adapter-vue-ui
npm install @solana/wallet-adapter-base \ @solana/wallet-adapter-react \ @solana/wallet-adapter-react-ui \ @solana/wallet-adapter-wallets \ @solana/web3.js

작성된 코드를 실행하기 전에 solana에서 logs를 실행하여 생성된 트랜잭션 로그를 확인할 수 있습니다.

$ solana logs Streaming transaction logs. Confirmed commitment
$ ts-node app/test.ts
Transaction executed in slot 112: Signature: 52ZYATSQG4mfoLMjJCP4Eha6Gz5Xd7hPDfnt8jAK4MsF7FGCiSCB2K79pkGuCNVwRVyWeB7AMbUKHNtt2ZongC6v Status: Ok Log Messages: Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 200000 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success

msg!로 출력한 Hello world가 출력되는 모습을 확인할 수 있습니다.

solana confirm을 이용하여 생성된 트랜잭션 signature를 이용하여 상세하게 확인할 수 있습니다.

$ solana confirm -v 52ZYATSQG4mfoLMjJCP4Eha6Gz5Xd7hPDfnt8jAK4MsF7FGCiSCB2K79pkGuCNVwRVyWeB7AMbUKHNtt2ZongC6v RPC URL: http://localhost:8899 Default Signer Path: /Users/jeongtaepark/.config/solana/id.json Commitment: confirmed Transaction executed in slot 112: Block Time: 2024-02-10T08:58:42+09:00 Version: legacy Recent Blockhash: 2jhkSJqLRPonyk5E9GH6g9amMPU2RM2cqAnHZHqkMMBf Signature 0: 52ZYATSQG4mfoLMjJCP4Eha6Gz5Xd7hPDfnt8jAK4MsF7FGCiSCB2K79pkGuCNVwRVyWeB7AMbUKHNtt2ZongC6v Account 0: srw- 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (fee payer) Account 1: -r-x 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 Instruction 0 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (1) Data: [175, 175, 109, 31, 13, 152, 155, 237] Status: Ok Fee:0.000005 Account 0 balance:499999998.7171374 ->499999998.7171324 Account 1 balance:0.00114144 Compute Units Consumed: 323 Log Messages: Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 200000 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Finalized

▶ instruction 만들기

솔라나에서는 트랜잭션을 직접 발생하는 것은 항상 좋지 않습니다. 왜냐하면 앞에서 언급했지만 솔라나는 한 트랜잭션에 여러 인스트럭션을 포함할 수 있기 때문입니다.

import { Program, Provider } from '@project-serum/anchor' import NodeWallet from '@project-serum/anchor/dist/cjs/nodewallet' import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from '@solana/web3.js' const connection = new Connection('http://localhost:8899', 'confirmed'); function createKeypairFromFile(path: string): Keypair { return Keypair.fromSecretKey( Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) ) }; const IDL = { version: "0.1.0", name: "my_counter", instructions: [ { name: "initialize", accounts: [], args: [] } ], metadata: { address: "6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2" } } const owner = createKeypairFromFile(require('os').homedir() + '/.config/solana/id.json'); const provider = new Provider( connection, (new NodeWallet(owner)), { preflightCommitment: 'confirmed' } ); const program = new Program(IDL, new PublicKey(IDL.metadata.address), provider); (async () => { const ix0 = program.transaction.initialize({}); const ix1 = program.transaction.initialize({}); const ix2 = program.transaction.initialize({}); const tx = await sendAndConfirmTransaction( connection, new Transaction().add(ix0).add(ix1).add(ix2), // Add our instruction (you can add more than one) [owner] ); console.log(tx); })();

solana logs를 통해 생성된 트랜잭션을 확인하면 다음과 같이 Log Messages가 포함된것을 볼 수 있습니다.

Streaming transaction logs. Confirmed commitment Transaction executed in slot 1730: Signature: ugVYNPrGxvE1iYsbXNEwSUNQLWCp5VfwpwpK5SJtPMqrPaExVL5gcXapRZac9YA2n7BWH5Xsn3qtGThiiVG2NSK Status: Ok Log Messages: Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 600000 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 599677 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 599354 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success

트랜잭션 Signature을 확인하면 다음과 같이 Instruction이 목록형태로 포함된 것을 확인할 수 있습니다.

트랜잭션에는 어떤 Instruction이든 상관없이 포함될 수 있습니다.

$ solana confirm -v ugVYNPrGxvE1iYsbXNEwSUNQLWCp5VfwpwpK5SJtPMqrPaExVL5gcXapRZac9YA2n7BWH5Xsn3qtGThiiVG2NSK RPC URL: http://localhost:8899 Default Signer Path: /Users/jeongtaepark/.config/solana/id.json Commitment: confirmed Transaction executed in slot 1730: Block Time: 2024-02-10T09:11:55+09:00 Version: legacy Recent Blockhash: 2NnvdJtxBBduPSkLBFrGf3N5P9Ddw5WDhGD8kBwsrp16 Signature 0: ugVYNPrGxvE1iYsbXNEwSUNQLWCp5VfwpwpK5SJtPMqrPaExVL5gcXapRZac9YA2n7BWH5Xsn3qtGThiiVG2NSK Account 0: srw- 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (fee payer) Account 1: -r-x 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 Instruction 0 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (1) Data: [175, 175, 109, 31, 13, 152, 155, 237] Instruction 1 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (1) Data: [175, 175, 109, 31, 13, 152, 155, 237] Instruction 2 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (1) Data: [175, 175, 109, 31, 13, 152, 155, 237] Status: Ok Fee:0.000005 Account 0 balance:499999998.71712744 ->499999998.71712244 Account 1 balance:0.00114144 Compute Units Consumed: 969 Log Messages: Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 600000 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 599677 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 599354 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Finalized

다음과 같이 컨트랙트 호출과 솔라나 전송을 하나의 트랜잭션으로 만들 수 있습니다.

(async () => { const ix0 = program.transaction.initialize({}); const ix1 = program.transaction.initialize({}); const ix2 = program.transaction.initialize({}); const ix3 = SystemProgram.transfer({ fromPubkey: owner.publicKey, toPubkey: Keypair.generate().publicKey, lamports: LAMPORTS_PER_SOL / 100, }) const tx = await sendAndConfirmTransaction( connection, new Transaction().add(ix0).add(ix1).add(ix2).add(ix3), // Add our instruction (you can add more than one) [owner] ); console.log(tx); })();

실행후 생성된 트랜잭션을 조회하면 다음과 같이 4개의 instruction이 포함된 것을 확인할 수 있습니다.

$ solana confirm -v 5j9N2wAwrYgeRWnxsbwCUhdxr3K5zxMqTPx4tCqEuPXViH7AemhFT2jVdw1cQt84b69eygXmamd5PaSJFKBFP2bg RPC URL: http://localhost:8899 Default Signer Path: /Users/jeongtaepark/.config/solana/id.json Commitment: confirmed Transaction executed in slot 7259: Block Time: 2024-02-10T09:59:01+09:00 Version: legacy Recent Blockhash: m95jR9e5tsC4nBzaNxg5AVFPTxBY1cZHRuEVi8EuCZ9 Signature 0: 5j9N2wAwrYgeRWnxsbwCUhdxr3K5zxMqTPx4tCqEuPXViH7AemhFT2jVdw1cQt84b69eygXmamd5PaSJFKBFP2bg Account 0: srw- 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (fee payer) Account 1: -rw- DfBdTFQndGqEiwg61yySaEnpzKWCQMcnmBko6YWAJCQo Account 2: -r-x 11111111111111111111111111111111 Account 3: -r-x 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 Instruction 0 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (3) Data: [175, 175, 109, 31, 13, 152, 155, 237] Instruction 1 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (3) Data: [175, 175, 109, 31, 13, 152, 155, 237] Instruction 2 Program: 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 (3) Data: [175, 175, 109, 31, 13, 152, 155, 237] Instruction 3 Program: 11111111111111111111111111111111 (2) Account 0: 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (0) Account 1: DfBdTFQndGqEiwg61yySaEnpzKWCQMcnmBko6YWAJCQo (1) Transfer { lamports: 10000000 } Status: Ok Fee:0.000005 Account 0 balance:499999998.7152674 ->499999998.7052624 Account 1 balance:0 ->0.01 Account 2 balance:0.000000001 Account 3 balance:0.00114144 Compute Units Consumed: 1119 Log Messages: Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 800000 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 799677 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 invoke [1] Program log: Instruction: Initialize Program log: Hello, world! Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 consumed 323 of 799354 compute units Program 6DPtkwnhvc32CwKJ5TNMsXabxoLcD2mxaJcjaPsJofb2 success Program 11111111111111111111111111111111 invoke [1] Program 11111111111111111111111111111111 success Finalized

● solana scan

solana scan은 이더스캔과 같은 사이트입니다.

https://solscan.io/

우측 상단에서 네트워크를 선택할 수 있습니다. 하지만 아쉽게도 Custom RPC는 사용할 수 없습니다.

● solana explorer

솔라나 탐색기는 솔라나 스캐너와 유사하지만 좀 더 심플한 UI를 가집니다.

https://explorer.solana.com/

우측의 Mainnet Beta 버튼을 누르면 다른 네트워크로 연결할 수 있습니다. 솔라나 탐색기는 로컬에 구동된 클러스터로 연결이 가능합니다.

그리고 앞에서 생성된 트랜잭션을 조회하면 다음과 같이 확인할 수 있습니다.

● solana playground(웹 IDE)

솔라나도 이더리움의 리믹스처럼 웹 기반 IDE를 제공합니다.

https://beta.solpg.io/

· 프로젝트 생성

네이티브를 선택해도 되지만 Anchor를 선택합니다. Native를 선택할 경우 프로그램에서 메서드를 분기하는것이 매우 귀찮습니다. 추후에 이 둘의 차이를 다루겠지만 여기서는 일단 anchor를 선택합니다.

우리가 앞에서 작성한 코드와 유사합니다. 하지만 처음보는 메크로들이 존재합니다. #[account] 입니다. 프로그램에 의해 파생될 주소가 가지는 데이터 공간을 의미합니다. 솔라나 프로그램은 상태를 저장하지 않습니다. PDA가 상태를 저장하며 PDA(Program Data Account)는 Program Account를 참조하고 있습니다.

#[derive(Accounts)]는 프로그램에서 PDA를 접근하기 위한 어댑터 역할을 합니다. #[derive(Accounts)]로 정의된 구조체가 어떻게 정의되었느냐에 따라서 PDA를 만들거나 PDA의 상태값을 변경할 수 있습니다.

#[derive(Accounts)] pub struct Initialize<'info> { // We must specify the space in order to initialize an account. // First 8 bytes are default account discriminator, // next 8 bytes come from NewAccount.data being type u64. // (u64 = 64 bits unsigned integer = 8 bytes) #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>, }

이때 #[account(init, payer = signer, space = 8+ 8)]의init은 account를 만들라는 의미입니다. 즉 PDA가 생성됩니다. 이때 해당 어카운트의 rent 비용을 지불할 대상을 payer로 선택할 수 있습니다. 이렇게 생성된 PDA는 제너릭으로 전달된 NewAccount로 접근할 수 있습니다. NewAccount 구조의 크기는 space로 정의합니다. space는 해당 PDA의 데이터 스토리지 크기를 의미합니다. solana는 Account가 계속 유지되기 위해 rent 비용이 1epoch 단위로 지불됩니다.

데이터 스토리지에 저장되는 데이터는 #[account]로 정의된 NewAccount 구조체입니다.

#[account] pub struct NewAccount { data: u64 }

u64는 8byte를 의미합니다. PDA의 공간은 사용되는 크기 + 8을 해줍니다.

그리고 #[program]이 의미하는 것은 Program Account로 호출할 수 있는 메서드가 정의됩니다.

#[program] mod hello_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Changed data to: {}!", data); // Message will show up in the tx logs Ok(()) } }

여기에 정의된 메서드들은 #[derive(Accounts)]로 정의된 구조체를 첫 번째 인자의 타입이 되어야 합니다.

#[derive(Accounts]는 복수개 존재할 수 있습니다.

use anchor_lang::prelude::*; declare_id!("96QBNcuHuQv1x1q1feJNFDckf6yNEYHcVdkh8QFvjT3i"); #[program] pub mod counter_anchor { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter_account = &mut ctx.accounts.counter_account; counter_account.count = 0; Ok(()) } pub fn increase(ctx: Context<Increase>, increment: u64) -> Result<()> { let counter_account = &mut ctx.accounts.counter_account; let current_count = &counter_account.count; counter_account.count = if u64::MAX - current_count >= increment { current_count + increment } else { u64::MAX }; Ok(()) } pub fn decrease(ctx: Context<Decrease>, decrement: u64) -> Result<()> { let counter_account = &mut ctx.accounts.counter_account; let current_count = &counter_account.count; counter_account.count = if current_count >= &decrement { current_count - decrement } else { 0 }; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = user, space = 8 + 8)] pub counter_account: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Increase<'info> { #[account(mut)] pub counter_account: Account<'info, Counter>, } #[derive(Accounts)] pub struct Decrease<'info> { #[account(mut)] pub counter_account: Account<'info, Counter>, } #[account] pub struct Counter { pub count: u64, }

· 빌드

두 번째 탭에서 빌드 및 업그레이드를 수행할 수 있습니다.

빌드가 완료되면 다음과 같이 콘솔에 빌드가 성공되었다고 출력됩니다.

그리고 여러가지가 바뀌는데 먼저, 지갑을 연결하라고 뜹니다.

· 지갑연결

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 버전이 존재합니다.

$ solana confirm -v 64dqPqaLKWBG14mhqeY5AF2JeSP4jH2psgabmQTDt9qFy8c2yn4hSkHhuxFZdMJZSoczKeoi3GKhw8hZdDq9QgR1 RPC URL: http://localhost:8899 Default Signer Path: /Users/jeongtaepark/.config/solana/id.json Commitment: confirmed Transaction executed in slot 12110: Block Time: 2024-02-10T10:38:11+09:00 Version: legacy Recent Blockhash: 97hcsdN2KCPaXv2HeT1husx4AUTh89Dv4rEzXkvD7jb7 Signature 0: 64dqPqaLKWBG14mhqeY5AF2JeSP4jH2psgabmQTDt9qFy8c2yn4hSkHhuxFZdMJZSoczKeoi3GKhw8hZdDq9QgR1 Signature 1: 2jD6QbSDn4jxAnstDUPPkWEAyGUSHVaoeTFqyhLPvFYaffsHCiPuJegNtZYJgU1J2uY9ebEENaMZpNctWDtngsfY Account 0: srw- 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (fee payer) Account 1: srw- 8GogCQQCMQE5ZiKQE8zo9QFfBY1xDqsF7iX6KPmKWZE8 Account 2: -rw- 5n9SSuSSGFzcVVNcB6ntkumHGXH1cJRHU4gmX4oVG8Zg Account 3: -rw- 8W4GQdU1mWxeCqZXzoH2r3tQFTA2Qapj4uETeHz1CHmN Account 4: -r-x 11111111111111111111111111111111 Account 5: -r-x BPFLoaderUpgradeab1e11111111111111111111111 Account 6: -r-- SysvarC1ock11111111111111111111111111111111 Account 7: -r-- SysvarRent111111111111111111111111111111111 Instruction 0 Program: 11111111111111111111111111111111 (4) Account 0: 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (0) Account 1: 8GogCQQCMQE5ZiKQE8zo9QFfBY1xDqsF7iX6KPmKWZE8 (1) CreateAccount { lamports: 1398960, space: 36, owner: BPFLoaderUpgradeab1e11111111111111111111111 } Instruction 1 Program: BPFLoaderUpgradeab1e11111111111111111111111 (5) Account 0: 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (0) Account 1: 5n9SSuSSGFzcVVNcB6ntkumHGXH1cJRHU4gmX4oVG8Zg (2) Account 2: 8GogCQQCMQE5ZiKQE8zo9QFfBY1xDqsF7iX6KPmKWZE8 (1) Account 3: 8W4GQdU1mWxeCqZXzoH2r3tQFTA2Qapj4uETeHz1CHmN (3) Account 4: SysvarRent111111111111111111111111111111111 (7) Account 5: SysvarC1ock11111111111111111111111111111111 (6) Account 6: 11111111111111111111111111111111 (4) Account 7: 8EgkLHEgk3eFCeP3LKXkatnoXfr3mR2uN7UnZ1DyWYm6 (0) Data: [2, 0, 0, 0, 16, 32, 6, 0, 0, 0, 0, 0] Status: Ok Fee:0.00001 Account 0 balance:500000097.3061535 ->500000095.9077333 Account 1 balance:0 ->0.00139896 Account 2 balance:0 ->2.79511512 Account 3 balance:1.39810392 ->0 Account 4 balance:0.000000001 Account 5 balance:0.000000001 Account 6 balance:0.00116928 Account 7 balance:0.0010092 Compute Units Consumed: 2670 Log Messages: Program 11111111111111111111111111111111 invoke [1] Program 11111111111111111111111111111111 success Program BPFLoaderUpgradeab1e11111111111111111111111 invoke [1] Program 11111111111111111111111111111111 invoke [2] Program 11111111111111111111111111111111 success Deployed program 8GogCQQCMQE5ZiKQE8zo9QFfBY1xDqsF7iX6KPmKWZE8 Program BPFLoaderUpgradeab1e11111111111111111111111 success Finalized

· 프로그램 초기화 호출

Program Account를 배포하였으니 #[program]에 정의된 initialize를 호출합니다. 이때 #[derive(Accounts)]로 정의된 Initialize 구조체에 의해 호출이 되므로 #[account(init, payer = signer, space = 8 + 8)]로 정의된 new_account가 존재하므로 이는 새로운 Program Data Account가 생성됩니다.

playground에서는 provider와 program을 주입하기 때문에 편하게 클라이언트 코드를 작성해볼 수 있습니다.

const newAccount = anchor.web3.Keypair.generate(); console.log(`created PDA: ${newAccount.publicKey}`); try { const tx = await pg.program.methods .initialize(new anchor.BN(11)) .accounts({ newAccount: newAccount.publicKey }) .signers([newAccount]) .rpc(); let latestBlockhash = await pg.connection.getLatestBlockhash("finalized"); await pg.connection.confirmTransaction({ signature: tx, blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, }); console.log(tx); } catch (err) { console.log(err); }

run 버튼을 누르면 작성된 client.ts가 실행됩니다.

여기서 중요한 점은 .accounts는 생성되는 PDA(Program Data Account)의 주소를 전달해야 합니다.

const tx = await pg.program.methods .initialize(new anchor.BN(11)) .accounts({ newAccount: newAccount.publicKey }) .signers([newAccount]) .rpc();

initialize는 #[program]정의된 메서드 입니다. 이때 전달되는 값은 정의된 메서드의 첫 번째 인자 이후의 파라미터 수 만큼 전달해야 합니다.

$ solana account 8X4bVeJfFHrgmyytxSELJ651RS7JotGhjeayU765Xa75 Public Key: 8X4bVeJfFHrgmyytxSELJ651RS7JotGhjeayU765Xa75 Balance: 0.00100224 SOL Owner: BGqeUqypEq5fsvLL7u8oSLwsmAjgkFqhminhts2oJq2G Executable: false Rent Epoch: 18446744073709551615 Length: 16 (0x10) bytes 0000: b0 5f 04 76 5b b1 7d e8 0b 00 00 00 00 00 00 00 ._.v[.}.........

PDA의 중요한 점은 Owner입니다. Program Account가 할당됩니다. 즉 해당 PDA는 Owner(Program Account)에 의해 생성되어졌음을 의미합니다.

앞에서 우리는 16 바이트 크기의 space를 할당해주었습니다. 따라서 해당 어카운트의 크기인 Length는 16 바이트가 됩니다.

#[derive(Accounts)] pub struct Initialize<'info> { // We must specify the space in order to initialize an account. // First 8 bytes are default account discriminator, // next 8 bytes come from NewAccount.data being type u64. // (u64 = 64 bits unsigned integer = 8 bytes) #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>, }

· PDA 조회

이제 해당 어카운트에 저장된 데이터를 읽어와보겠습니다. 이때 PDA의 account 정보를 저장합니다.

try { const data = await pg.program.account.newAccount.fetch( new anchor.web3.PublicKey('8X4bVeJfFHrgmyytxSELJ651RS7JotGhjeayU765Xa75') ); console.log(data.data.toString()) } catch (err) { console.log(err); }

0
0
이 글을 페이스북으로 퍼가기 이 글을 트위터로 퍼가기 이 글을 카카오스토리로 퍼가기 이 글을 밴드로 퍼가기

기타

번호 제목 글쓴이 날짜 조회수
14 [solana] 개발환경 구성하기 관리자 07-22 202
13 DNS 서버 취약점 보안 조치 관리자 10-13 616
12 백엔드 서비스인 포켓베이스 관리자 04-22 1,108
11 공휴일 API 관리자 08-26 905
10 유사도별로 이미지를 구성 관리자 05-23 727
9 vscode +1 관리자 04-26 1,117
8 VSCode 확장 SFTP Error: Handshake failed: 관리자 01-07 1,138
7 요약 관리자 01-04 779
6 마크다운 사용법 관리자 12-22 699
5 깃허브 요약 2 관리자 12-22 789
4 깃허브 요약 1 관리자 12-22 591
3 mysterydata 관리자 10-22 796
2 자바그룹웨어 +1 관리자 10-22 1,192
1 오픈소스 그룹웨어 관리자 10-21 1,434