기술/BlockChain

[Unity+Ethereum] 유니티에서 smart contract에 이더리움 전송 개발

leatherjean 2018. 4. 22. 15:55

유니티에서 이더리움 전송


 프로젝트를 진행하는 중에 유니티에서 이더리움을 내 컨트랙트로 보내야 하는 기능이 필요하여 구현하였다.

외국 블로거의 오픈소스를 참조했고 이 소스 하나로는 완전한 작동이 어려우니, 만약 이 소스를 사용하고 싶다면 

소스를 분석하고 사용하는 것을 추천한다. 주석을 각 줄마다 작성해놓았으므로 크게 어려움은 없을 것이다.

컨트랙트배포는 ropsten 테스트넷에 하였고, visual studio 에서 solidity 배포까지 지원한다고 하여 설치하고 

deploy 해보았지만, 아직 미흡한 상태인지 제대로 작동하지 않았다.

그래서 remix를 사용하여 손쉽게 컨트랙트를 배포하였고, 유니티에서 해당 컨트랙트의 함수에 트랜잭션을 날렸다.


 컨트랙트에 이더리움을 전송 받기 위해서는 컨트랙트의 default function에 payable을 명시해주어야한다. 

그렇지 않으면 이더리움을 주고 받을 수 없다. 물론 default function 말고 자신이 원하는 함수에만 선언해도 된다.

처음에 소스를 분석하여 이더리움을 보내려 하였을 때는 컨트랙트에 payable이 필요한지를 몰라서 

왜 계속 트랜젝션이 pending에서 fail이 되는건지 이해를 못했다. 하지만 payable이 명시되지 않은 함수였다는 것을

깨닫고 내가 직접 컨트랙트를 배포하여 ABI와 컨트랙트 주소를 바꾸었다.


 그런데 ABI와 컨트랙트 주소를 바꿨음에도 불구하고 contract.function 부분에서 컨트랙트에 존재하는 함수인데도

인식을 못하는 현상이 벌어졌다. 그래서 기존 소스에 있던 transfer를 넣어보았는데 비록 컨트랙트로 전송은 아니지만

어쨌든 자기 자신에게 트랜잭션을 보내긴 성공했었다. 이유를 곰곰히 생각해보았다. transfer은 컨트랙트의 기본 

내장함수여서 아무 컨트랙트에서나 호출이 가능한 것인가? 라는 생각까지 해보았지만 그런 정보는 찾아볼 수가 없었다.

나는 분명 pay라는 함수가 들어있는 컨트랙트를 배포하였고, 주소도 올바로 썼고, ABI도 똑바로 명시했는데 왜 인식을

못할까? 라는 생각으로 하루를 지샜다. 


 다시 코드를 확인해보았다. 여전히 transfer은 잘 인식하는데, 실제로 존재하는 pay는 function not found:pay 가

계속 오류 문구로 떴다. 여러방면으로 하루동안 생각한 결과, 새로 넣은 ABI와 컨트랙트 주소를 이 클래스에서

인식을 못하고 이전 것으로 계속 인식하는 것 같다 라는 생각이 가장 유력했다. 그래서 나는 ABI와 컨트랙트 주소를

static으로 선언하여 정적변수로 만들어보았다. 그러자 pay 함수가 인식이 되었고, 자기 자신에게만 보내지던

트랜젝션도 컨트랙트를 향해 보낼 수 있게 되었다.


 pay가 인식이 되자, ether valueAmount 부분도 payable의 영향으로 인해 인식되기 시작했다. 그래서 결과적으로

이더리움에서 컨트랙트로 1eth 전송 성공 했다.



 **사용한 라이브러리는 .net 용 이더리움 라이브러리인 Nethereum 입니다. unity 버전으로 사용하셔야합니다. 

Nethereum 자체에는 web3 연동 기능도 있어서 매우 유용하지만, unity의 .net 버전에서는 사용할 수 없습니다. 

그래서 따로 배포된 unity 용 nethreum 라이브러리를 사용하셔야 합니다.**



