본 글은 freeCodeCamp.Org의 Youtube 영상 'Solidity, Blockchain, and Smart Contract Course – Beginner to Expert Python Tutorial'와 관련 코드인 SmartContract의 Github 코드를 기초로 작성되었다.
Youtube 영상 링크: https://www.youtube.com/watch?v=M576WGiDBdQ&t=10336s
Github 코드 링크: https://github.com/smartcontractkit/full-blockchain-solidity-course-py
오늘의 코드: https://github.com/PatrickAlphaC/brownie_simple_storage
이번 포스팅은 유튜브 영상 04:27:55~05:06:34에 해당하는 내용이다.
Web3 기반으로 스마트 컨트랙트를 컴파일, deploy, 실행까지 해봤던 지난 시간의 코드가 복잡하고 비효율적으로 느껴지지 않았는가? 필자는 너무 복잡하고 비효율적이라고 느꼈다. Web3를 보다 효율적이고 간편하게 사용할 수 있는 도구 중 하나가 Brownie이다. 이번 시간에는 Brownie라는 Web3 기반 스마트 컨트랙트 플랫폼을 이용하여 Python 상에서 스마트 컨트랙트를 구현해보자.
cd ..
mkdir brownie_simple_storage
cd brownie_simple_storage
터미널에 위 코드를 입력하여 다시 demos 폴더로 돌아가 brownie_simple_storage라는 폴더를 추가하고 그 폴더에 위치하자.
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx를 통해 brownie를 설치하자. 위 두 코드를 터미널에 입력 후 Visual Studio Code를 껐다가 다시 켜자.
pipx install eth-brownie
위 코드를 입력하면 Brownie가 설치된다.
brownie init
위 코드를 입력하면 brownie_simple_storage 하에 여러 폴더가 생성되었음을 볼 수 있다. 폴더의 명칭을 보면 각 폴더에 어떠한 내용이 들어갈지 짐작이 가능하다.
Contracts 폴더에 SimpleStorage.sol을 만들고 그 내용을 예전과 동일하게 입력 및 저장해주자. 여기까지 완료됐으면 Explorer가 다음과 같이 구성되었을 것이다.
brownie compile
위 코드를 입력하면 SimpleStorage가 json으로 컴파일된다. 이는 build의 contracts 폴더에서 확인할 수 있다. 이전 시간에 진행한 컴파일 방법에 비하면 컴파일 방법이 매우 간단해졌다.
deploy를 시행하기 전 ganache-cli라는 블록체인 툴을 설치하자. ganache-cli는 블록체인 에뮬레이터이다. ganache-cli를 활용해 단일 노드 기반으로 블록체인 테스트를 진행할 수 있다.
ganache-cli 설치를 위해서는 먼저 node.js를 설치해야 한다. 아래 사이트에서 다운로드 및 설치를 진행해주자.
https://nodejs.org/ko/
npm install -g ganache-cli
node.js 설치가 끝났으면 위 코드를 터미널에 입력하여 ganache-cli 설치를 진행해주자.
(필자는 goorm 컨테이너에 node.js와 ganache-cli 설치를 해야 했는데, 이 과정이 만만치 않았다... 사지방을 벗어나 하루빨리 노트북이 있는 집으로 돌아가고 싶다는 충동이 또 강하게 들었다.)
본격적인 deploy 전 테스트를 하나 진행해보자. brownie에서는 deploy 전 테스트를 해볼 수 있는 기회를 제공한다. tests폴더에 들어가 'test_simple_storage.py'라는 이름의 파일을 하나 만들자.
from brownie import SimpleStorage, accounts
brownie에서는 위와 같이 contracts 폴더에 있는 계약을 가져올 수 있다. SimpleStorage를 가져왔다.
accounts는 계좌 배열로 ganachi-cli에서 제공하는 계좌이다. 이 계좌 배열의 계좌에 대해 실제 계좌처럼 에뮬레이션을 진행할 수 있다.
def test_deploy():
# Arrange
account = accounts[0]
# Act
simple_storage = SimpleStorage.deploy({"from": account})
starting_value = simple_storage.retrieve()
expected = 0
# Assert
assert starting_value == expected
'def 함수:
함수 내용~~'은 Python에서 함수를 정의하는 방법이다. 위 예시의 경우 test_deploy함수가 #Arrange부터 assert starting_value == expected까지 함수 내용으로 삼고 있다.
#은 파이썬에서 주석을 다는 기호이다. #을 입력하면 그 줄의 내용은 주석처리되어 컴파일 대상이 되지 않는다.
test는 보통 3단계에 걸쳐 이루어진다. Arrange, Act, Assert이다. 즉, test는 실행에 필요한 준비를 갖추고(Arrange), 실행을 한 후(Act), 확인을 하는(Assert) 과정이다.
Arrange 단계
위 예시에서는 accounts로부터 계좌를 하나 가져와서 이를 account에 저장함으로써 deploy를 준비한다.
Act 단계
SimpleStorage.deploy({"from": account})라는 단순한 함수로 deploy가 진행된다. 저번 시간과는 비교도 안 되게 deploy 코드가 간략해졌다. '계약명.deploy({"from": 실행 계좌})'를 입력하면 deploy가 진행된다. deploy된 계약을 simple_storage에 저장했다.
'계약.계약 내 함수' 형식으로 계약 내 함수 또한 brownie에서는 쉽게 불러올 수 있다. simple_storage 내 retrieve 함수를 call하여 반환값을 starting_value에 저장하고 있다.
유의할 점 하나는 call 함수의 인자에는 {"from": 실행 계좌}를 전달하지 않는 반면 transact 함수의 인자에는 {"from": 실행 계좌}을 전달한다는 점이다. 전자는 호출해도 체인 상에 기록되지 않는 반면 후자는 기록되기 때문이다.
예상값, 즉 expected 변수에 0을 저장하고 있다.
Assert 단계
'assert 내용'은 내용이 참인지 거짓인지 판별한다. 코드에서는 starting value와 expected의 값이 동일한지 확인하고 있다.
def test_updating_storage():
# Arrange
account = accounts[0]
simple_storage = SimpleStorage.deploy({"from": account})
# Act
expected = 15
txn = simple_storage.store(expected, {"from": account})
txn.wait(1)
# Assert
assert expected == simple_storage.retrieve()
다음으로 test_updating_storage라는 함수를 정의해보자.
Arrange 단계
test_deploy에서와 마찬가지로 가장 먼저 accounts배열의 첫 번째 계좌를 account에 저장한다. 다음으로 SimpleStorage를 deploy한 계약을 simple_storage에 저장한다.
Act 단계
expected에 15를 저장한다.
simple_storage의 store함수에 expected를 인자로 전달하고 있다. 이때 store함수가 transact 함수이므로 account 정보 또한 전달하고 있다. 이 함수에 의해 favoriteNumber에 15가 저장된다.
txn.wait(1)을 통해 transaction을 1초 유예하고 있다.(영상에는 txn을 정의하지 않고, wait 함수도 넣지 않았다. 'transaction.wait(초)'를 하면 해당 transaction이 입력된 초만큼 유예된다.)
Assert 단계
expected의 값이 favoriteNumber에 저장되었는지 retrieve 함수를 통해 확인하고 있다.
brownie test
터미널에 위 코드를 입력하면 tests 폴더 안 파일의 함수들이 assert되는지 여부를 확인할 수 있다. 함수 몇 개가 pass되고, 몇 개가 fail되었는지 알려준다.
brownie test -k test_updating_storage
brownie test --pdb
brownie test -s
위 코드는 각각 test와 관련된 코드이다.
첫 줄은 특정 함수(예시에서는 test_updating_storage)를 선택하여 test를 진행해볼 수 있는 코드이다.
둘째 줄은 test에서 오류가 발생했을 때 찾아내기 위한 코드이다. 이 코드를 입력하면 pdb창이 열리는데, 변수를 입력하면 변수 내용이 출력된다. 여러 변수에 대해 반복 가능하다.
셋째 줄은 test에 관한 보다 디테일한 정보를 제공한다. 어떠한 함수가 pass되고, fail되는지 표시해준다.
아래 사진은 test_deploy 함수의 expected를 15로 변경한 후 pdb창에 여러 값을 입력해본 결과이다.
scripts에서 본격적으로 스마트 컨트랙트를 deploy하기 전에 준비해야하는 과정이 몇 가지가 있다. 먼저 중요한 변수들을 환경변수화하기 위해 .env 파일을 추가해주자. 그리고 .env 파일 내에 다음 코드를 추가해주자.
export PRIVATE_KEY = 0xb5e857091a491a306f8a13ac49ed53655f51c2d780db0e9faf0305878b3f8fe5
export WEB3_INFURA_PROJECT_ID=a9c5bc0ec75c4a83a3e48086df81acfe
PROJECT_ID는 이전 시간의 RPC URL처럼 원하는 블록체인에 연결하기 위해 필요하다. Infura에 들어가면 다음과 같이 제공된다. API KEY가 PROJECT_ID이다.
다음으로 brownie-config.yaml이라는 파일을 추가하고 다음 코드를 입력해주자.
dotenv: .env
wallets:
from_key: ${PRIVATE_KEY}
해당 파일에 적힌 내용은 brownie 실행에 반영된다. Python 스크립트에 config를 가져오면 해당 파일의 내용이 모두 반영된다.
.env의 내용이 반영되어 export PRIVATE_KEY와 export WEB3_INFURA_PROJECT_ID가 유효해진다.
yaml 파일에서는 '${내용}'이 내용을 환경변수로 전환한 값이 반환된다.
자! 이제 script에 deploy.py라는 파일을 입력하고 코드를 추가해보자.
from brownie import accounts, config, SimpleStorage, network
def deploy_simple_storage():
account = get_account()
simple_storage = SimpleStorage.deploy({"from": account})
stored_value = simple_storage.retrieve()
print(stored_value)
transaction = simple_storage.store(15, {"from": account})
transaction.wait(1)
updated_stored_value = simple_storage.retrieve()
print(updated_stored_value)
def get_account():
if network.show_active() == "development":
return accounts[0]
else:
return accounts.add(config["wallets"]["from_key"])
def main():
deploy_simple_storage()
앞서 test 스크립트를 분석할 때 코드의 각 줄이 어떻게 동작하는지는 대부분 파악했다. 따라서 이번에는 deploy.py라는 파일의 스크립트 전체를 보면서 이 스크립트의 작동 방식을 파악해보자.
main함수에서는 deploy_simple_storage 함수를 호출하고 있다.
deploy_simple_storage는 get_account 함수를 호출하여 반환값을 account에 저장한다.
get_account 함수
get account 함수에는 생소한 구문이 등장하여 따로 볼 필요가 있다.
if-else 구문이 등장하였다.
if 조건식:
문장1
else:
문장2
위와 같은 형식의 코드에서 조건식이 참이면 문장1을, 거짓이면 문장2가 실행된다.
deploy.py 첫 줄에 보면 network를 import했다. network를 통해 network에 대한 정보를 쉽게 얻고, network를 쉽게 활용할 수 있다.
network.show_active()는 현재 연결된 network를 반환한다.
development network는 ganache-cli와 같이 프로그램 종료 후 사라지는 일시적 블록체인 네트워크를 의미한다. 반면 Rinkeby와 같은 Ethereum 블록체인은 영구적이다.
따라서 위 예시의 경우 ganache-cli와 같은 development 네트워크와 연결되어 있을 때 account 배열의 첫 요소를 반환하고, 이외의 경우에 brownie-config.yaml에 정의된 PRIVATE_KEY를 가져와 그 KEY에 해당하는 주소를 반환한다.
deploy_simple_storage
다시 deploy_simple_storage 함수로 돌아와서 코드를 보자.
SimpleStorage를 deploy해서 초기에 저장된 favoriteNumber의 값을 프린트하고 있다.
그 후 15를 favoriteNumber에 저장하는 transaction을 호출하고 1초의 유예시간을 두고 있다.
마지막으로 저장된 favoriteNumber를 출력하면서 함수가 마무리된다.
brownie run scripts/deploy.py
brownie run scripts/deploy.py --network rinkeby
터미널에 첫 줄을 입력하면 ganache-cli 기반으로 스크립트가 실행된다.
둘째 줄을 입력하면 rinkeby 네트워크 기반으로 스크립트가 실행된다. 실행 후 해당 transaction을 rinkeby etherscan에서 찾을 수 있다.
두 번째는 실제 블록체인상의 거래이니 첫 번째보다 실행에 조금 더 오랜 시간이 걸린다.
아래 사진은 rinkeby 네트워크 기반의 스크립트 실행 결과이다.
from brownie import SimpleStorage, accounts, config
def read_contract():
simple_storage = SimpleStorage[-1]
print(simple_storage.retrieve())
def main():
read_contract()
마지막으로 간단한 코드 하나만 확인하자. read_value.py를 scripts에 추가하고 위 코드를 입력해주자. SimpleStorage는 하나의 계약 배열이다. 그리고 배열[-1]을 입력하면 가장 최근에 deploy된 계약을 반환한다. 위 코드에서는 가장 최근에 deploy된 SimpleStorage에 저장된 favoriteNumber값을 출력하고 있다.
이번주도 2개의 포스팅에 성공하였다! 이번주도 보람찼다! ㅎㅎㅎ
오늘은 리트 시험이 있는 날이었다. 내 주변 사람들도 많이 봤는데 다들 정말 고생했다는 말 남겨주고 싶다. 꿈을 향해 인내하고 노력하면서 한 단계씩 실력을 만들어나가는 삶이 정말 의미 있고 멋진 삶인 듯하다. 오늘 그 인내와 노력의 결과가 다들 터져나왔길 기대한다. 포스팅을 통해 실력을 쌓아가는 나 자신에게도 수고했고 멋지다는 말 남기고 싶다. 다음 포스팅에서 보자!!!
Github Code License:
MIT License
Copyright (c) 2021 SmartContract
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.