[KO][NEAR 102] 1편: 니어 프로토콜 이해하기 — 메인넷 아키텍처와 RPC 호출

c0wjay
DSRV
Published in
33 min readAug 22, 2022

--

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

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

[NEAR 102 시리즈]

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

시작하기 전에..

이번 미디엄 글에서는 지난 8월 1일과 2일 양일간 DSRV Builder’s House에서 진행하였던 니어 프로토콜 핸즈온 워크샵에서 다뤘던 내용을 설명하고자 합니다. 위 밋업은 Boom Labs와 DSRV의 지원 하에 이루어졌습니다. 또한 워크샵을 준비 및 진행하는 과정에서의 제 개인 회고는 Boom Labs 미디엄 글에서 읽어보실 수 있습니다.

핸즈온 워크샵에 참여하지 않으셨던 분들도, 글을 읽고 따라하며 니어 프로토콜을 이해할 수 있도록 하는 것이 목표입니다.

다만 본 글을 읽으시기 전에, 윤수지님의 [NEAR 101] 1편: NEAR Counter 컨트랙트 톺아보기 글을 먼저 읽고 오시면 이해에 도움이 될 수 있습니다.위 미디엄 글에서 다룬 내용들은 본 글에서는 생략하겠습니다.

본 글에서 사용하는 자료들은 아래에서 확인하실 수 있습니다.

1. 니어 프로토콜의 구조

먼저 NEAR Protocol의 구조에 대해 짚어보고, 실습으로 넘어가겠습니다. 아래 구조도를 보면, 대략적인 구조를 이해하실 수 있을텐데요.

[그림 1–1] 니어 프로토콜 모식도, 출처: NEAR 101 slides

NEAR 블록체인에 배포된 컨트랙트는 near-api-js 프레임워크를 통해 RPC 노드와 소통을 하게 됩니다. 여기서 컨트랙트를 각각 블록체인에 배포한다고 하면, 역시 near-api-js 프레임워크를 통해 해당 코드가 담긴 RPC를 호출하고, 그 결과 NEAR 블록체인 위의 계정에 컨트랙트 코드가 배포됩니다.

NEAR VM은 Blockchain Layer에서의 변화를 읽습니다. 예를 들면, 사용자의 $NEAR 잔고 변화가 그 예시가 될 수 있겠네요. 또한, 컨트랙트의 연산 수행 결과를 Blockchain Layer에 반영하기도 합니다. Ethereum과의 차이점은 컨트랙트 계정이 별도로 상태를 저장하지 않고, 상태를 저장하는 스토리지를 별도로 두고 있다는 점입니다.

여기까지 읽어보시면 Solana와 유사하다고 생각하실 것입니다. Solana 역시 Account가 stateless하므로, 별도의 상태를 저장하는 스토리지가 존재합니다. 사용자는 트랜잭션 비용과 스토리지 비용(Storage Fee)를 별도로 지불하게 됩니다.

여기서 트랜잭션 비용은 해당 코드의 수행에 소모된 컴퓨터의 연산량에 비례하는 값이며, 일회성으로 코드의 수행할 때마다 지불되는 비용입니다. 스토리지 비용은 100kB당 1 NEAR로 책정되어 있으며, 계정이 필요한 스토리지만큼 NEAR를 스테이킹하고 용량을 빌리는 형태입니다. 또한 용량을 줄이면 차액만큼을 돌려받을 수 있습니다. [1, 2, 3]

기존 Ethereum의 단점은 시간이 지날수록 state tree의 용량이 커지기 때문에 Full Node가 부담해야 하는 불필요한 자원의 양이 시간이 지날수록 커진다는 점입니다. 따라서 사용자가 지불해야하는 트랜잭션 비용이 비싸지게 됩니다. NEAR는 Storage와 Contract를 분리하여 이러한 단점을 해결하고 있습니다. 트랜잭션 및 스토리지 비용에 대한 더 자세한 내용은 이 글을 참고하시길 바랍니다.

[그림 1–2] 니어 프로토콜의 state storage, 출처: NEAR 101 slides

참고로, 니어의 스토리지에 저장되는 데이터는 key: value pair의 형태(Map) 또는 key들의 집합(Set)로 저장됩니다. 니어에서 사용되는 자료 구조에 대해서는 이 글을 참고하시길 바랍니다. [4]

💡 DSRV's Tip: HashMap vs LookupMap vs UnorderedMap 이란? [5]

