튜토리얼#4 Substrate Kitties(1)

코와->코어·2022년 5월 9일
0

러스트 코드 많다그래서 공부할 겸 이 파트 한다했는데...... 파트 1, 2로 나눌 정도로 분량 많을 줄이야...... 허허허


Substrate Kitties 체인 만들기

이 튜토리얼에서 Substrate kitties라 불리는 NFT를 생성하고 소유권을 다루는 블록체인을 만드는 법을 배울 겁니다.

파트 1에서는 당신이 만든 키티들과 상호작용할 수 있는 기능을 포함해서 키티 팔레트를 만드는 방법을 다룹니다.

파트 2에서는 파트 1에서 만든 Substarte Kitties 블록체인과 상호작용하도록 프론트엔드를 개발하는 방법을 다룹니다.

1. 튜토리얼 목표

  • Substrate 노드를 만들고 실행시키는 기본 패턴을 배운다
  • 커스텀 FRAME 팔렛을 작성하고 런타임에 통합한다.
  • 스토리지 아이템들을 만들고 업데이트하는 법을 배운다
  • pallet extrinsic과 helper function을 작성한다
  • Substrate 노드를 커스텀 프론트엔드와 연결하기 위해 폴카닷JS API를 사용한다

시작하기 전에 설치할 것
참고

우리가 만드는 것

  • 기존의 소스로부터 만들어지거나 기존 키티들을 길러서 생성
  • 주인에 의해 설정된 가격에 팔림
  • 주인에게서 다른 주인에게로 전송

우리가 다루지 않는 것

  • 팔레트 테스트 코드
  • 정확한 무게 값 사용하기

2. 기본 세팅

커스텀 팔렛을 설정하고 간단한 스토리지 아이템을 포함하는 섭스트레이트 노드 템플릿을 사용하는 기본적인 패턴을 다룹니다.

당신의 템플릿 노드 설정하기
섭스트레이트 노드 템플릿에 네트워킹과 합희 계층이 포함되어 있기 때문에, 런타임과 팔렛의 로직을 만드는 것에만 집중하면 됩니다.

시작하기 위해서, 우리는 프로젝트 이름과 의존성을 설정해야 합니다. kickstart라는 CLI 도구로 쉽게 노드템플릿의 이름을 바꾸겠습니다.

  1. cargo install kickstart

  2. 프로젝트를 시작하고 싶은 루트 디렉토리에서
    kickstart https://github.com/sacha-l/kickstart-substrate
    : 제일 최근의 노드 템플릿 복사본을 클론해오고 노드와 팔레트 이름 설정하게 함

  3. kitties(node 폴더 안에 생김), kitties(pallet 폴더 안에 생김)

  4. 좋아하는 IDE로 kitties 디렉토리를 열고 이름을 kitties-tutorial로 바꿈
    kickstart가 수정한 디렉토리들

  • node : 당신의 노드가 런타임과 RPC 클라이언트들과 상호작용하는 걸 허용하는 모든 로직을 포함
  • pallets : 모든 커스텀 팔렛들이 있음
  • runtime : 모든 팔렛들이 집계되고 체인의 런타임에서 구현됨

5.runtime/src/lib.rs에서 커스텀 팔렛의 이름을 TemplateModule에서 SubstrateKitties로 수정

construct_runtime!(
    // --snip
    {
        // --snip
        SubstrateKitties: pallet_kitties,
    }
);

pallet_kitties의 발판 작성

kitties-tutorial <-- The name of our project directory
|
+-- node
|
+-- pallets
| |
| +-- kitties
| |
| +-- Cargo.toml
| |
| +-- src
| |
| +-- benchmarking.rs <-- Remove file
| |
| +-- lib.rs <-- Remove contents
| |
| +-- mock.rs <-- Remove file
| |
| +-- tests.rs <-- Remove file
|
+-- Cargo.toml

  1. pallets/kitties/src에 있는 benchmarking.rs, mock.rs, test.rs를 삭제하고 lib.rs의 내용을 지우기

