이 튜토리얼은 Python 스크립트를 NeoVM 호환 코드로 컴파일하는 도구인 Neo3-boa를 사용하여
대체 가능한 토큰을 만드는 방법을 보여줍니다. 이 토큰은 NEP-17 표준을 준수합니다.
NEP-17 표준은 토큰 컨트랙트가 Neo 블록체인에서
유효한 대체 가능한 토큰으로 간주되기 위해 따라야 하는 규칙 집합을 정의합니다.
이 표준은 컨트랙트가 구현해야 하는 메서드, 이벤트 및 콜백을 정의합니다. NEP-17 표준은
이더리움의 ERC-20 표준과 동등합니다.
컨트랙트가 표준을 따를 때, 다른 컨트랙트와 애플리케이션은 구현 세부사항을 알 필요 없이
상호작용할 수 있습니다. 이를 통해 다른 개발자들이 상호 운용 가능한 컨트랙트와 애플리케이션을
쉽게 만들 수 있습니다.
이 튜토리얼은 Python 3.7 이상 버전,
Visual Studio Code,
Python VS Code 확장, 그리고
Neo Blockchain Toolkit VS Code 확장이
설치되고 올바르게 구성되어 있다고 가정합니다.
Neo Blockchain Toolkit을 올바르게 구성하려면 튜토리얼을 따라야 합니다.
프로젝트용 새 폴더를 만들고 VS Code에서 열어주세요. 그런 다음 requirements.txt라는 파일을 만들고
그 안에 neo3-boa==1.0.0을 작성하세요.
Ctrl+Shift+P를 눌러 명령 팔레트를 열고 Python: Create Environment를 입력한 다음, Venv를 선택하고,
Python 버전을 선택하고, requirements.txt 파일을 선택한 후 Ok를 눌러 가상 환경을 생성하세요.
Ctrl+Shift+P를 누르고 Terminal: Create New Terminal을 입력하여 새 터미널을 만드세요. 터미널 줄의
시작 부분에 (.venv)가 표시되어야 합니다. 가상 환경이 활성화된 경우에만 컨트랙트를 컴파일할 수 있습니다.
참고: PowerShell 터미널을 사용하는 경우
Activate.ps1 cannot be loaded because running scripts is disabled on this system라는
오류가 발생할 수 있습니다. 이 경우 터미널 탭의 + 기호 오른쪽에 있는 ⌄ 아이콘을 클릭하고
Command Prompt를 선택하여 cmd 터미널을 사용하면 됩니다. 모든 것이 정상이면 터미널 줄의 시작 부분에(.venv)가 표시됩니다.
또한 Python 파일이 열려 있다면 VS Code의 오른쪽 하단 모서리를 확인하여 가상 환경이 활성화되어 있고
Python 3을 사용하고 있는지 확인할 수 있습니다.

coin.py라는 새 파일을 만들고 컨트랙트에 1억 개의 토큰이 존재할 것임을 나타내는 다음 메서드를 추가하세요:
from boa3.builtin.compile_time import public
@public(name='totalSupply', safe=True)
def total_supply() -> int:
return 10 ** 8
VS Code 터미널을 열고 다음 명령을 실행하여 컨트랙트를 컴파일하세요:
neo3-boa compile coin.py --debug
Neo Blockchain Toolkit에 필요한 디버그 정보를 생성하기 위해 --debug 플래그를 포함해야 합니다.
컴파일이 성공하면 3개의 새 파일이 생성됩니다:
coin.nef: 컨트랙트 바이너리 파일 (바이트코드);coin.manifest.json: 컨트랙트 매니페스트 파일. 이 파일은 컨트랙트를 배포하는 데 필요하며 nef와 함께coin.nefdbgnfo: 디버그 정보 파일. 이 파일은 Neo Blockchain Toolkit이 컨트랙트를 디버그하는 데 필요합니다.생성된 .nef 또는 .manifest.json 파일을 클릭하고 Ctrl+Shift+D를 눌러 실행 및 디버그 패널을 엽니다.
create a launch.json file을 클릭하고 환경으로 Neo Contract를 선택하세요.