UnorderedMap은 HashMap 자료 구조로 key:value 페어를 저장합니다. Rust standard의 HashMap과의 차이점은, HashMap은 in-memory에 저장하는 자료구조인 반면, UnorderedMap은 영구적인 스토리지에 저장하는 자료구조입니다.
HashMap은 하나의 function call 안에 data collection에 존재하는 모든 원소들을 iterate할 때나, 혹은 data collection에 저장되는 원소의 수가 적거나 고정되었을 경우에 쓰면 좋습니다.
UnorderedMap은 data collection의 원소의 수가 많은 경우, 그리고 function call 동안 그 원소들 중 소수의 일부만 접근해야할 경우에 쓰면 좋습니다.

LookupMap은 Trie 자료 구조로 key:value 페어를 저장합니다. LookupMap은 저장하고 있는 원소들에 대한 메타데이터를 따로 저장하고 있지 않기 때문에, 해당 key들의 벡터를 따로 저장하지 않는 한, 원소들을 iterate하는 것은 불가능합니다. 따라서 key들의 벡터를 따로 저장하는 UnorderedMap에 비해 사용하는 스토리지의 용량도 적습니다.
또한 퍼포먼스 상으로도 UnorderedMap의 경우, value를 얻는 데에 2회의 읽기가 사용되고, 자료 구조에 새 값을 넣는 데에 3회의 쓰기가 사용되는 반면, LookupMap은 양 쪽 모두에 1회의 읽기, 1회의 쓰기만 사용되기 때문에 더 좋습니다.
다만 LookupMap은 key들을 iterate하는 것이 불가능하기 때문에, key들을 iterate할 필요가 없는 경우에 사용하면 효율적입니다. (해당 key의 존재 유무와, 존재할 경우에 key에 대응되는 value를 return하는 행위만 필요할 경우)
💡 DSRV's Tip: 니어 프로토콜의 계정 및 키 구조니어 프로토콜의 계정 및 키 구조는 수지님의 글 말미의 DSRV's Tip 들을 참고하시길 바랍니다. 다만 본 글에서 재차 강조하고 싶은 점은, 여타 다른 메인넷들과는 다른 니어 프로토콜의 계정 설계입니다.
다른 메인넷들의 경우 시드 구문으로 키페어를 만들고, 키페어 중 공개키를 계정의 주소로, 개인키를 계정의 액세스 권한으로 설정하는 것이 보통입니다. 따라서 지갑 주소에는 그에 대응하는 1개의 개인키만 있는 것이 보통이며, 해당 개인키를 갖고있는 사람이 계정의 모든 권한을 갖게 됩니다.
니어는 이와 달리, 시드 구문으로 키 페어를 만드는 행위와 계정을 만드는 행위가 분리되어 있습니다. 니어의 계정은 유저가 정한 닉네임으로 만들 수 있으며, 해당 계정에는 여러 개의 키페어를 연결할 수 있습니다. 하나의 계정에 여러 개의 FullAccess Key가 연결될 수 있으며, 각각의 키들은 모두 해당 계정에 대한 admin 권한을 갖습니다. 또한 하나의 키도 여러 개의 계정에 연결할 수 있습니다.
즉 니어에서는 계정의 권한이 계정 종속적인 것이 아닌, FullAccess Key에 종속적입니다.

참고로, 니어의 계정 생성 과정은 크게 explicit account와 implicit account로 나뉩니다. implicit account의 경우, 계정의 생성 과정 상 유저가 닉네임을 설정할 수 없는 경우에 만들어지는데, 처음 만든 키페어의 공개키를 base58로 인코딩한 값을 계정의 주소로 사용합니다. 니어에 처음인 사람의 경우 implicit account는 이더리움의 계정 구조와 크게 차이가 없는 것으로 혼동할 수도 있지만, 단순히 계정 주소만 human-readable form이 아닐 뿐, 그 기능은 explicit account와 차이가 없습니다.
[그림 1–3] FullAccess Key와 FunctionCall Key 예시, 출처: NEAR Docs
💡 추가적으로, 니어에는 총 8가지의 액션이 존재합니다. CreateAccount, AddKey, DeleteAccount, DeleteKey, Transfer, Stake, DeployContract, FunctionCall 인데요, FullAccess Key는 8가지 액션에 대한 모든 권한을 갖지만, FunctionCall KeyFunctionCall 액션에 대한 권한만 갖습니다.예시로 위 사진에서,
공개키 'ed25519:CM4JtNo2sL3qPjWFn4MwusMQoZbHUSWaPGCCMrudZdDU' 인 키페어의 경우에는, 'tutorial.mike.testnet' 계정으로 하여금 'puzzle.testnet' 컨트랙트 계정 중 'foo'와 'bar' 메소드를 호출할 수 있게 하는 권한만 갖고 있습니다. 이 때, 이 키로 지불할 수 있는 가스비의 최대 한도는 0.25 NEAR (allowance에 저장된 값) 입니다.
참고로, FunctionCall Key로는 Transfer 액션을 수행할 수 없기 때문에, $NEAR를 첨부할 수 없습니다. 따라서 컨트랙트 패턴에서 해당 메소드를 호출하는 주체가 FullAccess Key인지, FunctionCall Key인지 확인하는 방법으로는 assert_one_yocto(); 라는 코드를 라인 중간에 넣고, 메소드를 호출하는 사람에게 1 yoctoNEAR를 첨부하도록 강제하는 방법이 있습니다. [6]