섭스트레이트에서 팔렛은 런타임 로직을 정의하는 데 사용되고, 우리는 섭스트레이트 키티들을 관리하는 모든 로직을 하나의 팔렛에 만들 것

모든 FRAME 팔렛은

  • frame_support와 frame_system의 의존성 집합
  • 필요한 attribute macros(configuration trait, storage item, function call...)
    를 가짐
  1. pallet/kitties/src/lib.rs 파일의 내용을 다음 코드로 교체
#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;

#[frame_support::pallet]
pub mod pallet {
    use frame_support::{
        sp_runtime::traits::{Hash, Zero},
        dispatch::{DispatchResultWithPostInfo, DispatchResult},
        traits::{Currency, ExistenceRequirement, Randomness},
        pallet_prelude::*
    };
    use frame_system::pallet_prelude::*;
    use sp_io::hashing::blake2_128;

    // TODO Part II: Struct for holding Kitty information.

    // TODO Part II: Enum and implementation to handle Gender type in Kitty struct.

    #[pallet::pallet]
    #[pallet::generate_store(pub(super) trait Store)]
    pub struct Pallet<T>(_);

    /// Configure the pallet by specifying the parameters and types it depends on.
    #[pallet::config]
    pub trait Config: frame_system::Config {
        /// Because this pallet emits events, it depends on the runtime's definition of an event.
        type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;

        /// The Currency handler for the Kitties pallet.
        type Currency: Currency<Self::AccountId>;

        // TODO Part II: Specify the custom types for our runtime.

    }

    // Errors.
    #[pallet::error]
    pub enum Error<T> {
        // TODO Part III
    }

    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        // TODO Part III
    }

    // ACTION: Storage item to keep a count of all existing Kitties.

    // TODO Part II: Remaining storage items.

    // TODO Part III: Our pallet's genesis configuration.

    #[pallet::call]
    impl<T: Config> Pallet<T> {

        // TODO Part III: create_kitty

        // TODO Part III: set_price

        // TODO Part III: transfer

        // TODO Part III: buy_kitty

        // TODO Part III: breed_kitty
    }

    // TODO Part II: helper function for Kitty struct

    impl<T: Config> Pallet<T> {
        // TODO Part III: helper functions for dispatchable functions

        // TODO: increment_nonce, random_hash, mint, transfer_from

    }
}
  1. 우리의 팔렛에서 sp_io를 사용할 것이므로 Cargo.toml파일의 의존성에 이것이 추가되어있는지 확인
sp-io = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.20" }
  1. 다음 커맨드로 팔렛 빌드해보기. 아직 Currency type을 구현하기 않았기 때문에 전체 체인은 빌드하지 않을 거임. 대신 팔렛의 에러가 있는지 없는지 확인하게 위해 빌드, unused import 에러는 무시해도 됨

cargo build -p pallet-kitties

storage item 추가

우리의 런타임에 변수를 저장하는 함수 로직을 추가해보자

storage macro에 의존하는 trait인 섭스트레이트 storage API에 있는 StorageValue를 사용할 것
우리가 선언하고 싶은 어떤 스토리지 아이템에 대해서 우리는 #[pallet::storage] 매크로를 먼저 써줘야 함

pallet/kitties/src/lib.rs 파일에서 Action 부분을 다음으로 교체

#[pallet::storage]
#[pallet::getter(fn count_for_kitties)]
pub(super) type CountForKitties<T: Config> = StorageValue<_, u64, ValueQuery>;

: 이것으로 총 존재하는 키티들의 개수를 우리의 팔렛에 추적하기 위한 스토리지 아이템을 생성

currency 구현 추가

우리의 노드를 만들기 전에, currency type을 팔렛 런타임 구현에 추가해야 함

runtime/src/lib.rs 에 다음 부분을 추가

impl pallet_kitties::Config for Runtime {
    type Event = Event;
    type Currency = Balances; // <-- Add this line
}

이제 진짜로 노드를 빌드하고 에러가 없는지 확인하기
cargo build --release

3. 유일성, 커스텀 타입, 스토리지 맵

pallet/kitties/src/lib.rs 파일을 다음 코드로 교체

#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;

