CosmWasm smart contract 배포하기

2023. 6. 14. 10:26Blockchain/Smart contract

728x90
반응형

※ 원글 작성 : 22년 5월 10일

Terra에 CW smart contract를 배포해보려 한다. NFT contract는 CosmWasm NFT를 참고로 작성할 것이다.

Store NFT images

IPFS를 설치하여 ipfs add IMAGE_NAME.jpg로 등록을 할 수 있지만 편의를 위해 NFT를 위한 file 등록 사이트를 사용하여 NFT image를 저장한다.

Image를 등록하면 위 그림과 같이 CID가 생성되고, CID를 통해 IPFS 내의 이미지 or 파일을 확인할 수 있다.
https://ipfs.io/ipfs/[your_image_CID]로 파일 확인이 가능하다.

NFT 등록을 위해서는 JSON 형식의 Metadata도 필요하다. Metadata 형식은 표준화 되어 있으며 Opensea docs에서 표준형식을 확인할 수 있다. Contract가 확인할 수 있는 JSON metadata를 생성 후 마찬가지로 IPFS에 저장하여 URI를 미리 생성해야 한다.

Cw721 Basic Contract

Config

cw721 Basic contract를 사용해본다. 코드를 다운로드 받고 해당 예시 코드를 보며 작성한다.

먼저, Cargo.toml뿐이니 Cargo.lock을 생성하기 위해 cargo build를 실행하여 target folder와 Cargo.lock을 생성한다.

cargo build 전에, Cargo.toml을 확인해보면

[package]
name = "cw721-base"
version = "0.13.2"
authors = [
  "Ethan Frey <ethanfrey@users.noreply.github.com>",
  "Orkun Külçe <orkun@deuslabs.fi>",
]
edition = "2018"
description = "Basic implementation cw721 NFTs"
license = "Apache-2.0"
repository = "https://github.com/CosmWasm/cw-nfts"
homepage = "https://cosmwasm.com"
documentation = "https://docs.cosmwasm.com"

exclude = [
  # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
  "artifacts/*",
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]

