
https://github.com/Ackee-Blockchain/school-of-solana
라는 프로그램을 만들 것이다.
함수는 3개.
pub fn create(ctx: Context<Create>, name: String) -> ProgramResult {
let bank = &mut ctx.accounts.bank;
bank.name = name;
bank.balance = 0;
bank.owner = *ctx.accounts.user.key;
Ok(())
}
#[derive(Accounts)]
pub struct Create<'info> {
#[account(init, payer=user, space=5000, seeds=[b"bankaccount", user.key().as_ref()], bump)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
#[account]
pub struct Bank {
pub name: String,
pub balance: u64,
pub owner: Pubkey,
}
}
이제 좀 구조가 보이기 시작한다.
account와 관계없는 데이터인 Name은 따로 받고,
함수 인자 구조체 위에는 [#derive]를 붙이고,
일반 데이터 구조체는 붙이지 않는다.
bank라는 계정에는 추가적으로 Bank라는 구조체를 붙인다.
즉 얘는 데이터 저장을 위한 Account. PDA로 만들 것이다. 랜덤이 아니라.
프로그램에서 Init을 해줘야 하기에 위에 init, seeds를 붙였다.
함수 인자의 system_program은 시스템 프로그램을 사용하기 위해선 반드시 포함해야 한다.
Transfer라던지, Init이라던지, 여기선 새로운 계정 생성을 위해 넣었다.
즉 이 함수는 bank PDA를 만들고, 데이터를 초기화한다.
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> ProgramResult {
let txn = anchor_lang::solana_program::system_instruction::transfer(
&ctx.accounts.user.key(),
&ctx.accounts.bank.key(),
amount
);
anchor_lang::solana_program::program::invoke(
&txn,
&[
ctx.accounts.user.to_account_info(),
ctx.accounts.bank.to_account_info()
],
)?;
ctx.accounts.bank.balance += amount;
Ok(())
}
#[derive(Accounts)]
pub struct Deposit<'info>{
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
amount 만큼 특정 bank에 입금하는 함수다.
bank는 여러개 만들 수 있기 때문에, 어떤 계좌인지 특정을 해줘야 한다.
나와 계좌의 잔액을 업데이트하기 위해, 시스템 프로그램의 transfer를 사용한다.
CPI할 때 invoke를 사용한다고 했었다.
좀 맘에 안드는건, 잔액은 시스템에서 내재된 기능으로 확인할 수 있을텐데 굳이
balance 데이터를 따로 둬서 관리한다는 거다. 저거 조작하면 어쩌려고.
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> ProgramResult {
let bank = &mut ctx.accounts.bank;
let user = &mut ctx.accounts.user;
if bank.owner != user.key() {
return Err(ProgramError::IncorrectProgramId);
}
let rent = Rent::get()?.minimum_balance(bank.to_account_info().data_len());
if **bank.to_account_info().lamports.borrow() - rent < amount {
return Err(ProgramError::InsufficientFunds);
}
**bank.to_account_info().try_borrow_mut_lamports()? -= amount;
**user.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}
#[derive(Accounts)]
pub struct Withdraw<'info>{
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
}
출금 함수다. 위 deposit과 똑같다고 생각할 수 있겠지만,
여기선 조심해야 할 사안이 2가지 있다.
rent
bank account에서 출금을 하는데, 만약 잔액이 0으로 가게 해선 안된다.
계정마다 데이터를 저장하는 만큼 담보금을 예치해놔야 하기 때문이다.
그래서 rent를 확인하고, 출금금액과 비교해서 뺀다.
try_borrow_mut_lmaports()
아까처럼 시스템 프로그램의 Transfer를 사용해야 할 거 같은데,
여기선 인자로 프로그램을 받지도 않는다.
try_borrow_mut_lmaports() 이게 대체 뭐길래.
즉 트랜잭션을 일으키지 않고, 계정의 상태를 업데이트한다.
나는 이게 이해가 안되는데, 모든 상태 변화는 트랜잭션이 일으켜야 한다.
그런데 이렇게 일반 DB 쓰는 것 처럼 한다면, 조작과 동기화 문제가 생기지 않나 싶다.
강사가 다양한 예를 보여주려고 한건지...
use anchor_lang::prelude::*;
use anchor_lang::solana_program::entrypoint::ProgramResult;
declare_id!("6uRHvqNvaaag9uTatEwUNwwnHmsv6iKLisysTV5bGUoK");
#[program]
pub mod bank {
use super::*;
pub fn create(ctx: Context<Create>, name: String) -> ProgramResult {
let bank = &mut ctx.accounts.bank;
bank.name = name;
bank.balance = 0;
bank.owner = *ctx.accounts.user.key;
Ok(())
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> ProgramResult {
let txn = anchor_lang::solana_program::system_instruction::transfer(
&ctx.accounts.user.key(),
&ctx.accounts.bank.key(),
amount
);
anchor_lang::solana_program::program::invoke(
&txn,
&[
ctx.accounts.user.to_account_info(),
ctx.accounts.bank.to_account_info()
],
)?;
ctx.accounts.bank.balance += amount;
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> ProgramResult {
let bank = &mut ctx.accounts.bank;
let user = &mut ctx.accounts.user;
if bank.owner != user.key() {
return Err(ProgramError::IncorrectProgramId);
}
let rent = Rent::get()?.minimum_balance(bank.to_account_info().data_len());
if **bank.to_account_info().lamports.borrow() - rent < amount {
return Err(ProgramError::InsufficientFunds);
}
**bank.to_account_info().try_borrow_mut_lamports()? -= amount;
**user.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}
}
#[derive(Accounts)]
pub struct Create<'info> {
#[account(init, payer=user, space=5000, seeds=[b"bankaccount", user.key().as_ref()], bump)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct Bank {
pub name: String,
pub balance: u64,
pub owner: Pubkey,
}
#[derive(Accounts)]
pub struct Deposit<'info>{
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Withdraw<'info>{
#[account(mut)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
}
먼저 위 Lib.rs를
anchor build // 컴파일
solana config set -o d //데브넷으로 변경
// 배포 계정의 SOL 확보
anchor deploy // 배포
프로그램을 배포한다.
컴파일 결과인 bank.json, bank.ts를 프론트엔드에 가져와야 한다.
이들은 마치 솔리디티 ABI처럼, 프론트엔드에서 호출할 때 자동완성과 타입 도움을 줄 것이다.