#[frame_support::pallet]
pub mod pallet {
    use frame_support::pallet_prelude::*;
    use frame_system::pallet_prelude::*;
    use frame_support::{
        sp_runtime::traits::Hash,
        traits::{ Randomness, Currency, tokens::ExistenceRequirement },
        transactional
    };
    use sp_io::hashing::blake2_128;

    #[cfg(feature = "std")]
    use frame_support::serde::{Deserialize, Serialize};

    // ACTION #1: Write a Struct to hold Kitty information.

    // ACTION #2: Enum declaration for Gender.

    // ACTION #3: Implementation to handle Gender type in Kitty struct.

    #[pallet::pallet]
    #[pallet::generate_store(pub(super) trait Store)]
    pub struct Pallet<T>(_);

    /// Configure the pallet by specifying the parameters and types it depends on.
    #[pallet::config]
    pub trait Config: frame_system::Config {
        /// Because this pallet emits events, it depends on the runtime's definition of an event.
        type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;

        /// The Currency handler for the Kitties pallet.
        type Currency: Currency<Self::AccountId>;

        // ACTION #5: Specify the type for Randomness we want to specify for runtime.

        // ACTION #9: Add MaxKittyOwned constant
    }

    // Errors.
    #[pallet::error]
    pub enum Error<T> {
        // TODO Part III
    }

    // Events.
    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        // TODO Part III
    }

    #[pallet::storage]
    #[pallet::getter(fn count_for_kitties)]
    pub(super) type CountForKitties<T: Config> = StorageValue<_, u64, ValueQuery>;

    // ACTION #7: Remaining storage items.

    // TODO Part IV: Our pallet's genesis configuration.

    #[pallet::call]
    impl<T: Config> Pallet<T> {

        // TODO Part III: create_kitty

        // TODO Part IV: set_price

        // TODO Part IV: transfer

        // TODO Part IV: buy_kitty

        // TODO Part IV: breed_kitty
    }

    //** Our helper functions.**//

    impl<T: Config> Pallet<T> {

        // ACTION #4: helper function for Kitty struct

        // TODO Part III: helper functions for dispatchable functions

        // ACTION #6: function to randomly generate DNA

        // TODO Part III: mint

        // TODO Part IV: transfer_kitty_to
    }
}

serde를 팔렛의 Cargo.toml파일에 추가하기
맞는 버전 찾아서!!

kitty struct 구조

하나의 Kitty가 가지고 있어야 하는 정보

  • dna : 고양이의 유니크한 속성인 dna를 식별하는데 사용되는 해시값. 새 고양이를 기르거나 다른 고양이 세대를 추적하는 데에도 사용됨 => [u8;16]
  • price : 주인이 설정한, 고양이를 사는 데 필요한 금액 => BalanceOf FRAME의 Currency trait에 있는 커스텀 타입
  • gender : Male or Female인 enum => Gender 타입 ! 곧 만들것임
  • owner : 한 명의 주인을 가리키는 계정 id

구조체 선언하기 전에 BalanceOf와 AccountOf를 추가해줘야 함

Action1 부분을 다음으로 교체

type AccountOf<T> = <T as frame_system::Config>::AccountId;
type BalanceOf<T> =
    <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

// Struct for holding Kitty information.
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
#[codec(mel_bound())]
pub struct Kitty<T: Config> {
    pub dna: [u8; 16],
    pub price: Option<BalanceOf<T>>,
    pub gender: Gender,
    pub owner: AccountOf<T>,
}

이 파일 맨 위에
use scale_info::TypeInfo;
이 줄 추가해서 우리의 구조체가 이 trait에 접근할 수 있도록 해주기

Gender 커스텀 타입 작성

  1. Male, Female 값을 갖는 enum 선언
    Action item 2 부분 다음 코드로 교체
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Gender {
    Male,
    Female,
}

enum 선언하기 전에 dereive 매크로 써줘야 함
우리 런타임의 다른 타입들과 소통하려면 이렇게 감싸줘야 함?

  1. kitty struct에 helper function 구현하기
    Gender type을 리턴하고 랜덤으로 Gender enum 값을 고르기 위해 랜덤 함수를 사용하는 gen_gender라는 퍼블릭 함수를 만들자