[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []

[dependencies]
cw-utils = "0.13.2"
cw2 = "0.13.2"
cw721 = { version = "0.13.2" }
cw-storage-plus = "0.13.2"
cosmwasm-std = { version = "1.0.0-beta8" }
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = { version = "1.0" }

[dev-dependencies]
cosmwasm-schema = { version = "1.0.0-beta8" }

dependencies의 버전이 현재 latest로 확인된다. 하지만, Terra SDK를 사용하여 contract를 testnet에 store하려 할 때

terra_sdk.exceptions.LCDResponseError:Status 400 - failed to execute message; message index: 0: Error calling the VM: Error during static Wasm validation: Wasm contract has unknown interface_version_* marker export (see https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/README.md): store wasm contract failed: invalid request
의 메시지로 interface_version을 지원하지 않는다고 한다. Stackoverflow에서 확인해보니 현재 Terra에서는 cosmwasm-std의 최신버전을 지원하지 않는것으로 보이며, dependencies의 버전을 다운그레이드 해야한다. 다운그레이드함으로써 contract의 코드가 많이 바뀔것 같다.

여기서는 CosmWasm/cw-nfts에 버전을 최신으로 clone하여 사용하고 있다. 물론 Version 0.9.2 등 cosmwasm-std의 버전이 낮은 상태를 지원하는 코드가 있지만 어떤 부분이 달라졌는지를 확인해보며 직접 알아보고자 손수 코드를 뜯으며 진행하겠다.


[dependencies]
cw-utils = "0.13.2"
cw2 = "0.9.1"
cw721 = { version = "0.9.1" }
cw-storage-plus = "0.9.1"
cosmwasm-std = { version = "0.16.0" }
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = { version = "1.0" }

[dev-dependencies]
cosmwasm-schema = { version = "0.16.0" }

[package.metadata.scripts]
optimize = """docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/rust-optimizer:0.12.5
"""

그리하여 dependencies의 버전을 위와같이 변경하고, smart contract 최적화를 위해 script도 Cargo.toml내에 추가한다. 참고로 script를 통한 contract 생성은 cargo run-script oprimize로 실행한다.


State.rs

cw721_base의 state를 그대로 사용한다. Contract struct는 아래와 같이 구성되어 있다.

pub struct Cw721Contract<'a, T, C>
where
    T: Serialize + DeserializeOwned + Clone,
{
    pub contract_info: Item<'a, ContractInfoResponse>,
    pub minter: Item<'a, Addr>,
    pub token_count: Item<'a, u64>,
    /// Stored as (granter, operator) giving operator full control over granter's account
    pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>,
    pub tokens: IndexedMap<'a, &'a str, TokenInfo<T>, TokenIndexes<'a, T>>,

    pub(crate) _custom_response: PhantomData<C>,
}

cosmwasm-std 다운그레이드를 진행하면 state.rs내에 변경 필요 부분이 생긴다.
cw_storage_plus에서 가져온 MultiIndex에서 token_owner_idx가 문제가 된다.
cargo check를 통해 오류메시지를 확인하고(expected struct 'Addr', Found struct 'TokenInfo') token_owner_idx를 아래와 같이 변경한다.

pub fn token_owner_idx<T>(d: &TokenInfo<T>, k: Vec<u8>) -> (Addr, Vec<u8>) {
    (d.owner.clone(), k)
}

TokenIndexes 내의 MultiIndex도 아래와 같이 변경한다.

pub struct TokenIndexes<'a, T>
where
    T: Serialize + DeserializeOwned + Clone,
{
    pub owner: MultiIndex<'a, (Addr, Vec<u8>), TokenInfo<T>>,
}

Execute.rs

Execute시작에 앞서 cw721-0.9.2 버전의 Cw721Execute가 burn을 지원하지 않기 때문에 Execute내의 burn 관련 기능을 일단 모두 제거한 후 시작한다.
method 'burn' is not a member of trait 'Cw721Execute

Blockchain에 contract가 등록되면 가장먼저 instantiate를 진행해야 한다. cw721의 instantiate는 간단하게 구성되어 있다.

    pub fn instantiate(
        &self,
        deps: DepsMut,
        _env: Env,
        _info: MessageInfo,
        msg: InstantiateMsg,
    ) -> StdResult<Response<C>> {
        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

        let info = ContractInfoResponse {
            name: msg.name,
            symbol: msg.symbol,
        };
        self.contract_info.save(deps.storage, &info)?;
        let minter = deps.api.addr_validate(&msg.minter)?;
        self.minter.save(deps.storage, &minter)?;
        Ok(Response::default())
    }

Instantiate를 위해 SDK에서 전달해줘야 하는 msg 형식은 아래와 같다.

// msg.rs

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    /// Name of the NFT contract
    pub name: String,
    /// Symbol of the NFT contract
    pub symbol: String,

    /// The minter is the only one who can create new NFTs.
    /// This is designed for a base NFT that is controlled by an external program
    /// or contract. You will likely replace this with custom logic in custom NFTs
    pub minter: String,
}

SDK에서 minter 주소를 생성하고 minting하기 위한 name과 symbol을 결정하여 json으로 contract에 전달하면 Instantiate가 완료된다. NFT를 minter의 정보, contract info 등이 Instantiate시에 체인에 저장된다.


가장 먼저 mint기능을 사용해본다. mintExecuteMsg::Mint()형식의 메시지를 수신하면 minting이 진행된다. SDK에서 아래와 같은 json형식으로 컨트랙트를 호출하여 Execute mint를 진행할 것이다.

"mint": {
    "token_id": "tokenId0",
    "owner": "terra1sj5r75y...h3z9d36",
    "token_uri": "https://ipfs.io/ipfs/NFT_JSON_METADATA_URI"
}

MintMsg에는 위 3개의 parameter 말고도 extension이 존재하는데, mandatory는 아니며, 이 컨트랙르를 custom extension할 시 사용한다. SDK의 메시지를 수신한 contract는 아래 코드를 통해 동작을 수행한다.

    pub fn mint(
        &self,
        deps: DepsMut,
        _env: Env,
        info: MessageInfo,
        msg: MintMsg<T>,
    ) -> Result<Response<C>, ContractError> {
        let minter = self.minter.load(deps.storage)?;

        if info.sender != minter {
            return Err(ContractError::Unauthorized {});
        }

        // create the token
        let token = TokenInfo {
            owner: deps.api.addr_validate(&msg.owner)?,
            approvals: vec![],
            token_uri: msg.token_uri,
            extension: msg.extension,
        };
        self.tokens
            .update(deps.storage, &msg.token_id, |old| match old {
                Some(_) => Err(ContractError::Claimed {}),
                None => Ok(token),
            })?;

        self.increment_tokens(deps.storage)?;

        Ok(Response::new()
            .add_attribute("action", "mint")
            .add_attribute("minter", info.sender)
            .add_attribute("owner", msg.owner)
            .add_attribute("token_id", msg.token_id))
    }