다 필요없고, 우리가 원하는건
이다.
이를 위해
이 필요하다.
이는 라이브러리에서 제공을 해준다.
이더리움과 달리 내가 따로 RPC 노드를 찾을 필요가 없다.
import { useWallet, useConnection } from '@solana/wallet-adapter-react';
const ourWallet = useWallet();
const { connection } = useConnection()
const getProvider = () => {
const provider = new AnchorProvider(connection, ourWallet, AnchorProvider.defaultOptions())
setProvider(provider)
return provider
}
이걸로 브라우저 익스텐션인 지갑을 연결하고,
솔라나 엔드포인트 노드를 설정하는 것이다.
너무너무 당연하겠지만,
데이터가 포함되어야 할 거 아닌가?
import idl from "./bank.json"
import { Bank } from "./bank"
import { PublicKey } from '@solana/web3.js';
const idl_string = JSON.stringify(idl)
const idl_object = JSON.parse(idl_string)
const programID = new PublicKey(idl.address) // 프로그램 주소
const createBank = async () => {
try {
const anchProvider = getProvider()
const program = new Program<Bank>(idl_object, anchProvider) // 프로그램 타게팅
await program.methods.create("New Bank").accounts({
user: anchProvider.publicKey
}).rpc() // create 함수 실행, 인자 1개
console.log("Wow, new bank was created")
} catch (error) {
console.error("Error while creating a bank: " + error)
}
}
const getBanks = async () => {
try {
const anchProvider = getProvider()
const program = new Program<Bank>(idl_object, anchProvider)
Promise.all((await connection.getParsedProgramAccounts(programID)).map(async bank => ({
...(await program.account.bank.fetch(bank.pubkey)), // Fetch : 상태 조회
pubkey: bank.pubkey
}))).then(banks => {
console.log(banks)
setBanks(banks)
})
} catch (error) {
console.error("Error while getting banks: " + error)
}
}
const depositBank = async (publicKey) => {
try {
const anchProvider = getProvider()
const program = new Program<Bank>(idl_object, anchProvider)
await program.methods.deposit(new BN(0.01 * web3.LAMPORTS_PER_SOL))
.accounts({
bank: publicKey,
user: anchProvider.publicKey
}).rpc()
console.log(" Deposit done: " + publicKey)
} catch (error) {
console.error("Error while depositing to a bank: " + error)
}
}
눈에 좀 익을 것이다.
특이한건, 분명 프로그램을 만들 땐 create()에서 bank 인자도 필요했는데,
name, user 밖에 없다.
#[derive(Accounts)]
pub struct Create<'info> {
#[account(init, payer=user, space=5000, seeds=[b"bankaccount", user.key().as_ref()], bump)]
pub bank: Account<'info, Bank>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
bank는 시스템에서 알아서 만들어준다. init이 그 의미다.
'위 정보를 바탕으로 니가 만들어줘~'
하는 것인데, 여기서 알 수 있는건 인자에 들어간다고 모두 넣어야하는 건 아니라는 것이다. 여기 구조체 인자에서는 사전에 필요한 데이터를 모두 명시하고 들어가는 느낌이랄까?

