[KO][NEAR 102] 2편: 니어 프로토콜 컨트랙트 패턴 이해하기

c0wjay
DSRV
Published in
28 min readSep 5, 2022

DSRV Dev Guild에서는 더 많은 개발자들과 Web3 인프라를 만들어가기 위해, 다양한 메인넷과 스마트 컨트랙트에 대한 가이드를 연재합니다.

Disclaimer: 이 글은 정보 전달을 위한 목적으로 작성되었으며, 특정 프로젝트에 대한 투자 권고, 법률적 자문 등 목적으로 하지 않습니다. 모든 투자의 책임은 개인에게 있으며, 이로 발생된 결과에 대해 어떤 부분에서도 DSRV는 책임을 지지 않습니다. 본문이 포괄하는 내용들은 특정 자산에 대한 투자를 추천하는 것이 아니며, 언제나 본문의 내용만을 통한 의사결정은 지양하시길 바랍니다.

[NEAR 102 시리즈]

  1. 니어 프로토콜 기본 개념 이해하기 — 메인넷 아키텍처와 RPC 호출
  2. 니어 프로토콜 컨트랙트 패턴 이해하기

시작하기 전에..

이번 미디엄 시리즈에서는 지난 8월 1일과 2일 양일간 DSRV Builder’s House에서 진행하였던 니어 프로토콜 핸즈온 워크샵에서 다뤘던 내용을 설명하고자 합니다. 이번 편은 그 중에서 2일차에 다뤘던 내용으로, 컨트랙트 패턴에 대해 다뤄봅니다. 니어 프로토콜에서 컨트랙트 업그레이드는 어떻게 이루어지고, Cross Contract Calls 패턴과 테스트 코드 작성에 대해 다뤄보겠습니다. 위 밋업은 Boom Labs와 DSRV의 지원 하에 이루어졌습니다. 또한 워크샵을 준비 및 진행하는 과정에서의 제 개인 회고는 Boom Labs 미디엄 글에서 읽어보실 수 있습니다.

핸즈온 워크샵에 참여하지 않으셨던 분들도, 글을 읽고 따라하며 니어 프로토콜을 이해할 수 있도록 하는 것이 목표입니다. 다만 본 글을 읽으시기 전에, “[NEAR 102] 1편: 니어 프로토콜 이해하기 — 메인넷 아키텍처와 RPC 호출”을 먼저 읽고 오시는 것을 추천드립니다. 본 글에서 사용하는 자료들은 아래에서 확인하실 수 있습니다.

1. Upgrading Contract & Schema Migration

이번 세션에서는 1편에서 배포하였던 FT 컨트랙트를 업그레이드 하도록 하겠습니다. 만일, 1편을 진행하지 않으신 분이 계시다면, 먼저 1편을 보고 FT 컨트랙트 배포까지 진행해주세요.

먼저 일단 다른 작업 없이, 컨트랙트를 업그레이드 해보도록 하겠습니다.

git checkout 4.contract/upgraded-ft
export USER="sender.testnet"
yarn build && near deploy --accountId $USER --wasmFile export/main.wasm

4번 브랜치로 체크아웃 해주세요. "sender.testnet" 대신 1편에서 컨트랙트를 배포한 본인 계정으로 환경변수를 설정해주신 후, 3번째 라인의 명령어를 통해 해당 계정으로 컨트랙트를 배포해주세요.

[그림 2–1] near-cli를 통한 컨트랙트 재배포 결과, 출처: @c0wjay, DSRV

위와 같이 해당 계정에 이미 배포된 컨트랙트가 있는데, 진행할 것이냐? (y/n)라는 문구가 뜹니다. y 를 입력해주세요. 그러면 배포가 진행됩니다.

near view $USER ft_balance_of '{"account_id": "'$USER'"}'
near view $USER ft_metadata

배포 후 컨트랙트와 상호작용하기 위해, 위 명령어들을 입력해주세요.

[그림 2–2] 재배포된 컨트랙트 호출 결과, 출처: @c0wjay, DSRV

그러면 위와 같이 'Cannot deserialize the contract state.' 에러가 뜹니다.

