‘이더리움 댑 개발’ 세미나 7. 6장. The Fundraiser Application 1/2
6장. The Fundraiser Application은 분량도 좀 되고 다루어야 할 내용도 많기 때문에 두 번의 세미나로 나누어서 진행합니다.
이번 세미나에서는 Editing the Beneficiary 까지만 다룹니다.
다음 링크에서 본문 소스 코드를 볼 수 있습니다. https://github.com/RedSquirrelTech/hoscdev/tree/master/chapter-7
Creating the Project
- fundraiser 디렉토리를 생성하고, 해당 디렉토리로 이동합니다.
-
12mkdir fundraisercd fundraiser
-
- truffle unbox react 명령을 실행합니다.
- 트러플은 프로젝트를 쉽게 할 수 있도록 템플릿을 제공하고 가져다 쓸 수 있도록 합니다. 템플릿을 박스라고 하고 unbox해서 가져다 사용할 수 있습니다.
- truffle init 대신 truffle unbox를 사용해서 프로젝트를 좀 더 쉽게 시작할 수 있습니다.
- react 박스는 리액트로 UI를 만들 수 있도록 프로젝트를 구성해 두었습니다.
- client 디렉토리에 관련 파일들이 생성됩니다.
- 트러플은 프로젝트를 쉽게 할 수 있도록 템플릿을 제공하고 가져다 쓸 수 있도록 합니다. 템플릿을 박스라고 하고 unbox해서 가져다 사용할 수 있습니다.
- 불필요한 파일들을 삭제합니다.
- contracts/SimpleStorage.sol
- migrations/2_deploy_contracts.js
- test 디렉토리의 모든 파일들
- contracts/Fundraiser.sol 파일과 test/fundraiser_test.js 파일을 추가합니다.
Initializing Fundraisers
그림 6.2는 새로운 기금 모금(fundraiser)을 만들기 위한 화면입니다.
기금 모금 생성을 위한 정보는 다음과 같습니다.
- 수혜자(beneficiary) 정보
- name, website, image url, description
- beneficiary address
- 모금 된 기금을 받을 계정(주소)
- custodian 또는 owner address
- 모금된 기금을 관리하는 계정(주소)
수혜자 이름(name)이 생성 시에 설정되는지를 테스트 합니다.
- fundraiser_test.js에 최 상위 테스트 스위트를 “Fundraiser”로, 테스트 스위트를 “constructor()”로, 테스트 케이스를 “it gets the beneficiary name”로 작성합니다.
-
123456789101112const FundraiserContract = artifacts.require("Fundraiser");contract("Fundraiser",() => {describe("constructor", () => {it("gets the beneficiary name", async () => {const name = "Beneficiary Name"fundraiser = await FundraiserContract.new(name);const actual = await fundraiser.name();assert.equal(actual, name, "name should match");});});});
- 컨트랙트는 배포 시에 인스턴스가 생성되기 때문에 매개변수를 갖는 생성자인 경우 배포 시에 생성자에 해당하는 인자들을 넘겨주어야 합니다. 하지만 기금 모금 생성은 컨트랙트 배포 시에 생성자의 인자를 결정할 수 없습니다. 새로운 기금 모금 생성 페이지에서 작성한 기금 모금 정보에 따라 생성자의 인자가 결정되어야 합니다.
- 다른 방법이 필요합니다. 다음 장에서는 이러한 경우를 다루기 위해 FundraiserFactory라는 것을 제시합니다.
- 이번 장에서는 배포하지 않고 컨트랙트를 테스트할 수 있는 방법을 사용합니다.
- deployed 함수를 사용하지 않고 new 함수를 사용합니다.
- FundraiserContract.new(name)
- deployed 함수를 사용하지 않고 new 함수를 사용합니다.
- 컨트랙트는 배포 시에 인스턴스가 생성되기 때문에 매개변수를 갖는 생성자인 경우 배포 시에 생성자에 해당하는 인자들을 넘겨주어야 합니다. 하지만 기금 모금 생성은 컨트랙트 배포 시에 생성자의 인자를 결정할 수 없습니다. 새로운 기금 모금 생성 페이지에서 작성한 기금 모금 정보에 따라 생성자의 인자가 결정되어야 합니다.
-
- 테스트가 통과할 수 있도록 Fundraiser 컨트랙트를 작성합니다.
-
12345678910contract Fundraiser {string private _name;constructor(string memory name) public {_name = name;}function name() external view returns(string memory) {return _name;}}
-
나머지 정보들에 대해서도 테스트 케이스를 작성합니다. 다음은 url에 대한 테스트 케이스 입니다.
-
1234567it("gets the beneficiary url", async () => {const name = "Beneficiary Name";const url = "beneficiaryname.org";fundraiser = await FundraiserContract.new(name, url);const actual = await fundraiser.url();assert.equal(actual, url, "url should match");});
- 테스트가 통과되도록 Fundraiser 컨트랙트를 수정합니다.
- 생성자가 두 개의 매개변수를 가지므로 첫 번째 테스트 케이스도 수정해야 합니다.
- 매번 테스트 케이스마다 반복해서 작성할 부분이 생깁니다. before() 함수를 사용해서 한 번만 Fundraiser 컨트랙트 인스턴스가 생성되도록 합니다.
-
12345678describe("constructor", () => {let fundraiser;const name = "Beneficiary Name";const url = "beneficiaryname.org";before(async () => {fundraiser = await FundraiserContract.new(name, url);});it("gets the beneficiary name", async () => { ...
- before()는 첫 번째 테스트 케이스 실행 전에 실행됩니다.
- after()는 마지막 테스트 케이스 실행 후에 실행됩니다.
- beforeEach()는 매 테스트 케이스 실행 전에 실행됩니다.
- afterEach()는 매 테스트 케이스 실행이 마치고 나면 실행됩니다.
-
- image url, description, beneficiary, custodian에 대해서도 테스트 케이스를 작성하고, Fundraiser 컨트랙트를 수정합니다.
- beneficiary와 custodian를 설정하려면 계정 주소가 필요함으로 accounts를 contract() 함수 인자로 받아야 합니다.
- beneficiary는 accounts[1]로, custodian은 accounts[0]으로 설정합니다.
- 수혜자 주소로 모금된 이더가 전송되어야 하기 때문에 address payable 타입으로 작성합니다.
- 관리자 주소는 수혜자 주소를 변경할 수 있는 역할 만 하고 이더를 받지는 않기 때문에 address 타입으로 작성합니다.
- beneficiary와 custodian를 설정하려면 계정 주소가 필요함으로 accounts를 contract() 함수 인자로 받아야 합니다.
Editing the Beneficiary
관리자는 수혜자를 변경할 수 있어야 합니다.
우리는 이미 이전 세미나에서 owner에 대한 개념을 다루었기 때문에 custodian 대신 owner를 적용하도록 합니다.
- 이전 세미나에서와 같이 오픈제플린을 설치합니다.
- 최신 버전의 오픈제플린 컨트랙트는 솔리디티 버전이 0.6.0으로 명시되어 있습니다. 설치된 솔리디티 컴파일러가 이 버전 보다 낮은 버전이면 컴파일 오류가 납니다.
- truffle-config.js 파일을 열고, module.exports 항목으로 networks 다음에 compilers를 추가합니다. 이렇게 하면 해당 솔리디티 컴파일러를 다운로드 합니다.
-
12345compilers: {solc: {version: "0.6.0"}}
-
- truffle-config.js 파일을 열고, module.exports 항목으로 networks 다음에 compilers를 추가합니다. 이렇게 하면 해당 솔리디티 컴파일러를 다운로드 합니다.
- 최신 버전의 오픈제플린 컨트랙트는 솔리디티 버전이 0.6.0으로 명시되어 있습니다. 설치된 솔리디티 컴파일러가 이 버전 보다 낮은 버전이면 컴파일 오류가 납니다.
- Fundraiser 컨트랙트가 Ownable을 상속받도록 합니다.
- Ownable.sol을 임포트 합니다.
- import “openzeppelin-solidity/contracts/access/Ownable.sol”
- _custodian 상태 변수와 custodian() 함수를 제거합니다.
- 생성자에서 다음과 같이 custodian을 owner로 설정합니다.
- transferOwnership(custodian);
- Ownable.sol을 임포트 합니다.
- Fundraiser 컨트랙트 변경 사항에 따라 테스트를 변경합니다.
- “it gets the custodian” 테스트 케이스에서 fundraiser.custodian()을 fundraiser.owner()로 변경합니다.
수혜자를 변경할 수 있도록 테스트 케이스와 Fundraiser 컨트랙트 함수를 추가합니다.
- “setBeneficiary()”를 테스트 스위트를 작성합니다.
- 이 테스트 스위트에서도 이전 테스트 스위트에서 작성한 것처럼 컨트랙트 생성을 위한 일을 해야 합니다.
- contract() 함수에 beforeEach() 함수를 사용해서 매번 테스트 스위트가 실행되기 전에 실행되는 공통 부분을 작성합니다.
- “it updated beneficiary when called by owner account” 테스트 케이스를 작성합니다.
- 새로운 수혜자를 accounts[2]로 설정합니다.
- owner만 수혜자를 변경할 수 있기 때문에 owner를 트랜잭션의 from으로 설정합니다.
- “it throws and error when called from a non-owner account”를 테스트 케이스로 작성합니다.
- owner가 아닌 계정으로 setBeneficiary 함수 호출 트랜잭션의 from으로 설정합니다.
-
1234567891011121314151617181920describe("setBeneficiary", () => {const newBeneficiary = accounts[2];it("updated beneficiary when called by owner account", async () => {await fundraiser.setBeneficiary(newBeneficiary, {from: custodian});const actualBeneficiary = await fundraiser.beneficiary();assert.equal(actualBeneficiary, newBeneficiary, "beneficiaries should match");});it("throws and error when called from a non-owner account", async () => {try {await fundraiser.setBeneficiary(newBeneficiary, {from: accounts[3]});assert.fail("withdraw was not restricted to owners")} catch(err) {const expectedError = "Ownable: caller is not the owner"const actualError = err.reason;assert.equal(actualError, expectedError, "should not be permitted")}});});
- 이 테스트 스위트에서도 이전 테스트 스위트에서 작성한 것처럼 컨트랙트 생성을 위한 일을 해야 합니다.
- 테스트가 통과할 수 있도록 Fundraiser 컨트랙트에 setBeneficiary() 함수를 추가합니다.
- onlyOwner modifier를 사용합니다.
-
123function setBeneficiary(address payable beneficiary) external onlyOwner {_beneficiary = beneficiary;}
솔리디티 보강
배열
- 동적 배열 – 새로 생성해서 크기 조정할 때
- arrs = new int32[](5);
- 배열을 public 상태변수로 선언하면, 컴파일러는 인덱스를 매개변수로 하는 gettter 함수를 자동생성합니다.
매핑
- 스토리지 데이터 영역에서만 사용할 수 있음
- 상태 변수 또는 스토리지 참조형으로만 선언할 수 있음
- 매핑을 public 상태변수로 선언하면, 컴파일러는 키를 매개변수로 하는 gettter 함수를 자동생성합니다.
함수
- 출력 매개변수는 함수가 실행될 때 기본값으로 초기화된 로컬 변수로 생각할 수 있습니다. 모든 출력 매개변수 값이 올바르게 저장된 경우 return문을 사용하지 않아도 됩니다.
- 다른 컨트랙트의 함수를 호출하면 트랜잭션 메시지가 블록체인에 저장됩니다.
- 컨트랙트(주소)로 다른 컨트랙트의 인스턴스를 구할 수 있습니다.
- 예) SampleContract sampleContract = SampleContract(0x4e6c…);
- 컨트랙트(주소)로 다른 컨트랙트의 인스턴스를 구할 수 있습니다.
- this로 public 함수를 호출하면 트랜잭션 메시지가 블록체인에 저장됩니다.
상속
- 상위 컨트랙트의 생성자 호출
-
12345678contract MyToken2 is DetailedERC20 {constructor (string name,string symbol,uint8 decimals) DetailedERC20(name, symbol, decimals) public {}}
- 다중 상속을 지원하기 때문에 다수의 생성자를 호출할 수 있어야 합니다. 공백 문자로 구분합니다.
-
- 재정의 함수에서 상위 컨트랙트의 함수 호출
- super 키워드 사용
- 다중 상속의 경우 상위 컨트랙트의 함수들이 순차적으로 호출됩니다.
- super 키워드 사용
추상 컨트랙트
- 구현되지 않은 함수가 하나 이상 포함되면 추상 컨트랙트입니다. 추상 컨트랙트는 인스턴스화할 수 없습니다.
인터페이스
- interface 키워드 사용
- https://solidity.readthedocs.io/en/v0.6.9/contracts.html#interfaces
라이브러리
- 클래스 라이브러리와 같이 재사용 가능한 공용 컨트랙트라고 생각하면 됩니다.
- library 키워드를 사용하고, 상태 변수를 가질 수 없고 상속을 지원하지 않습니다. payable로 이더를 받을 수 없습니다.
- 재사용은 소스코드 레벨에서도 가능하고, 배포된 컨트랙트를 재사용할 수도 있습니다.
- 소스코드 레벨에서 재사용할 경우 import하고 정적 함수를 호출하는 것처럼 라이브러리이름.함수이름 형태로 사용할 수 있습니다.
- 배포된 라이브러리를 호출하는 일반적인 방법은 배포된 라이브러리의 시그니처와 같은 로컬 추상 컨트랙트를 정의하는 것입니다.
- 추상 컨트랙트(라이브러리 주소)의 형태로 라이브러리 인스턴스를 얻을 수 있습니다.
- 배포된 라이브러리는 호출 컨트랙트의 컨텍스트에서 실행됩니다. 배포된 라이브러리는 소멸시킬 수 없습니다.
기타
- 변수 값 다시 초기화
- delete alpha;
- 다른 컨트랙트 인스턴스 생성
- new 키워드 사용
솔리디티 소스 코드 레이아웃
솔리디티의 기본적인 레이아웃은 다음과 같습니다.
- 솔리디티 컴파일러 버전
- pragma solidity <버전>
- 버전은 범위 또는 ‘^’를 사용해서 명시할 수 있음
- pragma solidity <버전>
- 컨트랙트나 라이브러리 임포트
- import <경로>
- 프로젝트의 contracts 폴더에 작성되는 컨트랙트는 contracts 폴더를 기준으로 절대 경로를 작성한다.
- import <경로>
- 컨트랙트 정의
- contract 키워드 사용
컨트랙트 코드의 레이아웃은 다음과 같이 하기를 추천합니다.
- 상태 변수(속성)
- 함수(메소드)
- 생성자
- external, public
- internal, private
- modifier 정의
- 이벤트 정의
- 열거형
- 구조체
Modifier
modifier는 함수의 선행조건이나 후행조건을 체크하기 위해 사용합니다.
- modifier 키워드 사용
-
1234modifier onlyOwner() {require(msg.sender == owner, "not owner");_;}
- ‘_;’에는 modifier를 사용하는 함수의 바디 내용이 들어갑니다.
- ‘_;’가 체크하는 문장 뒤에 나오면 선행조건을, 앞에 나오면 후행조건을 체크하는 것이 됩니다.
- 함수에서 사용될 때는 리턴이 없을 때는 함수 선언의 제일 끝에, 리턴이 있는 경우는 returns 앞에 작성합니다.
-
123function withdraw() public onlyOwner {msg.sender.transfer(address(this).balance);}
-
데이터타입
- 값 타입
- 불린(bool)
- true, false
- 정수
- int8에서 int256까지 8씩 커지는 타입을 가짐, uint8에서 uint256까지도 마찬가지
- int256은 int로, unit256은 uint로 사용 가능
- int8에서 int256까지 8씩 커지는 타입을 가짐, uint8에서 uint256까지도 마찬가지
- Fixed Point Numbers
- fixedMxN, ufixedMxN으로 작성
- M은 비트 수, N은 소수점 자리수
- M은 8에서 256까지 8씩 커짐, N은 0에서 80사이의 값 사용
- M은 비트 수, N은 소수점 자리수
- fixed는 fixed128x18, ufixed는 ufixed128x18를 대신해서 사용할 수 있음
- fixedMxN, ufixedMxN으로 작성
- Address(address, address payable)
- 20바이트로 표현되는 이더리움 주소를 나타냄
- Fixed-size byte arrays
- bytes1에서 bytes32까지 1씩 커짐
- byte는 bytes1을 대신해서 사용할 수 있음
- 열거형(enum)
- 컨트랙트
- 주소와 같은 의미로 사용
- 함수형
- 불린(bool)
- 참조타입
- 배열
- bytes는 동적 바이트 배열
- string은UTF-8 인코딩된 문자를 지원하는 동적 바이트 배열
- 구조체(struct)
- 자신의 타입을 갖는 속성을 가질 수 없음
- 매핑(mapping)
- 키-값 매핑(다른 프로그래밍 언어의 dictionary와 유사함)
- 키 타입은 정수, 불린, 주소, 바이트, 문자열만 가능, 값 타입은 모두 가능
- 열거하는 것이 불가능, 즉 반복문에서 키값쌍을 열거하면서 처리하는 것이 불가능
- 키-값 매핑(다른 프로그래밍 언어의 dictionary와 유사함)
- 배열