Action4를 다음 코드로 교체

fn gen_gender() -> Gender {
    let random = T::KittyRandomness::random(&b"gender"[..]).0;
    match random.as_ref()[0] % 2 {
        0 => Gender::Male,
        _ => Gender::Female,
    }
}

on-chain 무작위성 구현
위에서 아직 정의하지 않은 KittyRandomness라는 걸 사용했음 이제 알아볼까
frame_support에 있는 Randomness trait을 사용할 것

  1. 팔렛의 Configuration trait에서, Randomness trait에 바운드된 새로운 타입 정의
    Randomness trait은 output과 blocknumber 제네릭을 교체하기 위해 인자로 구체화해주는 게 필요함
    우리의 경우, 이 trait을 사용해서 함수듸 결과값이 Blake2 128-bit hash가 되길 원함
    ACtion 5를 다음으고 교체
type KittyRandomness: Randomness<Self::Hash, Self::BlockNumber>;
  1. 런타임에서 실제 타입을 구체화하기
    팔렛 설정할 때 새 타입 추가한 거에, 구체적인 타입을 설정하기 위해 우리 런타임을 설정해줘야 함
    노드 템플릿이 이미 RandomnessCollectiveFilp 팔렛을 가지고 있기 때문에 runtime/src/lib.rs 팡리 안에 KittyRandomness 타입을 선언해주기만 하면 됨
impl pallet_kitties::Config for Runtime {
    type Event = Event;
    type Currency = Balances;
    type KittyRandomness = RandomnessCollectiveFlip; // <-- ACTION: add this line.
}
  1. 랜덤 DNA 생성하기
    같은 블록에서 한 번 이상 이 함수를 호출할 때 다른 해시값을 얻기 위해 extrinsic__index를 사용할 것이고 blake_128도 사용할 것
    ACtion6번째 줄을 다음으로 교체
fn gen_dna() -> [u8; 16] {
    let payload = (
        T::KittyRandomness::random(&b"dna"[..]).0,
        <frame_system::Pallet<T>>::extrinsic_index().unwrap_or_default(),
        <frame_system::Pallet<T>>::block_number(),
    );
    payload.using_encoded(blake2_128)
}

남은 스토리지 아이템들 작성
모든 고양이들을 쉽게 추적하기 위해서 유일한 아이디를 우리의 스토리지 아이템의 글로벌 키로 사용하는 로직을 표준화할 것임

새 고양이의 아이디는 항상 유일해야 함
해시값을 아이디로 갖고 그에 대한 고양이 오브젝트를 맵핑하는 Kitties 스토리지 아이템을 사용할 것임

Action 7을 교체

런타임은
Kitties라는 스토리지 맵으로 유일한 자산인 currency나 kitties

#[pallet::storage]
#[pallet::getter(fn kitties)]
pub(super) type Kitties<T: Config> = StorageMap<
    _,
    Twox64Concat,
    T::Hash,
    Kitty<T>,
>;

그 자산들의 소유권인 계정 아이디를 KittiesOwned라는 스토리지 맵으로 다룸

#[pallet::storage]
#[pallet::getter(fn kitties_owned)]
/// Keeps track of what accounts own what Kitty.
pub(super) type KittiesOwned<T: Config> = StorageMap<
    _,
    Twox64Concat,
    T::AccountId,
    BoundedVec<T::Hash, T::MaxKittyOwned>,
    ValueQuery,
>;
  

FRAME이 제공하는 StorageMap으로 Kitty 구조체의 스토리지 인스턴스 생성할 것

ACtion 9를 다음 줄로 교체

#[pallet::constant]
type MaxKittyOwned: Get<u32>;

MaxKittyowned라는 타입을 config trait에 추가

parameter_types! {              // <- add this macro
    // One can own at most 9,999 Kitties
    pub const MaxKittyOwned: u32 = 9999;
}

/// Configure the pallet-kitties in pallets/kitties.
impl pallet_kitties::Config for Runtime {
    type Event = Event;
    type Currency = Balances;
    type KittyRandomness = RandomnessCollectiveFlip;
    type MaxKittyOwned = MaxKittyOwned; // <- add this line
}