2. 개발 환경 설치

컨트랙트 개발에 필요한 기초적인 이론 지식들은 여기서 마치고, 실습 단계로 넘어가도록 하겠습니다. 그 전에 먼저 개발 환경을 설치하도록 하겠습니다.

본 실습은 Mac OS에서 진행되었으므로, Mac PC 또는 리눅스 환경에서 진행하시는 것을 권장드립니다. 또한 본 실습은 Rust를 기준으로 진행되기 때문에, Rust 언어에 대한 기초 지식이 요구됩니다.

# Node.js 설치
brew install node
npm install --global yarn
# Rust 설치
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh source $HOME/.cargo/env

# Wasm toolchain 추가
rustup target add wasm32-unknown-unknown
# near-cli 설치, node 버전 12 이상 요구됨.
npm install -g near-cli

# near-api-js 설치
npm i --save near-api-js
# ts-node 설치
npm install -g typescript
npm install -g ts-node

위 명령어들을 각각 터미널에 입력하여, 각각의 패키지들을 설치해주시길 바랍니다.

니어 테스트넷 지갑은 본 페이지를 참고하여, 니어 테스트넷 지갑 사이트에서 생성합니다. [7]

본 실습에서 사용할 포스트맨의 경우, 이 글을 참고하여 설정해주세요. [8]

또는 아래 JSON 파일을 복사하여, Postman Import > Raw Text에 붙여넣기하여 진행하셔도 좋습니다.

Rust 에디터의 경우엔, VSCode를 사용했으며, 확장으로는 rust-analyzer, rust syntax, rust extension pack을 사용하였습니다.

git clone https://github.com/boomlabs-web3/learn-near-in-rust.git

마지막으로 실습에 사용할 레포지토리를 클론합니다.

3. RPC Node와 소통

[그림 1–4] RPC 호출 모식도, 출처: NEAR 101 slides

앞서 말했듯이, 클라이언트에서 RPC Node와 소통을 하기 위해서는 near-api-js 프레임워크를 사용해야 합니다. 본 실습에서 사용할 near-cli 뿐만 아니라 NEAR의 웹페이지 월렛, 니어 익스플로러 등등도 모두 내부에서는 near-api-js를 사용하여 RPC Node와 소통을 합니다.

그러면 본격적으로 실습으로 넘어가도록 하겠습니다. 먼저 NEAR 블록체인에 트랜잭션을 어떻게 보내는 지를 알아보도록 하겠습니다. 1 $NEAR를 전송하는 과정을 아래 3가지 방법으로 진행해보며, 그 개념을 익혀보겠습니다.

💡 Mission: boomlabs.testnet으로 1NEAR를 보내기
1) near-cli 이용 (HIGH LEVEL)
2) near-api-js 이용 (MID LEVEL)
3) near-api-jspostman 이용 (LOW LEVEL)

3.1. near-cli 이용하기 (HIGH LEVEL)

[그림 1–5] NEAR CLI 설명, 출처: near-cli 깃헙 레포

실습 전, near-cli에 대해 가볍게 설명하겠습니다.

NEAR CLI는 node.js 어플리케이션으로, 니어 블록체인과의 소통은 역시 near-api-js를 이용하여 소통합니다. 커맨드 라인 인터페이스(CLI) 툴로, 개발자가 near-api-js로 스크립트를 짜지 않더라도, 가벼운 동작들은 터미널에 간단한 명령어를 입력하여 동작할 수 있도록 만들어진 도구입니다.