💡 무슨 일이 일어나는 지 알아볼까요?

이건 1편에서 배포했던 컨트랙트 코드의 일부입니다. 보면 Contract Struct에 tokenmetadata 필드만 존재하며, 해당 struct로 Contract 객체가 initialize 됩니다.

이건 새로 배포한 컨트랙트 코드의 Struct 입니다. Contract struct에 controller 란 필드가 추가된 것을 알 수 있습니다. 여기서, 이 controller 필드의 의의에 대해 TMI를 하자면, 새로 배포한 컨트랙트의 경우엔 ft_mint 라는 메소드를 새로 구현했습니다. 해당 FT를 민팅할 수 있는 메소드인데요, 이 토큰에 대한 민팅 권한 (ft_mint 메소드 호출 권한)을 따로 두기 위해. controller 필드를 만들고, 해당 필드에 저장된 주소의 계정에게 권한을 주기 위해 만들었습니다.

다시 돌아와서, 위 Contract struct로 새 코드가 "sender.testnet" 으로 배포되었는데요. 1편에서 설명드렸듯이, 니어는 state storage를 따로 보관하고 있습니다. 그리고 컨트랙트는 해당 storage에 저장된 state를 deserialize하여 불러오고, state를 바꿀만한 코드가 실행되고, 바뀐 state는 다시 serialize하여 storage에 저장합니다.

즉, 컨트랙트를 재배포했을 때, 컨트랙트는 controller 필드가 추가된 struct를 기준으로 storage에서 state를 불러옵니다. 하지만, 실제로 storage에 저장된 state의 형태는 이전 버전인 token 필드와 metadata 필드만 존재하는 형태입니다. 따라서 deserialize 과정에서 오류가 생기기 때문에 'Cannot deserialize the contract state.' 에러가 뜨는 것이죠.

이 문제를 해결하기 위해서는, Schema migration이라는 과정을 진행해야 합니다. [1] 아래에서 더 다뤄보겠습니다.

1.1. Schema migration

Schema migration은 생각보다 간단합니다.
먼저 1편에서 배포했던 컨트랙트와 같은 OldContract struct로 old 객체를 선언합니다.
그리고 env::state_read().unwrap() 를 통해 스토리지에 저장된 state를 불러온 후, old객체에 unwrapping 합니다.
이 과정에서, 스토리지에 저장된 state와 OldContract 객체의 struct는 호환되기 때문에, 저장된 state의 deserialize가 원활히 진행됩니다.

마지막으로, controller 필드까지 포함된 Contract struct의 객체를 initialize하고, 스토리지에 다시 저장합니다. 여기서 controller 필드에 저장되는 값은 이 migrate 메소드를 호출하는 계정의 주소가 됩니다. [2]

# 이미 배포된 컨트랙트의 migrate 함수 호출
near call $USER migrate '' --accountId $USER

# 배포와 동시에 initialize 함수 호출
near deploy --wasmFile export/main.wasm --initFunction "migrate" --initArgs "{}" --accountId $USER

한번 실습으로 진행해보겠습니다.

위와같이 이미 upgrade된 컨트랙트 코드를 배포한 계정의 migrate 메소드를 호출하거나, 또는 upgrade된 컨트랙트 코드를 계정에 재배포함과 동시에 migrate 메소드를 호출할 수도 있습니다.

[그림 2–3] migration한 컨트랙트 호출 결과, 출처: @c0wjay, DSRV

그러면 위와 같이 schema migration한 struct는 잘 호환이 되며, ft_balance_of, ft_metadata 메소드를 호출해도 'Cannot deserialize the contract state.' 에러가 발생하지 않는 것을 확인할 수 있습니다.