이 코드를 추가해라~~

컴파일 되는지 확인!
cargo build --release

4. 디스패쳐블, 이벤트, 에러

  • create_kitty : 한 계정이 고양이 한 마리를 민팅하게 허락하는 dispatchable 또는 publicly callable함수
  • mint : 우리 팔렛의 스토리지 아이템을 업데이트하고 에러를 체크하는 create_kitty에 의해 호출되는 도우미 함수
  • Events: FRAME의 #[pallet::event] attribute 사용

Public and Private 함수들
최적화를 위해 복잡한 로직을 private 도우미 함수로 쪼갤 것
private함수는 다양한 dispatchavle함수들에 의해 호출 가능

  • create_kitty는 dispatchable 함수 또는 extrinsic임: origin이 sign받았는지 확인, 사인한 계정으로 랜덤 해시 생성, 그걸로 새로운 고양이 오브젝트 만들고 private mint()함수 호출

  • mint는 private helper function: 그 고양이가 이미 존재하지 않는지 확인, 새 고양이 아이디로 스토리지 업데이트, 스토리지에 있는 새 총 고양이 개수와 새 주인의 계정 업데이트, 고양이가 성공적으로 만들어졌다ㅡㄴ 신호에 대한 이벤트 쌓기

create_kitty dispatchable 작성하기

FRAME에서 dispatchable은 항상 같은 구조임

모든 팔렛 dispatchables는 #[pallet::call] 매크로 아래에 있고, impl<T: Config>Pallet<T> {}로 선언되어야 함
  • weights : 팔렛::call 의 요구사항에 따라, 모든 디스패쳐블 함수들은 연관 무게를 가져야 함 무게는 실행 시간에 블록을 맞추려고 계산의 양에 대해 세이프가드를 제공하기 때문에 서브스트레이트에서 개발하는 데 중요한 부분임

서브스트레이트의 weighting system은 개발자들이 연산 복잡도를 매 extrinsic에 대해 생각하도록 강제한다. 그래서 노드가 최악의 실행시간을 가정하게 하고 구체적인 블록 시간보다 더 오래 걸리는 extrinsic들과 네트워킹하는 것을 피하게 한다. 무게는 어떠한 사인된 extrinsic에 대해서도 수수료 시스템과도 내부적으로 연결되어 있음

pallets/kitties/src/lib.rs에서 action1을 다음으로 교체

  #[pallet::weight(100)]
pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult {
    let sender = ensure_signed(origin)?; // <- add this line
    let kitty_id = Self::mint(&sender, None, None)?; // <- add this line
    // Logging to the console
    log::info!("A kitty is born with ID: {:?}.", kitty_id); // <- add this line

    // ACTION #4: Deposit `Created` event

    Ok(())
}                                                             

로깅하기 위해서 맞는 버전의 log::info Cargo.toml파일에 추가하기

mint() 함수 작성하기

  • owner: &T::AccountId, 고양이 주인
  • dna: Option<[u8;16]>, 만약 None이면 랜덤 DNA 생성됨
  • gender: Option

    Result<T::Hash, Error\<\T>>를 리턴

Action 2부분을 다음으로 교체

  // Helper to mint a Kitty.
pub fn mint(
    owner: &T::AccountId,
    dna: Option<[u8; 16]>,
    gender: Option<Gender>,
) -> Result<T::Hash, Error<T>> {
    let kitty = Kitty::<T> {
        dna: dna.unwrap_or_else(Self::gen_dna),
        price: None,
        gender: gender.unwrap_or_else(Self::gen_gender),
        owner: owner.clone(),
    };

    let kitty_id = T::Hashing::hash_of(&kitty);

    // Performs this operation first as it may fail
    let new_cnt = Self::count_for_kitties().checked_add(1)
        .ok_or(<Error<T>>::CountForKittiesOverflow)?;

    // Check if the kitty does not already exist in our storage map
    ensure!(Self::kitties(&kitty_id) == None, <Error<T>>::KittyExists);

    // Performs this operation first because as it may fail
    <KittiesOwned<T>>::try_mutate(&owner, |kitty_vec| {
        kitty_vec.try_push(kitty_id)
    }).map_err(|_| <Error<T>>::ExceedMaxKittyOwned)?;

    <Kitties<T>>::insert(kitty_id, kitty);
    <CountForKitties<T>>::put(new_cnt);
    Ok(kitty_id)
}