NEAR CLI 명령어에 대한 정보는 이 문서를 참고해주세요. [9]

near login

위 명령어를 입력하여 near login 과정을 진행합니다. 해당 과정은 윤수지 님의 글에 자세히 정리되어 있습니다.

❗️ near login에서 문제가 생겼나요?Failed to verify accountId. [-32700] Parse error: Failed parsing args: missing field `account_id`위와 같은 오류와 함께, near login이 진행되지 않는다면,
본 스택오버플로우 글을 참고하여, AirPlay Receiver를 꺼주세요.
💡 DSRV's Tip: near login은 어떤 과정인가요?
니어는 크게 3가지 장소에 키를 저장할 수 있습니다. Browser Local Storage, In Memory, 그리고 Unencrypted File System 입니다. [10]
여기서, near-cli를 이용하기 위해서는 Unencrypted File System에 저장된 KeyStore들을 이용하는데요, 그 위치는 맥 OS의 경우엔 ~/.near-credentials 경로입니다.
[그림 1–6] .near-credentials 폴더 예시, 출처: @c0wjay, DSRV
💡 해당 위치로 가면 위 [그림 1-6]처럼, 각 계정 별로 암호화되지 않은 채로 공개키와 개인키 페어가 json 형식으로 저장되어 있음을 확인할 수 있습니다.
[그림 1–7] 브라우저 로컬 스토리지 예시, 출처: @c0wjay, DSRV
💡 브라우저 지갑의 경우에는, 위와 같이 브라우저 로컬 스토리지에 계정 별로 개인키를 저장하고 있습니다.  처음 NEAR를 쓰는 사람의 경우에는, ~/.near-credentials에 아무런 키페어가 저장되어 있지 않기 때문에, near-cli에 사용할 계정의 Access Key를 저장하는 과정이 필요한데요.  near login 커맨드를 통하여, 브라우저 월렛으로 리다이렉트되고, 계정(예: c0wjay_boomlabs.testnet)으로 서명하는 과정을 거치면, c0wjay_boomlabs.testnet 계정에 연결된 새로운 키페어가 생성되고, 해당 키페어는 ~/.near-credentials/testnet/ 위치에 c0wjay_boomlabs.testnet.json 파일명으로 저장됩니다.  참고로 저장되는 키페어는 브라우저 로컬 스토리지에 저장되어 있는 키페어와는 다른, "새로 생성된 키페어"입니다. 또한 near login 과정 없이, 해당 계정에 연결된 키페어 (ex. 브라우저 로컬 스토리지에 저장된 개인키와 대응되는 공개키)를 형식에 맞춰서 매뉴얼하게 ~/.near-credentials/testnet/ 위치의 계정주소.json 파일에 저장하여도, near-cli를 사용하실 수 있습니다.export USER="sender.testnet"
near send $USER boomlabs.testnet 1

near login이 정상적으로 진행되었다면, 위 명령어를 입력하시면 됩니다. 환경변수를 설정하실 때, "sender.testnet"대신 본인의 NEAR 지갑주소를 입력해주세요.

[그림 1–8] near-cli를 통한 1 NEAR 전송 결과, 출처: @c0wjay, DSRV

그러면 위와 같이 아주 간단하게 boomlabs.testnet으로 1 NEAR를 보내게 됩니다.

3.2. near-api-js 이용하기 (MID LEVEL)

다음으로는, 위 과정에서 1NEAR를 보내는 동안 near-cli 내에서 일어나는 일들을 near-api-js를 사용하여 진행해보도록 하겠습니다.

본 실습은 NEAR REPL환경에서 진행하도록 하겠습니다. 이는 near-cli가 제공하는 near-api-js repl 환경으로, line by line으로 그 결과를 터미널에서 실행시킬 수 있어, typescript로 스크립트를 전부 짜서 실행하는 번거로움을 덜어줍니다.

near repl

터미널에 위 명령어를 입력하여 repl 환경에 접속합니다.

NEAR REPL 환경에서는 near-api-js 모듈이 자동으로 import되어 있기 때문에 첫 번째 라인은 건너뛰어도 좋습니다. 두 번째 라인을 통해 near-api-jsconnect, KeyPair, keyStores, utils 모듈을 정의합니다.

다음으로는 위 라인들을 입력합니다. 첫 번째부터 세 번째 라인까지의 역할은, 각 OS별로 설정된 .near-credentials 폴더의 path를 credentialsPath 상수에 정의하는 것이며, 마지막 라인은 위 path ( UnencryptedFileSystem)에 저장되어 있는 키페어들로부터 KeyStore 객체를 만듭니다.