💡 DSRV's Tip: #[init(ignore_state)]가 무엇인가요? [3]1편에서 다뤘듯이, #[init] attribute macro는 아래의 메소드가 컨트랙트가 initialize될 때 호출하는 메소드임을 decorate합니다. 이 때, attribute macro의 인자로 ignore_state를 넣을 수 있습니다. 인자없이, #[init] macro만 사용한다면, initialize 과정에서 해당 컨트랙트 struct의 state가 이미 스토리지에 저장되어 있다면 "The contract has already been initialized" 라는 에러를 반환합니다.
하지만 #[init(ignore_state)]를 사용한다면, 이미 컨트랙트 struct의 state가 저장되어 있어도, 무시하고 진행합니다.
아래 코드를 통해 조금 더 딥다이브 해보겠습니다.
위 두 코드는 attribute macro를 처리하는 부분입니다. attribute macro 내에 `init` 이라는 string이 들어있으면, 파싱을 하여 `InitAttr` struct에 저장합니다. 이 때 `ignore_state` 가 인자로 들어있으면, `ignore_state` field를 `true`로, 없다면 해당 field를 `false`로 저장합니다.

그리고 `init_attr.ignore_state` 가 `true`라면, `method_type`은 `MethodType::InitIgnoreState`으로,
`false`라면, `method_type`은 `MethodType::Init`으로 저장합니다.
`method_wrapper` 함수는 `MethodType::Init` 이라면, `near_sdk::env::state_exists()` 를 통해, 해당 struct의 state가 스토리지에 이미 저장되어 있는지 확인합니다.

이미 state가 존재한다면, `"The contract has already been initialized"` 라는 에러메세지를 반환합니다.
하지만 `MethodType::InitIgnoreState`라면, 위와 같은 검사 과정을 스킵합니다.

1.2. Metadata update

위 migrate 코드를 활용하면, 이미 스토리지에 저장된 state의 struct를 바꿀 뿐만 아니라, 데이터도 변경할 수 있습니다. 이번 시간에는 토큰의 메타데이터를 변경하여, 토큰의 아이콘을 생성하고 decimal, 이름 등을 바꿔보도록 하겠습니다.

코드는 위와 같습니다. migrate 메소드와 큰 차이가 없는데요, 보시면 migrate_metadata 메소드를 호출할 때, 인자로 metadata 를 넘겨주며, 해당 metadata를 새로 state에 저장하는 것임을 알 수 있습니다.

near deploy --wasmFile export/main.wasm --initFunction "migrate_metadata" --initArgs '{"metadata": { "spec": "ft-1.0.0", "name": "BOOM LABS UPGRADED TOKEN", "symbol": "BOOM", "icon": "'$ICON'", "decimals": 4 }}' --accountId $USER

여기서 $ICON 환경변수는, 본인이 바꾸고자 하는 토큰 아이콘의 svg 파일을 입력해주세요. 이번 시간에는 src 폴더에 저장되어 있는 boomlabslogo.svg 파일로 입력습니다. svg 파일 데이터는 이 글을 참고하여 변환해주세요. [4]

[그림 2–4] 상: 메타데이터 업데이트 전 테스트넷 지갑 상의 토큰 모습 / 하: 메타데이터 업데이트 후 테스트넷 지갑 상의 토큰 모습, 출처: @c0wjay, DSRV

테스트넷 지갑으로 들어가면, 첫 번째 사진에서 두 번째 사진으로 토큰 메타데이터가 변경된 것을 알 수 있습니다.

2. Cross Contract Calls

다음으로는 Cross Contract Calls에 대해 배워보겠습니다.

[그림 2–5] vending-machine 컨트랙트 전체 모식도, 출처: @c0wjay, DSRV