이렇게 하면 프로젝트 폴더 내에 .vscode/launch.json 파일이 생성됩니다. 파일을 열고 operation
필드 값을 totalSupply로 바꾸세요:
...
"invocation": {
"operation": "totalSupply",
"args": []
}
...
다음으로 F5를 눌러 디버깅을 시작하세요. Neo Blockchain Toolkit은 컨트랙트 배포를 시뮬레이션하여
테스트 환경에서 totalSupply 메서드를 호출할 수 있게 합니다. 이 환경은 디버그 세션이 끝난 후에는
지속되지 않습니다. 따라서 컨트랙트 스토리지를 변경하는 메서드를 테스트하려면 컨트랙트를 로컬 neo-express
인스턴스나 테스트넷에 배포해야 합니다.
totalSupply 메서드를 디버깅하는 것은 그리 유용하지 않지만, 환경이 제대로 작동하는지 테스트하는
좋은 방법입니다. 디버깅 후에는 디버그 콘솔에서 소비된 GAS와 메서드의 반환값을 보여주는 메시지를 받게 됩니다.
구현할 일부 메서드를 테스트하려면 스토리지에 일부 데이터가 있는 것이 중요합니다. 따라서 컨트랙트가
배포될 때 스토리지에 일부 정보를 저장하겠습니다.
Neo에는 가상 머신에 의해 자동으로 호출되는 일부 메서드가 있습니다. 예를 들어, 컨트랙트가 배포되거나
업데이트될 때마다 호출되는 _deploy 메서드가 있으며, 이는 모든 종류의 스마트 컨트랙트에서 자주 사용됩니다.
이 메서드를 사용하여 컨트랙트 스토리지를 초기화하고, 모든 토큰을 스마트 컨트랙트를 배포한 사람에게
주겠습니다. 이를 위해 runtime.script_container를 사용하여 발신자의 스크립트 해시를 가져오겠습니다.
그런 다음 storage.put 메서드를 사용하여 발신자를 전체 총 공급량의 소유자로 스토리지에 저장합니다.
우리의 전략은 소유자를 키로 하고 그들이 소유한 토큰 수량을 스토리지의 값으로 하는 것입니다.
# coin.py에 다음 코드를 추가하여 업데이트:
from typing import Any
from boa3.builtin.contract import Nep17TransferEvent
from boa3.builtin.interop import runtime, storage
from boa3.builtin.interop.blockchain import Transaction
@public
def _deploy(data: Any, update: bool):
# 컨트랙트가 처음 배포되는 경우, 모든 토큰을 컨트랙트 배포자에게 추가
if not update:
container: Transaction = runtime.script_container
storage.put(container.sender, total_supply())
# 토큰이 발행되었음을 알리기 위해 Transfer 이벤트를 트리거, 이는 NEP-17 요구사항입니다
# NEP-17 이벤트에 대한 자세한 내용은 여기를 확인하세요: https://github.com/neo-project/proposals/blob/master/nep-17.mediawiki#events
Nep17TransferEvent(None, container.sender, total_supply())
밑줄로 시작하는 메서드는 사용자가 호출할 수 없습니다. 이 예제에서 _deploy 메서드는
컨트랙트가 배포될 때 자동으로 호출됩니다.
NEP-17 표준은 토큰 컨트랙트가 구현해야 하는 5개의 필수 메서드를 정의합니다:
토큰 심볼을 반환합니다. 이 예제에서는 문자열 COIN을 반환합니다.
# coin.py에 다음 코드를 추가하여 업데이트:
@public(safe=True)
def symbol() -> str:
return "COIN"
다른 컨트랙트나 사용자가 사용할 메서드를 작성할 때는 public 데코레이터를 사용해야 합니다.
safe 매개변수는 메서드를 호출할 때 컨트랙트 스토리지를 변경하지 않으며 다른 컨트랙트나
사용자가 안전하게 호출할 수 있음을 나타냅니다.
추가될 새 메서드 중 하나를 실행하려면 Neo3-boa를 사용하여 스마트 컨트랙트를 다시 컴파일하고
.vscode/launch.json 파일의 operation을 실행하려는 메서드 이름으로 변경해야 합니다.
컨트랙트에서 반환되는 문자열은 기본적으로 16진수 문자열로 표시되므로, operation을 symbol로만
변경하여 테스트를 실행하면 COIN 대신 434f494e이 반환됩니다. 하지만
"return-types"
구성을 추가하여 읽기 쉬운 문자열로 캐스팅할 수 있습니다.
...
"invocation": {
"operation": "symbol",
"args": []
},
"return-types": [
"string",
],
...
토큰에서 사용되는 소수점 자릿수를 반환합니다.
이는 토큰 잔액을 표시할 때 소수점 정밀도를 제공하는 데 사용됩니다. Neo는 부동소수점 타입을 지원하지 않기 때문입니다.
부동소수점 타입은 종종 신뢰할 수 없기 때문입니다. 이 예제에서는 2를 반환하지만, 실제 상황에서는 더 큰 소수점 수를 원할 것입니다.
# coin.py에 다음 코드를 추가하여 업데이트:
@public(safe=True)
def decimals() -> int:
return 2
토큰의 총 공급량을 반환합니다. 이 메서드는 이미 이전에 구현했지만, 소수점이 2자리로 정의되었으므로
공급량에 10 ** 2를 곱해야 합니다.
# coin.py를 다음 코드로 total_supply 메서드를 덮어써서 업데이트:
@public(name='totalSupply', safe=True)
def total_supply() -> int:
return (10 ** 8) * 10 ** decimals()
name 매개변수는 이 메서드가 어떻게 호출될지를 정의하는 데 사용됩니다. Neo의 첫 번째 지원 언어가 C#, VB.Net, F#,
Java, Kotlin이었기 때문에 Neo 표준 메서드의 명명 규칙은 snake_case가 아닙니다. 하지만 Neo3-boa는 필요할 때
public 데코레이터에 name 매개변수를 추가하는 한 snake_case 메서드로 더 파이썬다운 코드를 작성할 수 있게 해줍니다.
특정 주소의 토큰 잔액을 반환합니다. 모든 토큰은 컨트랙트 스토리지에 저장되고 주소와 연결되어야 하며,
스토리지에 액세스하기 위해 storage.get 메서드를 사용할 것입니다.
이 스마트 컨트랙트에서는 주소가 가진 토큰 수에 액세스하는 키가 주소의 스크립트 해시(UInt160 타입으로 표현)가 되도록 만들었습니다.
storage.get 메서드는 bytes 값을 반환하므로 반환하기 전에 int로 변환해야 합니다.
# coin.py에 다음 코드를 추가하여 업데이트:
from boa3.builtin.type import UInt160, helper as type_helper
@public(name='balanceOf', safe=True)
def balance_of(account: UInt160) -> int:
assert len(account) == 20 # NEP-17은 주소가 20바이트 길이여야 함을 요구합니다
amount_in_bytes = storage.get(account)
return type_helper.to_int(amount_in_bytes)
매개변수가 필요한 메서드를 실행하려면 .vscode/launch.json 파일의 args 목록에 매개변수를 추가해야 합니다.
예를 들어 balanceOf 메서드를 실행하려면 잔액을 확인하려는 주소를 추가해야 합니다.
서명자를 정의하지 않았기 때문에,
스마트 컨트랙트를 배포할 계정은 0x0000000000000000000000000000000000000000이며 이것이
0이 아닌 잔액을 가진 유일한 계정입니다.
...
"invocation": {
"operation": "balanceOf",
"args": [ "0x0000000000000000000000000000000000000000" ]
},
...
발신자로부터 지정된 주소로 일정 수의 토큰을 전송하며, 선택적 데이터 매개변수를 전달합니다.
이 예제에서는 데이터 매개변수를 사용하지 않지만, NEP-17 표준에서 요구합니다. 이 메서드는 아래 코드의
주석에 자세히 설명된 많은 특수성이 있습니다.
# coin.py에 다음 코드를 추가하여 업데이트:
from boa3.builtin.interop import blockchain, contract
@public
def transfer(from_address: UInt160, to_address: UInt160, amount: int, data: Any) -> bool:
# NEP-17은 주소가 20바이트 길이여야 함을 요구합니다
assert len(from_address) == 20
assert len(to_address) == 20
# 또한 금액이 0보다 크거나 같아야 함을 요구합니다
assert amount >= 0
tokens_sender = balance_of(from_address)
tokens_receiver = balance_of(to_address)
# 토큰을 전송하려면 발신자가 인증되어야 합니다.
# `check_witness` 메서드를 사용하여 발신자가 토큰의 소유자인지 확인합니다.
if not runtime.check_witness(from_address):
return False
if tokens_sender < amount:
return False
# `amount`가 0이거나 발신자가 수신자와 같은 경우, 스토리지를 변경할 필요가 없습니다
if amount != 0 and from_address != to_address:
# `storage.put` 메서드를 사용하여 스토리지를 변경합니다
storage.put(from_address, type_helper.to_bytes(tokens_sender - amount))
storage.put(to_address, type_helper.to_bytes(tokens_receiver + amount))
# 메서드가 성공하면 Transfer 이벤트를 발생시켜야 합니다
# 자세한 내용은 여기를 확인하세요: https://github.com/neo-project/proposals/blob/master/nep-17.mediawiki#transfer-1
Nep17TransferEvent(from_address, to_address, amount)
# to_address가 스마트 컨트랙트인 경우, onNEP17Payment를 호출해야 합니다
if blockchain.get_contract(to_address) is not None:
contract.call_contract(to_address, 'onNEP17Payment', [from_address, amount, data])
return True
transfer 메서드는 스토리지의 값을 변경하므로 safe로 플래그를 지정할 수 없습니다.
transfer 메서드는 NEP-17 표준의 가장 복잡한 메서드입니다. 가장 많이 디버깅해야 할 메서드입니다.
하지만 함수의 끝에 도달하여 True를 반환하려면 check_witness 함수를 통과해야 합니다.
따라서 호출할 때 서명자를 추가하거나
check_witness를 수행할 때 항상 True를 반환하도록 런타임 속성을 추가할 수 있습니다.
아래 예제에서는 0x00000... 주소에서 0x99999... 주소로 5.00 토큰을 전송하고 있습니다.
...
"invocation": {
"operation": "transfer",
"args": [
"0x0000000000000000000000000000000000000000",
"0x9999999999999999999999999999999999999999",
500,
null
]
},
"runtime": {
"witnesses": {
"check-result": true
}
}
...
디버거는 단순히 호출을 시뮬레이션하므로 스토리지에 대한 변경사항은 디버그 세션이 끝난 후에는
지속되지 않습니다. 따라서 나중에 0x99999... 주소의 잔액을 확인하려고 하면 500을 반환하지 않습니다.
NEP-17 표준은 수신자가 컨트랙트인 경우 호출되어야 하는 onNEP17Payment라는 단일 콜백을 정의합니다.
이 콜백은 수신자에게 토큰을 받았음을 알리는 데 사용됩니다. 이 콜백을 구현하는 것은 수신자의 몫입니다.
수신자는 예외를 발생시켜 전송을 거부할 수 있습니다.
# coin.py에 다음 코드를 추가하여 업데이트:
from boa3.builtin.contract import abort
@public(name='onNEP17Payment')
def on_nep17_payment(from_address: UInt160, amount: int, data: Any):
abort() # 이 예제에서는 스마트 컨트랙트가 자신에게 전송된 모든 전송을 거부합니다
Neo3-boa는 metadata 데코레이터를 사용하여 컨트랙트 메타데이터를 정의할 수 있게 해줍니다. 이 정보는
컨트랙트 매니페스트 파일을 생성하는 데 사용됩니다. 메서드를 추가하기 전후에 coin.manifest.json을 확인하여
파일에 대한 변경사항을 볼 수 있습니다.
우리 예제에서는 컨트랙트 이름과 지원되는 표준을 정의하지만, 원하는 모든 정보를 추가할 수 있습니다.
또한 컨트랙트가 onNEP17Payment 메서드를 호출할 수 있도록 권한을 추가하고 있습니다. 컴파일러가
이 단계를 자동으로 수행하지만, 컨트랙트 코드에서 권한을 정의하는 것이 좋은 관행입니다.
from boa3.builtin.compile_time import metadata, NeoMetadata
@metadata
def manifest_metadata() -> NeoMetadata:
meta = NeoMetadata()
meta.name = 'Coin Contract'
meta.supported_standards = ['NEP-17']
meta.add_permission(methods=['onNEP17Payment'])
return meta
이 간단한 NEP-17
예제를 확인하여 이 예제만큼 간단하지만 더 응집력 있고 구현에서 더 많은 기능을 사용하는
스마트 컨트랙트를 보세요.
또한 토큰을 발행하고 소각할 수도 있는 스마트 컨트랙트를 검토하려면 이 더 복잡한 NEP-17
예제를 확인하세요.
더 현실적인 환경에서 스마트 컨트랙트를 테스트하려면 neo-express로 개인 네트워크를 만들고
스마트 컨트랙트를 배포하거나, 스마트 컨트랙트를 테스트넷에 배포할 수 있습니다.