앞서 만든 keyStore 객체 및 노드 url 등을 포함한 connection 설정 값을 만들고, RPC 노드를 통해 NEAR 블록체인에 연결합니다.

위 라인은 RPC 노드에 쿼리를 하여, "sender.testnet"의 account 객체를 돌려받습니다.
여기서 "sender.testnet"은 각자의 계정 주소로 바꿔 입력해주세요.

첫 라인은 1 NEAR를 yoctoNEAR로 환산해주고,

다음 라인을 통해, 위의 "sender.testnet"으로부터 "boomlabs.testnet"으로 1NEAR를 전송하는 트랜잭션을 만들고 RPC 호출로 전송합니다.

위 과정들을 터미널에 아래와 같이 입력하면,

[그림 1–9] near repl 환경에서 1 NEAR 전송 과정 예시, 출처: @c0wjay, DSRV

정상적으로 수행되었다면, 아래와 같이 result 객체를 반환해줍니다.

[그림 1–10] 1 NEAR 전송 결과 예시, 출처: @c0wjay, DSRV

만약, .near-credentials 경로에 저장되어 있는 keyPair가 유효하지 않거나, 혹은 해당 위치에 keyPair가 저장되어 있지 않은 계정을 통해 1 NEAR를 전송하고자 한다면, 마지막 라인에서 아래와 같은 에러가 뜨게 됩니다.

[그림 1–11] 1 NEAR 전송 실패 — KeyNotFound 에러 — 예시, 출처: @c0wjay, DSRV

3.3. near-api-js와 postman 이용하기 (LOW LEVEL)

다음으로는 위 과정에서 wrapping 되었던 near-api-js의 메소드를 조금 더 자세하게 뜯어보고, 마지막으로 트랜잭션에 서명한 결과를 postman을 통해 RPC 호출을 하여 1 NEAR를 보내보도록 하겠습니다.

위 과정은 REPL 환경에서 진행 시 오류가 생기므로, ts 스크립트 파일을 실행하는 방식으로 진행하겠습니다. 예제 코드는 이미 GitHub에 업로드 하였으므로, 2. 개발환경 설치 과정에서 클론하였던 폴더로 이동한 후, 아래 Command를 입력하여 1번 브랜치로 이동해주세요.

git checkout 1.rpc/near-api-js

그러면 해당 폴더에 near-meetup-example.ts 파일이 생긴 것을 확인하실 수 있습니다. 해당 파일의 코드를 라인별로 설명하겠습니다.

먼저 위 라인들을 통해 near-api-jsjs-sha256 모듈을 import합니다.

그리고 위 라인은 RPC 노드의 url을 통해 NEAR RPC provider 객체를 생성합니다.

본 단계에서는 keyStore 모듈을 이용하지 않고, 직접 개인키로부터 키페어를 생성하여 트랜잭션 서명을 진행할 것 입니다. 따라서 .near-credentials/testnet/ 위치로 가셔서, “실습자의 계정 주소”.json 파일에 저장된 Private Key를 복사하여 위 라인에 바꿔 입력해주세요.

다음 라인은 위 privateKey string으로부터 ed25519 알고리즘을 통해 keyPair를 생성합니다.

여기서 'sender.testnet' 대신 실습자의 계정 주소를 바꿔 입력해주세요. 이 라인들의 역할은 실습자의 계정 주소와 공개키를 RPC 노드에 쿼리하여, 이로 부터 accessKey를 받는 과정입니다. 위 과정에서 개인키는 사용되지 않았으므로, 위 과정은 RPC 노드로 부터 블록체인 데이터를 읽기만 하는 과정입니다.

accessKey에 저장된 현재 논스 값을 가져온 후, 1을 더합니다. 논스 값은 각 밸리데이터 노드의 트랜잭션 풀에서 트랜잭션의 순서를 결정하기 위해 사용되는 값으로, 항상 이전 값보다 커야 합니다. 따라서 이전 값보다 적어도 1 이상을 더해주는 것입니다. [11]

다음으로는 1 NEAR를 transfer하겠다는 action을 만듭니다. action에 대한 설명은 1. 니어 프로토콜의 구조를 참고해주세요.