Cross Contract Calls를 위해, 앞서 배포하였던 토큰 컨트랙트와 상호작용을 하는 새 컨트랙트를 배포해보겠습니다. 그 모식도는 위와 같으며, vending-machine이라고 이름을 붙여봤습니다.

  1. 유저는 vending-machine 컨트랙트 계정인 vendor.sender.testnetget_token 메소드를 호출합니다.
  2. vendor.sender.testnet 컨트랙트 struct에는 token_contract 필드가 있는데요, 해당 필드엔 토큰 컨트랙트인 token.sender.testnet 주소가 저장되어 있습니다. vendor.sender.testnet는 컨트랙트에 저장되어 있는 token.sender.testnetft_mint 메소드를 호출합니다. 이 때, 인자로 sender.testnetamount를 넘겨줍니다.
  3. token.sender.testnet의 컨트랙트 struct의 controllervendor.sender.testnet로 저장되어 있습니다. 따라서 vending-machine만 토큰에 대한 mint authority를 갖고 있습니다. ft_mint 메소드 호출 시에 넘겨준 sender.testnet 으로 amount 만큼 토큰을 발행합니다. ft_mint 메소드의 결과로 "{ amount } tokens for { sender.testnet } are minted” 라는 메세지를 담아 promise를 vending-machine에 반환합니다.
  4. vending-machine은 promise를 받으면, promise를 처리하는 callback 메소드가 실행됩니다. callback 메소드는 결과에 따라 에러 메세지를 반환하거나, ft_mint 메소드 결과로 받은 메세지를 unwrapping합니다.
  5. 유저는 에러 메세지 또는 "{ amount } tokens for { sender.testnet } are minted” 메세지를 확인할 수 있습니다.
git checkout 5.contract/cross-contract-call

위 기능을 한번 코드에서 확인해보겠습니다. 먼저 위 명령어를 입력하여 5번 브랜치로 이동해주세요.

learn-near-in-rust
├─ token
│ ├─ src
│ │ └─ lib.rs
│ └─ target

├─ vending-machine
│ ├─ src
│ │ ├─ external_traits.rs
│ │ └─ lib.rs
│ └─ target

└─ export
├─ token.wasm
└─ vending-machine.wasm

파일의 계층 구조는 위와 같습니다. learn-near-in-rust/token/src/ 위치에는 토큰 컨트랙트 코드가 저장되어 있습니다. learn-near-in-rust/vending-machine/src/ 위치에는 vending-machine 컨트랙트 코드가 저장되어 있습니다. 그리고 위 두 컨트랙트를 wasm 파일로 컴파일하면, 각각 learn-near-in-rust/export/ 위치에 저장됩니다. 그러면 한번 코드를 확인해보겠습니다.

토큰 컨트랙트 코드는 큰 특이점은 없습니다. 다만 4번 브랜치의 FT 컨트랙트 코드와의 차이점은, ft_mint 메소드가 string을 반환한다는 점입니다. 이는 ft_mint 메소드 마지막 라인에 써져있는데, 어떤 유저에게 얼마 만큼의 토큰이 민팅되었음을 나타내는 메세지를 반환합니다.

먼저 external_traits.rs 파일부터 확인해보겠습니다. 해당 파일은 cross contract calls로 호출할 상대 컨트랙트(여기에서는 토큰 컨트랙트)의 인터페이스를 정의하는 파일입니다. #[ext_contract(..)] attribute macro로 상대 컨트랙트의 trait을 정의합니다. [5]

did_promise_succeed 메소드는 반환받은 promise의 PromiseResultSuccessful인지 확인하여 bool을 반환하는 유틸 메소드입니다.

위는 cross contract calls를 진행하는 vending-machine의 컨트랙트 코드입니다. 이를 한번 뜯어보겠습니다.

2.1. Cross-contract Calls & Promises

위는 get_token 메소드의 일부입니다. 보시면 해당 메소드는 Promise 객체를 반환하는 것을 알 수 있습니다. Cross-contract calls은 네트워크에서 두 종류의 promises를 생성함으로써 동작합니다.

  1. 외부 컨트랙트를 실행하기 위한 promise (Promise.create)와,
  2. 위 promise 결과를 토대로, 본 컨트랙트의 다른 메소드를 실행하기 위한 call-back promise (Promise.then) 입니다.

두 종류의 promise 모두 같은 인자를 받아야 합니다. 그 구조는 아래와 같은데요,

  • 상호작용하고자 하는 컨트랙트의 주소
  • 해당 컨트랙트 내의 실행하고자 하는 메소드
  • 메소드를 실행하기 위해 넘겨줘야 하는 (암호화된) 인자
  • Cross contract calls에 사용되는 가스 (본 컨트랙트 실행을 위해 넘겨준 총 가스에서 뺍니다.)
  • 첨부할 NEAR (컨트랙트 잔고에서 뺍니다.)