먼저 새 고양이 오브젝트를 생성
유일한 kitty_id를 고양이의 현재 특성에 기초한 해시 함수를 사용해 생성
CountForKitties를 스토리지 게터함수인 Self::count_for_kitties()를 사용해 증가시키고
check_add()함수로 오버플로우 확인
kitty_id가 이미 존재하지는 않는지 검사

검사 다 통과하면 스토리지 아이템을 업데이트:
try_mutate로 고양이 주인 벡터 업데이트
섭스트레이트 스토리지 맵 API가 제공하는 insert 메소드로 실제 고양이 오브젝트를 저장하고 kitty_id와 연관시키기
StorageValue API에 의해 제공된 put으로 최신 고양이 개수 저장

pallet Events 구현하기
우리 팔렛은 함수 마지막에 이벤트도 발생시킬 수 있음
FRAME의 #[pallet::event]를 통해서 쉽게 팔렛의 이벤트를 관리하고 선언할 수 있음, FRAME 매크로로 이벤트를 enum 선언처럼 할 수 있음

  #[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config>{
    /// A function succeeded. [time, day]
    Success(T::Time, T::Day),
}

attribute macro인 #[pallet::generate_deposit(pub(super) fn deposit_event)]을 사용할 수 있음

이를 통해 특정 이벤트를 적립할 수 있음, ㅇSelf::deposit_event(Event::Success(var_time, var_day));
이런 패턴을 통해서!

우리 팔렛의 설정 trait Config 안에다 새로운 연관 타입인 Event를 추가해줘야 함
그리고 runtime/src/lib.rs에도 추가해줘야 함
``rust
/// Configure the pallet by specifying the parameters and types it depends on.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type Event: From<Event> + IsType<::Event>;
//--snip--//
}