accessKey에 저장된 block_hash 값으로 부터 바이트 배열로 변환하여 recentBlockHash 상수에 저장합니다. 이는 트랜잭션이 생성된지 24시간이 안되었음을 증명하기 위한 용도이므로, accessKey를 받아온 후로부터 24시간 내에 트랜잭션을 생성 및 서명하여 RPC 호출을 해야 합니다.

위의 값들로 부터 트랜잭션 객체를 생성합니다. 이 트랜잭션의 의미는 'sender.testnet' 으로 부터 'boomlabs.testnet' 으로 1 NEAR만큼 전송하며, 이 때 'publicKey' 의 공개키에 해당되는 키의 권한을 사용하겠다는 것을 의미합니다. 따라서 이후 라인에서 트랜잭션에 서명할 때에는 위 'publicKey' 에 대응하는 개인키로 서명해야 합니다.

위의 트랜잭션을 borsh serialization 하여, serializedTx 상수에 저장합니다.

그리고 이를 다시 sha256으로 해싱하여 uint 8 배열의 해시값을 구합니다.

구한 트랜잭션 해시 값을 'publicKey' 에 대응하는 키페어로 서명하여, 서명된 트랜잭션(signedTransaction) 객체를 만듭니다.

마지막으로 위에서 서명된 트랜잭션 객체를 borsh serialize 후, base64로 인코딩하여 RPC 호출을 합니다. 위 과정을 전부 진행한 후, 스크립트 파일을 실행하면 트랜잭션을 자동적으로 RPC 호출까지 진행합니다. 다만 본 실습에서는 postman의 사용법도 잠깐 익힐겸, 위 result 라인은 주석처리하고, 아래와 같이 signedSerializedTx 값을 콘솔에 찍어보도록 하겠습니다.

위 과정들을 토대로, near-meetup-example.ts 파일의 13번째, 17번째 라인을 수정하신 후, 터미널 창에 아래 명령어를 입력해주세요.

ts-node near-meetup-example.ts
[그림 1–12] near-meetup-example.ts 파일 실행 결과, 출처: @c0wjay, DSRV

그러면 위와 같이 “Transaction Results: " 다음에 signedSerializedTx 값을 반환해줍니다. 이 값들을 복사하신 후, 아래와 같이 포스트맨의 import하였던 NEAR RPC Call > Testnet RPC > Body 로 가셔서, params에 복사한 값을 붙여넣기하고 send 버튼을 눌러주세요.

[그림 1–13] 포스트맨을 통한 트랜잭션 전송 예시, 출처: @c0wjay, DSRV

그러면 [그림 1–13]의 하단부처럼, RPC 호출의 결과로 트랜잭션 해시값을 반환해줍니다. 반환된 트랜잭션 해시값을 복사합니다.

아래 예시처럼, https://testnet.nearblocks.io/txns/ 뒤에 붙여서 블록 익스플로러 페이지로 이동하거나, 혹은 블록 익스플로러에 직접 검색해보세요.

[그림 1–14] 블록 익스플로러 결과 예시: https://testnet.nearblocks.io/txns/8fkKQvVbhL2xYCp1hGLtCU8jZJxAjRwdYKcbh9eyKM9k 출처: @c0wjay, DSRV

그러면 boomlabs.testnet으로 1NEAR가 전송되었다는 결과를 익스플로러 상으로도 확인하실 수 있습니다.

4. 예제 Fungible Token 컨트랙트 배포

2번 브랜치의 컨트랙트 구조 분석에서 사용한 예제는 NEAR 101 글에서 다룬 Counter 예제와 동일하며, 해당 글에서 자세히 다루고 있기 때문에, 이번 글에서는 다루지 않도록 하겠습니다. 위 내용이 궁금하신 분들은 수지님의 글을 정독해주시길 부탁드립니다.

다음으로는 정말 간단한 기능만 있는 Fungible Token 컨트랙트를 배포해보는 시간을 갖도록 하겠습니다.

git checkout 3.contract/simple-ft

먼저 터미널에 위 명령어를 입력하여, 3번 브랜치로 이동합니다.

그러면 src/lib.rs 파일에 저장되어 있는 컨트랙트 코드를 확인하실 수 있습니다. 니어 컨트랙트의 기초 구조는 NEAR 101 글에서 상세하게 다루고 있으므로, 생략하겠습니다.

본 컨트랙트 코드는 near_contract_standards crate를 import하고 있습니다.[12]

참고로 FT 스탠다드에 대한 스펙 정리 문서는 이 페이지를 참고해주세요. [13]