위 인자들로 구성되어 있습니다. [6]

get_token 메소드 내 promise 역시 위와 같이 구성되어 있습니다. vending-machine 컨트랙트 struct의 token_contract 필드에 저장된 FT 컨트랙트 주소로 cross contract call을 날리며, 첨부한 NEAR는 1 yocto NEAR, 그리고 GAS_FEE 만큼의 가스를 cross contract call에 소모합니다. 그리고 get_token 메소드를 호출한 유저 계정 주소와 get_token 메소드 호출 시 넘겨준 amount를 인자로 넘겨주어 ft_mint 메소드를 호출합니다. 이는 위 유저 계정 주소로 amount 만큼의 토큰을 발행하는 역할을 합니다. 또한 "{ amount } tokens for { sender.testnet } are minted” 라는 스트링을 반환해주겠죠.

2.2. Callback

다음으로 위에서 반환된 스트링을 처리하는 callback promise에 대해 알아보겠습니다.

callback promise는 이전에 보낸 cross contract call이 제대로 처리된 후에 차례대로 처리되어야 합니다. 이를 위해 promise.then( .. )와 같은 문법을 사용하는데요. 이를 promise chaining이라 부릅니다. [7]

그리고 괄호 안에는 2.1. 절에서 설명했던 cross contract calls의 구조가 들어갑니다. 여기서 promise chaining을 위해, 다른 컨트랙트를 다음으로 호출할 수도 있습니다. 다만 본 예제에서는 다른 컨트랙트를 호출하는 대신, cross contract calls의 결과를 처리하기 위해, vending-machine 컨트랙트 내의 callback 메소드를 호출합니다. 본인 스스로를 호출하는 것이기 때문에, Self::ext(env::current_account_id()) 와 같이 문법을 사용하며, 그 뒤의 내용은 cross contract calls와 다를 바가 없습니다. [8]

