2023. 6. 14. 10:26ㆍBlockchain/Smart contract
※ 원글 작성 : 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
기능을 사용해본다. mint
는 ExecuteMsg::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
마찬가지로, 다운그레이드 덕분에 코드를 수정해야한다. Query
는 Execute
와는 달리 수정할 부분이 너무 많아서 다 못적을...것 같다. (괜히 뜯으면서 했다는 생각을 잠깐 했다.)
크게 보면, 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 NFTtoken_id
를 통한 NFT의 owner를 확인한다.
nft_info
: NFT informationtoken_id
를 통해 URI등의 NFT 정보를 보여준다.
approved_for_all
: all operators can access owner's token
Owner token에 접근 가능한 모든 operator를 보여준다. 현재 따로 설정하지 않았기 때문에 빈 list를 반환한다.
all_nft_info
: all NFT informationtoken_id
검색을 통한 관련된 NFT의 모든 정보를 보여준다.nft_info
와owner_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 개발에 많은 참고가 될 수 있을것이다.
'Blockchain > Smart contract' 카테고리의 다른 글
Golang을 이용하여 Solidity smart contract 다루기 (2) | 2024.11.13 |
---|---|
CosmWasm 컨트랙트 개발 시 전략과 신경 쓰면 좋은 점 (1) | 2023.11.01 |
Remix로 contract 만들고 배포하기 (0) | 2023.07.11 |
CosmWasm 기본 개념 및 구조 (0) | 2023.07.06 |
CosmWasm Contract Migration (0) | 2023.06.15 |