Contract struct는 위와 같이 정의되어 있습니다. NEP-141 스탠다드를 따르는 token과, NEP-148 스탠다드를 따르는 token의 metadata가 field에 정의되어 있습니다. 여기서 LazyOption<T>이란, Rust Standard의 Option<T>와 유사한 자료구조입니다.

이 자료구조의 쓰임새는, 너무 큰 용량의 데이터 등을 저장함에 있어서 contract가 항상 해당 데이터를 deserialize하지 않고, 필요 시에만 deserialize하게 만들어줍니다. [5]

즉, 컨트랙트 코드에서 해당 데이터를 조회하는 등의 경우에만 해당 데이터가 deserialize되며, 다른 보통의 경우엔 해당 데이터는 deserialize되지 않습니다. LazyOption은 컨트랙트 initialize 과정에서만 값을 initialize할 수 있습니다.

위 코드는 컨트랙트를 initialize하는 코드입니다. 참고로, #[init] attribute macro 아래의 메소드는 컨트랙트를 배포할 때 --initFunction argument를 통해 호출할 수도 있습니다.

initialization method는 컨트랙트를 먼저 배포한 후, near call 을 통해 해당 메소드를 호출하여 initialization을 진행하거나, 컨트랙트의 배포와 동시에 initialization을 진행할 수도 있습니다. 자세한 내용은 아래 배포하는 과정에서 다시 다뤄보겠습니다.

new methodowner_id, total_supply, metadata를 인자로 받습니다.

그리고, 해당 Contract struct가 이미 initialize 되었는지, 그리고 metadata는 스탠다드에 맞게 valid한지 확인한 후,

Contract struct를 initialize합니다. 이 때 metadata 인자로 받은 값이 있다면, Contract.metadata field에 Some<FungibleTokenMetadata> 형태로 저장합니다.

다음 코드는 owner_idtotal_supply인자로 받은 값을 토대로, 해당 token의 storage에 owner_id를 등록하고, 해당 owner_id가 갖고있는 초기 토큰의 양을 total_supply로 정해주는 코드입니다. 위 메소드는 아래 FT standard 코드에 자세히 명시되어 있습니다.

internal_register_account 메소드는 인자로 받은 account_idLookupMap<AccountId, Balance> 구조의 FungibleToken.accounts field에 key : value = account_id : 0으로 insert 합니다.

insert의 결과로, FungibleToken.accountsaccount_id key가 저장되어 있지 않으면 None을 반환하며, 만약 이미 account_id key가 저장되어 있다면, 해당 key에 대응되는 value를 Some()에 담아 반환합니다.

internal_register_account 메소드는 위에서 만약 Some()에 담겨진 값이 반환된다면, "The account is already registered"라는 메세지와 함께 패닉 오류를 일으킵니다.

internal_deposit 메소드는 FungibleToken.accounts field에 저장된 account_id key에 대응하는 Balance value를 가져와서, 해당 값에 메소드 인자로 받은 amount 만큼을 더합니다. 이 값은 다시 FungibleToken.accounts field에 저장된 account_id key에 대응하는 value로 저장하고, FungibleToken.total_supply field에도 기존 값에 amount만큼 더해서 추가하되, 만일 total_supply 값이 overflow 된다면 "Balance overflow" 란 메세지와 함께 패닉을 야기합니다.

insert 메소드는 LookupMap의 implementation에 자세히 명시되어 있습니다.

다시 learn-near-in-rust 레포의 예시 FT 컨트랙트 코드로 돌아오면, 다음 라인은 아래와 같습니다. 이는 Event emit과 관련된 코드입니다.

Event Emit의 경우, NEP-297 스탠다드의 확장으로, 니어 블록체인이나 써드파티 어플리케이션이 FT의 mint, transfer, burn 이벤트들을 트래킹할 수 있도록 블록체인 위에 로깅을 하는 코드입니다.

써드파티 사용자들은 NEAR indexer 또는 NEAR Lake 프레임워크 등을 통해 온 체인상의 위 이벤트 등을 트래킹할 수 있습니다. [14]

FT의 기능에는 큰 영향을 주지 않기 때문에, 본 실습에서는 생략하도록 하겠습니다.

코드의 마지막 부분에는 ft_metadata 메소드가 정의되어 있습니다. “m”이라는 storage_key로 저장되어 있는 metadata를 아래 FungibleTokenMetadata struct로 deserialize하여 반환해주는 코드입니다.