호출하는 callback 메소드는 위와 같습니다. 특별한 내용은 없고, 만약 이전 cross contract call이 성공적으로 진행되었다면, PromiseResult::Successful(_) 안에 담겨진 스트링 ("{ amount } tokens for { sender.testnet } are minted”)을 unwrapping하여 반환해줍니다.

만일 cross contract call 도중에 문제가 있었다면, "Error on calling token contract" 란 로그를 띄웁니다.

2.3. 실습

코드를 분석해보았으니, 실습을 진행해보겠습니다.

near create-account "vendor".$USER --masterAccount $USER --initialBalance 10 && near create-account "token".$USER --masterAccount $USER --initialBalance 10

먼저 vending machine 컨트랙트와 토큰 컨트랙트를 각각 배포할 서브 계정을 만듭니다.

yarn buildnear deploy --accountId "vendor".$USER --wasmFile export/vending-machine.wasm && near deploy --accountId "token".$USER --wasmFile export/token.wasm

그리고 vending machine 컨트랙트 코드와 토큰 컨트랙트 코드를 각각 wasm 파일로 컴파일한 후, 위에서 만든 서브 계정에 각각 배포를 합니다.

near call "token".$USER new '{"owner_id": "vendor.'$USER'", "metadata": { "spec": "ft-1.0.0", "name": "BOOM LABS TOKEN", "symbol": "BOOM", "icon": "'$ICON'", "decimals": 8 }}' --accountId $USERnear call "vendor".$USER new '{"token_contract": "token.'$USER'"}' --accountId "vendor".$USER

각각의 컨트랙트를 initialize 합니다.

near call "token".$USER storage_deposit '' --accountId $USER --amount 0.00125near call "vendor".$USER get_token '{"amount": "3"}' --amount 3 --accountId $USER --gas 100000000000000

토큰 컨트랙트의 스토리지에 본인 계정 주소를 등록한 후, vending-machine 컨트랙트의 get_token 메소드를 호출합니다.

[그림 2–6] get_token 메소드 호출 결과, 출처: @c0wjay, DSRV

그러면 위와 같이 ‘3 tokens for sender.testnet are minted’ 라는 스트링이 출력됩니다!

3. Unit Test & Integration Test

[그림 2–7] NEAR 블록체인의 테스트 단계 모식도, 출처: NEAR 101 slides

NEAR의 테스트 환경은 크게 unit test, simulation test, integration test로 나뉠 수 있습니다. 다만 그 쓰임새에 있어서 simulation test와 integration test 단계를 굳이 나누는 것은 불필요하다고 생각하며, 현재 NEAR에서 simulation test는 액티브하게 지원하고 있지 않기 때문에 unit test와 integration test로만 단계를 나누어 테스트를 진행해도 좋습니다. 본 글에서는 두 테스트에 대해 다뤄보겠습니다. [9, 10]

Unit test는 컨트랙트의 메소드를 각각 테스트하는 용도입니다. Unit test의 장점은 가볍고 빠르게 해당 메소드의 로직을 테스트할 수 있습니다. 다만 이는 로컬 환경에서, 메소드에 고립되어 진행되기 때문에 실행에 소모된 gas fee를 계산한다던지, cross contract calls을 테스트한다던지 등의 블록체인 환경 위에서 일어나는 사건에 대해선 테스트할 수 없습니다. Integration test는 실제 NEAR network과 같은 테스트환경에서 진행합니다. testnet에서도 진행 가능하며, 또한 위 환경을 모킹한 로컬 sandbox 환경에서도 진행할 수 있습니다. 이는 unit test와 달리 실제 NEAR 블록체인 환경과 같은 테스트 환경에서 진행하기 때문에, E2E 테스트가 가능합니다. [10, 11]

Unit test의 경우 Rust의 cargo의 테스트 모듈을 이용합니다. 이와 관련된 내용은 윤수지님의 “[NEAR 101] 1편: NEAR Counter 컨트랙트 톺아보기”에 자세히 설명되어 있기 때문에, 해당 글을 읽고 참고하시길 바랍니다.

git checkout 6.contract/test

실습을 위해 6번 브랜치로 체크아웃해주세요. Integration test는 learn-near-in-rust/integration-test/src/lib.rs 파일에 있습니다. 이번 예제의 integration test는 로컬 sandbox 환경을 구성하고, 로컬넷 환경에 vending-machine 컨트랙트를 배포하고, vending-machine 컨트랙트의 get_token 메소드가 cross-contract call이 잘 수행되는 지, 테스트해보겠습니다. 이 때, 상대 토큰 컨트랙트는 이미 테스트넷에 배포된 컨트랙트를 “Spooning” 하여 가져올 예정입니다. [12]

먼저 처음부터 보겠습니다. main 메소드를 보면, anyhow::Result<()>를 반환하는데요. anyhow란 에러를 처리하는 crate 입니다. 다음 라인에는 worker를 정의하고 있습니다. worker는 로컬 sandbox환경과 상호작용하기 위한 게이트웨이입니다. 그리고 learn-near-in-rust/export/위치에 저장된 wasm 파일을 읽고, wasm에 정의하고, worker.dev_deploy(&wasm).await?;를 통해 로컬 sandbox 환경에 dev_deploy 합니다. [13]

다음은 실제 테스트넷에 배포된 FT 컨트랙트를 Spooning하기 위한 라인들입니다. [14] 첫 라인은 Testnet archival node에 연결하고, 이와 상호작용할 worker를 grab하는 부분입니다. 우리는 이 archival node로 부터 특정 시점의 FT 컨트랙트를 가져올 계획입니다. [15]

토큰 컨트랙트 ID는 "contract.boomlabs.testnet"입니다. 다음은 testnet의 특정 시점으로 부터 FT 컨트랙트를 pull down하여, token_contract 객체에 저장하는 라인입니다. .import_contract 메소드는 AccountId("contract.boomlabs.testnet")와 테스트넷에 연결된 Worker를 인자로 받습니다. 테스트넷으로 부터 컨트랙트를 import하고, ImportContractTransaction 객체를 반환합니다. [16]

다음으로 .initial_balance 메소드는 토큰 컨트랙트의 initial balance를 1000 $NEAR로 설정하고, .block_height메소드는 96257940 블록 시점의 컨트랙트를 가져올 것으로 설정합니다.

마지막으로 .transact().await 메소드를 통해 ImportContractTransaction을 실행하고, 그 결과를 반환받습니다.

다음으로는 계정을 생성하는 부분입니다. 로컬 sandbox 환경도 메인넷/테스트넷과 같이, TLA가 존재해야 하고, TLA로 부터 서브계정들을 생성할 수 있습니다. 첫 번째 라인은, TLA역할을 할 root 계정을 생성하는 라인입니다.

다음은 root 계정으로 부터, jay 라는 서브계정을 생성합니다. .create_subaccount 메소드는 worker와 계정 주소로 쓸 스트링을 인자로 받고, CreateAccountTransaction 객체를 반환합니다. 30NEAR를 initial balance로 설정하고, 다시 .transact().await 메소드를 통해 트랜잭션이 실행됩니다.

위 두 라인은 실제 컨트랙트 호출을 하는 부분입니다. 예를 들어 첫 라인은 near-cli 에서 near call 'token_contract' new_default_meta '{"owner_id": "vendor_contract"}' --accountId 'jay' 명령어 처리와 같은 의미입니다.

다음 test_get_token 메소드는 cross contract call이 잘 이루어지는지 확인하기 위한 테스트 코드입니다. jayvendor_contractget_token 메소드를 2 토큰만 받게끔 호출했을 때, 실제로 반환받는 메세지가 "2 tokens for jay are minted" 와 같은지 확인하는 라인이며, 만약 같다면 " Passed ✅ gets default message" 메세지를 띄웁니다.

그러면 한번 코드를 실행해보겠습니다.

npm run test

위 명령어를 입력하면,

[그림 2–8] Unit test결과, 출처: @c0wjay, DSRV

유닛 테스트를 통과하는 것과,

[그림 2–9] Integration test결과, 출처: @c0wjay, DSRV

로컬 sandbox 환경에서 블록 3개 처리 후, integration-test를 통과하는 것을 확인할 수 있습니다.

만약 위와 같이 test_get_token 메소드 내에서 get_token 메소드를 호출할 때 넘겨주는 인자를 수정하면, 아래와 같이 integration-test가 실패합니다.

[그림 2–10] Integration test 실패 결과, 출처: @c0wjay, DSRV

글을 마무리하며

이번 시간에는 니어 프로토콜의 심화 컨트랙트 패턴에 대해 알아보았습니다. 먼저 컨트렉트를 업그레이드하는 방법에 대해 알아보았으며, 다음으로는 Cross contract calls을 하는 법, 마지막으로는 컨트랙트 개발 중 테스트 코드를 작성하고 테스트하는 방법에 대해 알아보았습니다. 이는 DSRV Builder’s House 2일차 세션에서 진행했던 내용과 일맥상통합니다.

1편과 2편을 통해 니어 컨트랙트 개발에 필요한 기본적인 지식을 전달하는 것이 목표이며, 잘 전달되었기를 바랍니다. 긴 글 읽어주셔서 감사하며, 다음 시간에는 다른 글로 또 찾아뵙겠습니다.

💡 이번 밋업을 주최한 Boom Labs는 “크립토/Web3” 최고의 빌더들이 펠로우쉽으로 모여 지식을 공유하고 창의적인 아이디어와 파괴적인 실험을 통해 Web3 업계에 필요한 도구를 함께 만들어가는 빌더들의 커뮤니티입니다. 
Boom Labs에서는 현재 EVM 트랙, zkp 트랙, 온체인 데이터 분석 트랙, 그리고 Rust Contract 트랙 총 4가지를 운영하고 있습니다. 본 밋업은 그 중 Rust Contract 트랙의 일환으로 주최하였습니다. Boom Labs에 대해 알아보고 싶으신 분은 아래 링크를 참고해주세요.
https://linktr.ee/boomlabs

--

--

c0wjay
DSRV
Writer for

Rustacean interested in Programming Languages.