NFT를 생성하려는 sender(=minter)가 메시지 전송을 통해 NFT token 정보를 저장한다. 당연히 sender와 minter가 같아야지만 minting을 수행하고, token의 owner, token_uri등의 정보를 저장한다.

Query.rs

마찬가지로, 다운그레이드 덕분에 코드를 수정해야한다. QueryExecute와는 달리 수정할 부분이 너무 많아서 다 못적을...것 같다. (괜히 뜯으면서 했다는 생각을 잠깐 했다.)

크게 보면, Cw721Query에서 지원하는 traits이 변경되었다. 과거 버전에서는 all_approvals가 존재했지만 11버전으로 와서는 Operators, approval, approvals로 기능이 세분화되었다. 그래서 ApprovalResponse, ApprovalsResponse, OperatorsResponse를 지원하지 않기 때문에 삭제를 하고, impl에서 Operator부분만 all_approvals로 변경하여 trait 형식을 맞췄다.

    fn all_approvals(
        &self,
        deps: Deps,
        env: Env,
        owner: String,
        include_expired: bool,
        start_after: Option<String>,
        limit: Option<u32>,
    ) -> StdResult<ApprovedForAllResponse> {
        let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
        let start_addr = maybe_addr(deps.api, start_after)?;
        let start = start_addr.map(|addr| Bound::exclusive(addr.as_ref()));

        let owner_addr = deps.api.addr_validate(&owner)?;
        let res: StdResult<Vec<_>> = self
            .operators
            .prefix(&owner_addr)
            .range(deps.storage, start, None, Order::Ascending)
            .filter(|r| {
                include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block)
            })
            .take(limit)
            .map(parse_approval)
            .collect();
        Ok(ApprovedForAllResponse { operators: res? })
    }

Result

간단히 mint만 해서 SDK로 전송을 했다. token_id는 'lunch', token_uri는 위에서 등록한 NFT metadata를 가리키는 URI로 contract에 전달하면 Terra client가 Transaction broadcast result를 뱉어준다.


Example의 Query는 9가지의 기능이 있다. SDK를 통해

  • minter : minter addr
    Minter의 주소를 보여준다.

  • contract_info : contract information
    NFT contract의 정보를 준다. 설정한 NFT의 이름과 그 symbol을 return한다.

  • num_tokens : the number of tokens
    현재 contract에 등록된 token의 개수를 보여준다. 'lunch' 한가지만 현재 등록되어있는 상태라 count는 1을 나타낸다.

  • owner_of : owner of the NFT
    token_id를 통한 NFT의 owner를 확인한다.

  • nft_info : NFT information
    token_id를 통해 URI등의 NFT 정보를 보여준다.

  • approved_for_all : all operators can access owner's token
    Owner token에 접근 가능한 모든 operator를 보여준다. 현재 따로 설정하지 않았기 때문에 빈 list를 반환한다.

  • all_nft_info : all NFT information
    token_id검색을 통한 관련된 NFT의 모든 정보를 보여준다. nft_infoowner_of의 정보가 함께 return된다.

  • tokens : all tokens of owner
    owner가 보유하고 있는 모든 token을 response한다. token_id가 return된다.

  • all_tokens : all tokens of contract
    현재 smart contract에 등록되어있는 모든 token을 확인한다. token_id가 return되며, 현재는 'lunch'만 등록하여 해당 ID만 확인 가능하다.


Conclusion

Terra에 CosmWasm contract를 배포하고 query해봤다. cw721-base를 사용해서 extension내의 metadata는 없는 상태이다. cw721-metadata-onchain도 결국은 cw721-base에서 extension generic만 metadata로 입력한 것이기에 metadata 저장만 onchain에 하지 않는것 말고는 모두 동일한 기능을 제공한다. Cosmwasm에서 example을 잘 정리해둬서, FT/NFT smart contract 개발에 많은 참고가 될 수 있을것이다.

728x90
반응형