get 메소드는 아래 LazyOption의 implementation에 자세히 명시되어 있습니다.

다른 기능 없이, 기본적인 기능만 정의하여 FT 컨트랙트를 구성해 보았습니다.

물론 컨트랙트 코드에는 정의되지는 않았지만, 아래 FungibleToken 스탠다드ft_transfer라던지, ft_transfer_call, ft_total_supply, ft_balance_of 메소드 역시 사용 가능합니다.

그러면 실제로 FT 컨트랙트를 배포해보겠습니다.

export USER="sender.testnet"

환경 변수로 본인 계정 주소를 설정해주세요.

yarn build && near deploy --accountId $USER --wasmFile export/main.wasm
near call $USER new '{"owner_id": "'$USER'", "total_supply": "1000", "metadata": { "spec": "ft-1.0.0", "name": "BOOM LABS TOKEN", "symbol": "BOOM", "decimals": 8 }}' --accountId $USER

yarn build./export/ 경로에 main.wasm 파일을 컴파일하여 저장합니다. near deploy 명령어로는 저장된 main.wasm 파일을 본인 계정 주소에 배포합니다. 참고로 yarn build 명령어는 아래 bash script를 실행합니다.

#!/usr/bin/env bash

set -e && RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release && mkdir -p ./export && cp target/wasm32-unknown-unknown/release/*.wasm ./export/main.wasm

dev-deploydeploy의 차이점은 NEAR 101 글을 참고해주세요.

near call 명령어로 컨트랙트의 new 메소드를 호출하여, 다음과 같은 인자를 넘기며 컨트랙트를 Initialization 합니다.

"owner_id": "'$USER'", "total_supply": "1000", "metadata": { "spec": "ft-1.0.0", "name": "BOOM LABS TOKEN", "symbol": "BOOM", "decimals": 8 }

위 과정은 아래 명령어와 같이 배포와 동시에 진행할 수도 있습니다.

yarn build && near deploy --wasmFile export/main.wasm --initFunction 'new' --initArgs '{"owner_id": "'$USER'", "total_supply": "1000", "metadata": { "spec": "ft-1.0.0", "name": "BOOM LABS TOKEN", "symbol": "BOOM", "decimals": 8 }}' --accountId $USER

그리고 니어 테스트넷 지갑으로 이동하시면 아래와 같이 BOOM LABS TOKEN을 확인하실 수 있습니다.

[그림 1–15] 니어 테스트넷 지갑에 저장된 토큰 예시, 출처: @c0wjay, DSRV
near view $USER ft_metadata ''
near view $USER ft_balance_of '{"account_id":"'$USER'"}'
near view $USER ft_total_supply ''

또한 위 명령어를 통해 토큰의 메타데이터나, $USER 계정이 소유하고 있는 토큰의 수, 및 전체 토큰 공급 수를 확인할 수 있습니다.

[그림 1–16] near-cli를 통한 토큰 데이터 조회 예시, 출처: @c0wjay, DSRV
export RECEIVER="receiver.testnet"
near call $USER storage_deposit '{"account_id": "'$RECEIVER'"}' --accountId $USER --amount 0.00125
near call $USER ft_transfer '{"receiver_id": "'$RECEIVER'", "amount": "100"}' --accountId $USER --amount 0.000000000000000000000001

그리고 위 명령어를 입력하여 receiver.testnet 계정으로 토큰 100개를 전송할 수도 있습니다.

[그림 1–17] near-cli를 이용한 토큰 전송 예시, 출처: @c0wjay, DSRV

글을 마무리하며

이번 시간에는 니어 프로토콜의 아키텍처에 대해 간단히 알아봤으며, 실습 과정을 통해 니어에서의 트랜잭션을 생성하는 과정과, 컨트랙트 배포 과정에 대해 알아보았습니다. 이는 DSRV Builder’s House 1일차 세션에서 진행했던 내용과 일맥상통합니다.

다음 시간에는 1편의 마지막에 배포하였던 FT 컨트랙트를 업그레이드를 해보는 과정을 통해 Contract Upgrade 및 Schema Migration에 대해 배워보겠습니다.

해당 컨트랙트와 소통하는 컨트랙트를 배포하여 Cross Contract Calls에 배우고, 마지막으로 니어 프로토콜의 컨트랙트를 테스트하는 방법에 대해서도 배워볼 예정입니다.

긴 글 읽어주셔서 감사합니다.

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

--

--

c0wjay
DSRV

Rustacean interested in Programming Languages.