using System.Collections;
using System.Numerics;
using System.Text;
using Nethereum.Contracts;
using Nethereum.JsonRpc.UnityClient;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Util;
using Nethereum.Hex.HexTypes;
using UnityEngine;
using Nethereum.ABI.Encoders;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Signer;
public class TokenContractService : MonoBehaviour
{
// 클래스를 싱글톤으로 생성한다.
private static TokenContractService instance;
public static TokenContractService Instance { get { return instance; } }
public void OnDestroy() { if (instance == this) instance = null; }
// 이 클래스는 https://www.ethereum.org/token 이 주소에서 찾을 수 있는 토큰 컨트랙트 소스에서 사용한다.
// 사용할 컨트랙트의 ABI를 정의한다. 이것을 얻기위한 하나의 방법은
// remix IDE로 가서 contract details를 보는 것이다.
public static string ABI = @"[{""constant"": false,""inputs"": [],""name"": ""getEther"",""outputs"": [], ""payable"": true, ""stateMutability"": ""payable"", ""type"": ""function""},{ ""constant"": false,""inputs"": [], ""name"": ""pay"", ""outputs"": [], ""payable"": true, ""stateMutability"": ""payable"", ""type"": ""function""},{ ""payable"": true, ""stateMutability"": ""payable"", ""type"": ""fallback"" },{ ""inputs"": [], ""payable"": true, ""stateMutability"": ""payable"", ""type"": ""constructor""}, { ""constant"": true, ""inputs"": [], ""name"": ""seller"", ""outputs"": [ { ""name"": """", ""type"": ""address"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function""}]"; // example ABI:
public static string TokenContractAddress = "컨트랙트 주소";
// 새로운 컨트랙트를 정의한다. (Nethereum.Contracts)
private Contract contract;
private string _url;
void Awake()
{
if (instance == null) instance = this;
// WalletManager로부터 url를 검색한다.
_url = WalletManager.Instance.networkUrl;
// 여기에 새로운 컨트랙트로 컨트랙트를 할당한다. 그리고 ABI, Contract Address를 보낸다.
this.contract = new Contract(null, ABI, TokenContractAddress);
WalletManager.Instance.AddInfoText("[Loading Token Info...]", true);
WalletManager.Instance.LoadingIndicator.SetActive(true);
}
public void SendFundsButtonPressed()
{
WalletData wd = WalletManager.Instance.GetSelectedWalletData();
if (wd != null)
{
///StartCoroutine(SendFunds(wd.address, WalletManager.Instance.recepientAddressInputField.text, wd.privateKey, WalletManager.Instance.fundTransferAmountInputField.text));
StartCoroutine(SendFunds("보내는 주소", "받는 컨트랙트 주소", "보내는 주소 프라이빗 키", WalletManager.Instance.fundTransferAmountInputField.text));
}
else
{
WalletManager.Instance.logText.Log("No Wallet Account Found");
}
}
public TransactionInput CreateTransferFundsTransactionInput(
// 컨트랙트를 향한 이 트랜잭션을 사용한다.
// 이 트랜잭션을 실행시키는 address (addressFrom),
// 이 트랜잭션을 받는 address (addressTo),
// 이 트랜잭션을 실행시키는 address의 프라이빗 키 (privateKey),
// 이 컨트랙트에 보내는 매개변수 값. ping 예제에서는 pingValue (pingValue),
// 소비할 최대 가스량,
// 1 가스가 소모될때마다 지불할 가격, 가스 값. (가격이 높을수록 tx이 블록에 포함되는 속도도 높아질 것이다.)
// 이 컨트랙트에 보내는 이더의 수량. 단위는 wei다 . (wei*10^18 = 1eth)
string addressFrom,
string addressTo,
string privateKey,
BigInteger functionValue,
HexBigInteger gas = null,
HexBigInteger gasPrice = null,
HexBigInteger valueAmount = null)
{
var function = contract.GetFunction("pay");
///return function.CreateTransactionInput(addressFrom, gas, gasPrice, valueAmount, addressTo, transferAmount);
//CreateTransactionInput 함수의 정의를 피킹 해보니
//valueAmount 이후의 값은 contract 함수의 파라미터 값을 넣는 것이었다.
//우리의 pay 함수는 contract가 필요없다.
return function.CreateTransactionInput(addressFrom, gas, gasPrice, valueAmount);
}
public IEnumerator SendFunds(string addressFrom, string addressTo, string accountPrivateKey, string functionValue)
{
// 함수를 위해 인코딩 된 값들을 트랜잭션 인풋에 넣고 생성한다.
// public key의 sender와 receiver가 필요하다. (addressFrom, addressTo),the private key (accountPrivateKey),
// 컨트랙트로 보낼 functionValue의 양
// 가스량 (500000 in this case),
// 가스 값 (10), (default value 로 설정하고 싶으면 0을 입력하면 된다)
// transfer 하고 싶은 이더의 양.
var transactionInput = CreateTransferFundsTransactionInput(
addressFrom,
addressTo,
accountPrivateKey,
new BigInteger(1000000), // functionValue
new HexBigInteger(100000), // gas
new HexBigInteger(100000), // gasPrice
new HexBigInteger(1000000000000000000) // ether valueAmount
);
// 여기에 새로운 signed transaction unity request를 만든다.
// 매개변수로는 전에 얻었던 url, private key, user address
// 이것은 자동적으로 트랜잭션에 서명을 해줄 것이다.
var transactionSignedRequest = new TransactionSignedUnityRequest(WalletManager.Instance.networkUrl,
accountPrivateKey, addressFrom);
// 그러면 전송하고 기다리면 된다.
WalletManager.Instance.logText.Log("Sending fund transfer transaction...");
yield return transactionSignedRequest.SignAndSendTransaction(transactionInput);
if (transactionSignedRequest.Exception == null)
{
// 예외가 없다면 결과를 출력할 것이다.
WalletManager.Instance.logText.Log("transfer tx submitted: " + transactionSignedRequest.Result);
WalletManager.Instance.CopyToClipboard(transactionSignedRequest.Result);
Application.OpenURL("https://ropsten.etherscan.io/tx/" + transactionSignedRequest.Result);
}
else
{
Debug.Log("Error submitting transfer tx: " + transactionSignedRequest.Exception.Message);
}
}
}