Action3을 다음으로 교체해서 이벤트 선언해라
```rust
  // A new Kitty was successfully created. \[sender, kitty_id\]
Created(T::AccountId, T::Hash),
/// Kitty price was successfully set. \[sender, kitty_id, new_price\]
PriceSet(T::AccountId, T::Hash, Option<BalanceOf<T>>),
/// A Kitty was successfully transferred. \[from, to, kitty_id\]
Transferred(T::AccountId, T::AccountId, T::Hash),
/// A Kitty was successfully bought. \[buyer, seller, kitty_id, bid_price\]
Bought(T::AccountId, T::AccountId, T::Hash, BalanceOf<T>),

Action 4를 다음으로 교체해서 완성해라
Self::deposit_event(Event::Created(sender, kitty_id));

에러 처리
FRAME을 통해 [#pallet::error]로 우리 팔렛에 에러를 구체화하고 팔렛 함수 안에서 사용할 수 있게 해줌
#[pallet::error]아래에 FRAME 매크로를 사용해서 가능한 모든 에러를 Action5줄로 교체

/// Handles arithmetic overflow when incrementing the Kitty counter.
CountForKittiesOverflow,
/// An account cannot own more Kitties than `MaxKittyCount`.
ExceedMaxKittyOwned,
/// Buyer cannot be the owner.
BuyerIsKittyOwner,
/// Cannot transfer a kitty to its owner.
TransferToSelf,
/// This kitty already exists
KittyExists,
/// This kitty doesn't exist
KittyNotExist,
/// Handles checking that the Kitty is owned by the account transferring, buying or setting a price for it.
NotKittyOwner,
/// Ensures the Kitty is for sale.
KittyNotForSale,
/// Ensures that the buying price is greater than the asking price.
KittyBidPriceTooLow,
/// Ensures that an account has enough funds to purchase a Kitty.
NotEnoughBalance,
  

다 잘 빌드되는지 확인
cargo build --release

Polkadat-JS Apps UI로 테스트하기
루트 디렉토리에서 다음 실행
./target/release/node-kitties --tmp --dev

Developer에서 extrinsc으로 가서 사인된 extrincic을 substrateKitties를 사용해서 사인하고 제출하는데, createKitty 디스패쳐블 호출해서 해라. 앨리스, 밥, 찰리의 계정으로 해봐라

이벤트로 가서 Created찾고 블록 디테일 이벤트 실행된 거 확인가능해야 함

chain state가서 substrateKitties팔렛 가서 Kitties(hash)Kitty 쿼리 실행해서 디테일 확인
include option체크 안 한 상태에서만 다음 정보 볼 수 있음

5. Kittiy들과 상호작용하기

고양이 가격 설정

  • 고양이 주인 확인 : 스토리지에 있는 오브젝트들을 수정하는 함수들을 만들면서 오직 적절한 사용자들만 성공적으로 그 디스패쳐블 ㅅ함수들의 로직을 실행할 수 있는지를 확인해야 함 Action #1a에 붙여넣기
    ensure!(Self::is_kitty_owner(&kitty_id, &sender)?, <Error>::NotKittyOwner); Action #1b에 붙여넣기
    pub fn is_kitty_owner(kitty_id: &T::Hash, acct: &T::AccountId) -> Result<bool, Error> {
    match Self::kitties(kitty_id) {
    Some(kitty) => Ok(kitty.owner == *acct),
    None => Err(<Error>::KittyNotExist)
    }
    }
  • 우리 가격 업데이트: 스토리지에서 고양이 오브젝트 가져와서 새 가격으로 업데이트하고 다시 스토리지에 저장
    ACtion2다음으로 교체
    kitty.price = new_price.clone();
    <Kitties<T>>::insert(&kitty_id, kitty);
    
  • 이벤트 적립하기: 다 성공적으로 이뤄지면 이벤트 적립 가능 ACtion 3 다음으로 교체
    // Deposit a "PriceSet" event.
    Self::deposit_event(Event::PriceSet(sender, kitty_id, new_price));

고양이 교환
transfer(): 디스패쳐블 함수, publickly 호출가능하 다스패처블
transfer_kitty_to() : private helper function. transfer사 호출, 고양이 교환할 때 모든 스토리지 업데이트 담당

action 4

#[pallet::weight(100)]
pub fn transfer(
  origin: OriginFor<T>,
  to: T::AccountId,
  kitty_id: T::Hash
) -> DispatchResult {
  let from = ensure_signed(origin)?;

  // Ensure the kitty exists and is called by the kitty owner
  ensure!(Self::is_kitty_owner(&kitty_id, &from)?, <Error<T>>::NotKittyOwner);

  // Verify the kitty is not transferring back to its owner.
  ensure!(from != to, <Error<T>>::TransferToSelf);

  // Verify the recipient has the capacity to receive one more kitty
  let to_owned = <KittiesOwned<T>>::get(&to);
  ensure!((to_owned.len() as u32) < T::MaxKittyOwned::get(), <Error<T>>::ExceedMaxKittyOwned);

  Self::transfer_kitty_to(&kitty_id, &to)?;

  Self::deposit_event(Event::Transferred(from, to, kitty_id));

  Ok(())
}
  • 트랜잭션 사인됐나 확인하고

  • 고양이 주인이 보냈나 확인하고
    주인에게 보내는 거 아닌가 확인하고
    받는 사람이 하나 이상의 고양이 가질 수 있는지 확인하고
    transfer_kitty_to함수 실행

    Action5

    #[transactional]
    pub fn transfer_kitty_to(
      kitty_id: &T::Hash,
      to: &T::AccountId,
    ) -> Result<(), Error<T>> {
      let mut kitty = Self::kitties(&kitty_id).ok_or(<Error<T>>::KittyNotExist)?;
    
      let prev_owner = kitty.owner.clone();
    
      // Remove `kitty_id` from the KittiesOwned vector of `prev_owner`
      <KittiesOwned<T>>::try_mutate(&prev_owner, |owned| {
          if let Some(ind) = owned.iter().position(|&id| id == *kitty_id) {
              owned.swap_remove(ind);
              return Ok(());
          }
          Err(())
      }).map_err(|_| <Error<T>>::KittyNotExist)?;
    
      // Update the kitty owner
      kitty.owner = to.clone();
      // Reset the ask price so the kitty is not for sale until `set_price()` is called
      // by the current owner.
      kitty.price = None;
    
      <Kitties<T>>::insert(kitty_id, kitty);
    
      <KittiesOwned<T>>::try_mutate(to, |vec| {
          vec.try_push(*kitty_id)
      }).map_err(|_| <Error<T>>::ExceedMaxKittyOwned)?;
    
      Ok(())
    }

    #[transactional]은 오직 ok를 리턴할 때만 스토리지 바꿀 수 있도록 해줌

고양이 구매

그 고양이가 판매중인지 확인
현재가격이 사용자의 예산에 있는지와 충분한 free balance를 가지고 있는지 확인
ACtion 6 교체

// Check the kitty is for sale and the kitty ask price <= bid_price
if let Some(ask_price) = kitty.price {
  ensure!(ask_price <= bid_price, <Error<T>>::KittyBidPriceTooLow);
} else {
  Err(<Error<T>>::KittyNotForSale)?;
}

// Check the buyer has enough free balance
ensure!(T::Currency::free_balance(&buyer) >= bid_price, <Error<T>>::NotEnoughBalance);

고ㅑㅇ이 받을 공간 있나 확인

Action7다음으로 교체

// Verify the buyer has the capacity to receive one more kitty
let to_owned = <KittiesOwned<T>>::get(&buyer);
ensure!((to_owned.len() as u32) < T::MaxKittyOwned::get(), <Error<T>>::ExceedMaxKittyOwned);

let seller = kitty.owner.clone();

Action 8
// Transfer the amount from buyer to seller
T::Currency::transfer(&buyer, &seller, bid_price, ExistenceRequirement::KeepAlive)?;

// Transfer the kitty from seller to buyer
Self::transfer_kitty_to(&kitty_id, &buyer)?;

// Deposit relevant Event
Self::deposit_event(Event::Bought(buyer, seller, kitty_id, bid_price));

고양이 기르기
ACtion 9
let new_dna = Self::breed_dna(&parent1, &parent2)?;

ACtino 10
Self::mint(&sender, Some(new_dna), None)?;

제네시스 설정
초기 상태 설정해주기
FRAME's #[pallet::genesis_config] 사용
제네시스 블록에 어떤 고양이 오브젝트 선언하도록 허용?

ACtion 11
// Our pallet's genesis configuration.
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub kitties: Vec<(T::AccountId, [u8; 16], Gender)>,
}

// Required to implement default for GenesisConfig.
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig {
fn default() -> GenesisConfig {
GenesisConfig { kitties: vec![] }
}
}

#[pallet::genesis_build]
impl<T: Config> GenesisBuild for GenesisConfig {
fn build(&self) {
// When building a kitty from genesis config, we require the dna and gender to be supplied.
for (acct, dna, gender) in &self.kitties {
let _ = <Pallet>::mint(acct, Some(dna.clone()), Some(gender.clone()));
}
}
}

node/src/chain_spec.rs로 가서 맨 위에 use node_kitties_runtime::SubstrateKittiesConfig; 추가하고 testnet_genesis함수에 추가
substrate_kitties: SubstrateKittiesConfig {
kitties: vec![],
},

빌드, 실행, 상호작용하기
cargo build --release
./target/release/node-kitties --dev --tmp

Fund multiple users with tokens so they can all participate
Have each user create multiple Kitties
Try to transfer a Kitty from one user to another using the right and wrong owner
Try to set the price of a Kitty using the right and wrong owner
Buy a Kitty using an owner and another user
Use too little funds to purchase a Kitty
Overspend on the cost of the Kitty and ensure that the balance is reduced appropriately
Breed a Kitty and check that the new DNA is a mix of the old and new

profile
풀스택 웹개발자👩‍💻✨️

0개의 댓글