truffle-dappchain-exampleでは、Loom DappChain のサンプルコードが多岐に渡り纏められており、チュートリアルで使用方法を学ぶ事が出来ます。
サンプルコードをより深く理解する為、今回は ERC721 部分のみ抽出し、トークンをサイドチェーンに転送する処理を覗いてみたいと思います。
環境
今回はWSL(Ubuntu)で動作確認しました。各パッケージのバージョンは以下の通りです。
Ubuntu 16.4 (WSL)
node 10.15.3
npm 6.4.1
truffle 5.0.1
※truffleのバージョンは「5.0.1」である必要があり、もしバージョンが新しい場合は5.0.1に戻す必要があります(2019/4/10確認:truflle@5.0.11で試しましたが、サイドチェーンへの転送操作で失敗してしまいます。)
※その際は、以下のコマンドで一旦アンインストールしてから、再度インストールして下さい。
npm uninstall -g truffle
npm install -g truffle@5.0.1
また、テストネットへデプロイする為に、INFURAのPROJECT ID が必要です。
無料で取得できるので、用意しておいて下さい。
作業フォルダを用意する
チュートリアルでは、
# gateway-tutorialディレクトリにチュートリアルリポジトリをクローン
git clone https://github.com/loomnetwork/truffle-dappchain-example gateway-tutorial
cd gateway-tutorial
# dependenciesのインストール
yarn
上記の様に git clone していますが、余計な部分もくっついてきて邪魔なので、
ERC721 の転送に必要なコードのみ使う為、新しいフォルダを作成します。
mkdir erc721-gateway-tutorial
cd erc721-gateway-tutorial
アカウントを用意する
チュートリアルでは、2つのテストネットを使用しています。
・メインチェーン Rinkeby
・サイドチェーン External Dev Plasma
サイドチェーン側のアカウント作成
まずはサイドチェーンのアカウントを作成します。
ここはチュートリアルの方法をそのまま使います。
サイドチェーン用アカウントを作成する為に、loom コマンドをインストールします。
curl https://raw.githubusercontent.com/loomnetwork/loom-sdk-documentation/master/scripts/get_loom.sh | sh
# LOOM_BIN をダウンロードしたloomバイナリに参照する
# どこからでも簡単に実行できるようにする
export LOOM_BIN=pwd
/loom
次に、 loom genkey で、アカウントを作成します。
# $LOOM_BIN genkey -k extdev_private_key -a extdev_public_key
local address: 0xf8419a0404a4080D43c029eeE42D45d00d516eCc
local address base64: +EGaBASkCA1DwCnu5C1F0A1Rbsw=
すると上記の様にサイドチェーン側の ether アドレスが表示され、2つのファイルが生成されます。
・extdev_private_key
・extdev_public_key
メインチェーン側のアカウント作成
テストネット Rinkeby は、通常の ethereum アカウントを作成して使用します。
下記は、リポジトリ内の gen-eth-key.js を少しだけ修正したものです。
const fs = require('fs')
const path = require('path')
const bip39 = require('bip39')
const hdkey = require('ethereumjs-wallet/hdkey')
const prefix = "rinkeby";
let mnemonic = bip39.generateMnemonic()
const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(mnemonic))
const wallet_hdpath = "m/44'/60'/0'/0/"
const wallet = hdwallet.derivePath(wallet_hdpath + '0').getWallet()
fs.writeFileSync(path.join(__dirname, `./${prefix}_account`), '0x' + wallet.getAddress().toString('hex'))
fs.writeFileSync(path.join(__dirname, `./${prefix}_mnemonic`), mnemonic)
fs.writeFileSync(path.join(__dirname, `./${prefix}_private_key`), wallet.getPrivateKey().toString('hex'))
では、実行しましょう。
node gen-eth-key.js
下記3つのファイルが生成されます。
・rinkeby_mnemonic
・rinkeby_account
・rinkeby_private_key
これで、メインチェーン側のアカウントが作成できました。
サイドチェーンへのデプロイには gas はかかりませんが、メインチェーンは当然 gas が必要です。
メインチェーン側のアカウントを作成したら、ファイル「rinkeby_account」のアカウントに対し、少額の Rnkeby-Ether を入金しておいて下さい(0.5 ether もあれば十分です)。
truffle環境の初期化
ではデプロイ環境を作っていきましょう。まず truffle を初期化します。
truffle init
次に、リポジトリから必要な部分のみ抽出した package.json を配置します。
{
"name": "erc721-tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"bip39": "^2.5.0",
"bn.js": "^4.11.8",
"commander": "^2.18.0",
"ethereumjs-tx": "^1.3.7",
"ethereumjs-wallet": "^0.6.2",
"loom-js": "1.38.0",
"loom-truffle-provider": "^0.11.0",
"openzeppelin-solidity": "^1.12.0",
"truffle-hdwallet-provider": "^1.0.2"
},
"author": "",
"license": "ISC"
}
最後に、必要パッケージをインストールします。
npm install
package.json の "dependencies" で定義された各種パッケージが全てインストールされます。
なお、web3 パッケージは、"loom-js" の依存パッケージとして自動的にインストールされます。
("web3": "1.0.0-beta.33")
ERC721コントラクトの配置
次に、Solidityファイルを配置します。今回使用するのは2つのファイルです。
pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";
contract MyRinkebyToken is ERC721Token {
constructor() ERC721Token("MyRinkebyToken", "MRT") public {
}
function mint(uint256 _uid) public
{
_mint(msg.sender, _uid);
}
// Convenience function to get around crappy function overload limitations in Web3
function depositToGateway(address _gateway, uint256 _uid) public {
safeTransferFrom(msg.sender, _gateway, _uid);
}
}
MyRinkebyToken.sol は、ERC721に準拠したトークンコントラウトです。
メインチェーンとして、テストネット rinkeby へデプロイします。
pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";
contract MyToken is ERC721Token {
// Transfer Gateway contract address
address public gateway;
constructor(address _gateway) ERC721Token("MyToken", "MTC") public {
gateway = _gateway;
}
// Used by the DAppChain Gateway to mint tokens that have been deposited to the Ethereum Gateway
function mintToGateway(uint256 _uid) public
{
require(msg.sender == gateway, "only the gateway is allowed to mint");
_mint(gateway, _uid);
}
}
MyToken.sol も、ERC721に準拠したトークンコントラクトです。
サイドチェーンとして、External Dev Plasma へデプロイします。
この2つのコントラクトを橋渡しするのが「TransferGatewayコントラクト」です。TransferGatewayについてはこちらに詳しく解説されています。
次に、それぞれのコントラクトをデプロイする為の定義ファイルを配置します。
「migrations」フォルダに、デプロイ定義ファイルを2つ配置します。
const MyToken = artifacts.require('MyToken');
const gatewayAddress = '0xe754d9518bf4a9c63476891ef9AA7d91C8236A5D';
module.exports = function (deployer, network, accounts) {
if (network === 'rinkeby') {
return;
}
deployer.then(async () => {
await deployer.deploy(MyToken, gatewayAddress);
const myTokenInstance = await MyToken.deployed();
console.log('*************************************************************************');
console.log(`MyToken Contract Address: ${myTokenInstance.address}`);
console.log('*************************************************************************');
});
}
サイドチェーン「External Dev Plasma」へ、コントラクト「MyToken.sol」をデプロイします。
const MyRinkebyToken = artifacts.require('MyRinkebyToken');
var obj_MyRinkebyToken;
module.exports = function (deployer, network, accounts) {
if (network !== 'rinkeby') {
return;
}
deployer.deploy(MyRinkebyToken).then((instance) => {
obj_MyRinkebyToken = instance;
return obj_MyRinkebyToken.mint(1);
}).then(() => {
return obj_MyRinkebyToken.mint(2);
}).then(() => {
return obj_MyRinkebyToken.mint(3);
}).then(() => {
console.log('*************************************************************************');
console.log(`MyRinkebyToken Contract Address: ${obj_MyRinkebyToken.address}`);
console.log('*************************************************************************');
});
}
メインチェーン「Rinkeby」へ、コントラクト「MyRinkebyToken」をデプロイします。
次に、デプロイ用のネットワーク設定ファイル truffle-config.js を配置します。今回は ERC721 に必要な2つのネットワーク定義とコンパイルバージョン指定のみを抽出しました。
const { readFileSync } = require('fs')
const path = require('path')
const LoomTruffleProvider = require('loom-truffle-provider')
const HDWalletProvider = require('truffle-hdwallet-provider');
module.exports = {
networks: {
extdev_plasma_us1: {
provider: function() {
const privateKey = readFileSync(path.join(__dirname, 'extdev_private_key'), 'utf-8')
const chainId = 'extdev-plasma-us1'
const writeUrl = 'http://extdev-plasma-us1.dappchains.com:80/rpc'
const readUrl = 'http://extdev-plasma-us1.dappchains.com:80/query'
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
},
network_id: '9545242630824'
},
rinkeby: {
provider: function() {
const mnemonic = readFileSync(path.join(__dirname, 'rinkeby_mnemonic'), 'utf-8')
const infura_project_id = readFileSync(path.join(__dirname, 'INFURA_PROJECT_ID'), 'utf-8')
return new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/${infura_project_id}`, 0, 10)
},
network_id: 4,
gasPrice: 15000000001,
skipDryRun: true
}
},
compilers: {
solc: {
version: '0.4.24'
}
}
}
「extdev_plasma_us1」は、サイドチェーン用のネットワーク定義です。サイドチェーン用プライベートキーファイル extdev_private_key を読み込み、サイドチェーンへデプロイします。
「rinkeby」は、メインチェーン用のネットワーク定義です。テストネット rinkeby へデプロイする為に、INFURA のゲートウェイを使用しています(URLには、"v3"を加えています)。
サイドチェーン(External Dev Plasma)へデプロイ
まず「MyToken.sol」をデプロイします。
チュートリアルでは、以下のコマンドでデプロイしていますが、
yarn deploy:extdev
今回は yarn は使わず下記コマンドでデプロイします。
truffle deploy --network extdev_plasma_us1
引数「extdev_plasma_us1」は、truffle-config.js で記述したサイドチェーンへのデプロイ定義です。
このコマンドにより、まずコンパイルが行われ、続けてデプロイが実行されます。
デプロイ時の処理は「2_deploy_extdev.js」に記述しています。
Starting migrations...
======================
> Network name: 'extdev_plasma_us1'
> Network id: 9545242630824
> Block gas limit: 0
:
2_deploy_extdev.js
==================
Deploying 'MyToken'
-------------------
> transaction hash: 0xcf5bcc7e7f92206a58a26d628eb12327f1617b9480af35c587b8c4d4e517c48c
> Blocks: 0 Seconds: 0
> contract address: 0xfd47bB959AB5C3e1a464DC61Ca27e62FA684Ba25
> account: 0xF8419A0404a4080d43c029eeE42d45d00D516ecC
> balance: 0
> gas used: 0
> gas price: 0 gwei
> value sent: 0 ETH
> total cost: 0 ETH
*************************************************************************
MyToken Contract Address: 0xfd47bB959AB5C3e1a464DC61Ca27e62FA684Ba25
*************************************************************************
:
Summary
=======
> Total deployments: 2
> Final cost: 0 ETH
デプロイ情報は「./build/contracts/MyToken.json」に格納されます。
:
"networks": {
"9545242630824": {
"events": {},
"links": {},
"address": "0xfd47bB959AB5C3e1a464DC61Ca27e62FA684Ba25",
"transactionHash": "0xcf5bcc7e7f92206a58a26d628eb12327f1617b9480af35c587b8c4d4e517c48c"
}
:
コントラクトアドレス(ここでは 0xfd47bB959AB5C3e1a464DC61Ca27e62FA684Ba25 )は、サイドチェーン側のアドレスです。
メインチェーン(rinkeby)へデプロイ
次は「MyRinkebyToken.sol」をデプロイします。
チュートリアルでは、以下のコマンドでデプロイしています。
yarn deploy:rinkeby
ここでも yarn は使わず下記コマンドでデプロイします。
truffle deploy --network rinkeby
引数「rinkeby」は、truffle-config.js で記述したメインチェーン(テストネット)へのデプロイ定義です。
デプロイ時の処理は「3_deploy.rinkeby.js」に記述しており、以下の通り実行されます。
・コントラクト「MyRinkebyToken」のデプロイ
・トークン生成(UID=1)
・トークン生成(UID=2)
・トークン生成(UID=3)
※デプロイ後に、トークンを3つ発行しているので、処理には時間が掛かります。
Starting migrations...
======================
> Network name: 'rinkeby'
> Network id: 4
> Block gas limit: 6996332
:
3_deploy_rinkeby.js
===================
Deploying 'MyRinkebyToken'
--------------------------
> transaction hash: 0x938e58d5e6efc6b619d6576c7817a6cdfa78f01e19fbbd3814631df26281c6c9
> Blocks: 0 Seconds: 5
> contract address: 0xD265Cece7137d7D1031B2b93E649EcA0f27A29e9
> account: 0x9a27B0dfb5C61cB1BeeC46D0Ba0448a87C1bF481
> balance: 0.262541474997502765
> gas used: 2219773
> gas price: 15.000000001 gwei
> value sent: 0 ETH
> total cost: 0.033296595002219773 ETH
*************************************************************************
MyRinkebyToken Contract Address: 0xD265Cece7137d7D1031B2b93E649EcA0f27A29e9
*************************************************************************
:
Summary
=======
> Total deployments: 2
> Final cost: 0.037458525002497235 ETH
デプロイ情報は「./build/contracts/MyRinkebyToken.json」に格納されます。
※deployを2回に分けて実行しているのは、truffleが同時に2つのネットワークに対しデプロイを行う機能を持っていないからだと思います。migrations 定義ファイルでは以下の様な条件式でネットワークを判別しています。
if (network !== 'rinkeby') {
return;
}
マッピング
トランスファーゲートウェイを使う事で、メインチェーンのトークンをサイドチェーンへ転送し、サイドチェーン側で利用する事が出来る様になります。
チュートリアルでは gateway-cli.js を使い、様々なコマンドを実行しています。
「トークン生成」も、チュートリアルでは以下のコマンドを使って実施していますが、
# Rinkebyでトークンを生成
node ./gateway-cli.js mint-token 1
node ./gateway-cli.js mint-token 2
node ./gateway-cli.js mint-token 3
今回は、前述の「3_deploy_rinkeby.js」内でデプロイ後にトークン生成を行ったので、上記コマンドは不要です。
以下より、ERC721 トークンの転送に関わる機能のみを(やたらと長い)gateway-cli.js から抽出し、実行していこうと思います。
まずは今回使用する共通処理を配置します。
const fs = require('fs')
const path = require('path')
const Web3 = require('web3')
const {
Client, NonceTxMiddleware, SignedTxMiddleware, Address, LocalAddress, CryptoUtils, LoomProvider,
Contracts, Web3Signer, soliditySha3
} = require('loom-js')
const rinkebyGatewayAddress = '0xb73C9506cb7f4139A4D6Ac81DF1e5b6756Fab7A2'
const extdevChainId = 'extdev-plasma-us1'
const MyRinkebyTokenJSON = require('./build/contracts/MyRinkebyToken.json')
const MyTokenJSON = require('./build/contracts/MyToken.json')
function loadRinkebyAccount() {
const infura_project_id = fs.readFileSync(path.join(__dirname, 'INFURA_PROJECT_ID'), 'utf-8')
const privateKey = fs.readFileSync(path.join(__dirname, './rinkeby_private_key'), 'utf-8')
const web3js = new Web3(`https://rinkeby.infura.io/v3/${infura_project_id}`)
const ownerAccount = web3js.eth.accounts.privateKeyToAccount('0x' + privateKey)
web3js.eth.accounts.wallet.add(ownerAccount)
return { account: ownerAccount, web3js }
}
function loadExtdevAccount() {
const privateKeyStr = fs.readFileSync(path.join(__dirname, './extdev_private_key'), 'utf-8')
const privateKey = CryptoUtils.B64ToUint8Array(privateKeyStr)
const publicKey = CryptoUtils.publicKeyFromPrivateKey(privateKey)
const client = new Client(
extdevChainId,
'wss://extdev-plasma-us1.dappchains.com/websocket',
'wss://extdev-plasma-us1.dappchains.com/queryws'
)
client.txMiddleware = [
new NonceTxMiddleware(publicKey, client),
new SignedTxMiddleware(privateKey)
]
client.on('error', msg => {
console.error('PlasmaChain connection error', msg)
})
return {
account: LocalAddress.fromPublicKey(publicKey).toString(),
web3js: new Web3(new LoomProvider(client, privateKey)),
client
}
}
async function getRinkebyTokenContract(web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(
MyRinkebyTokenJSON.abi,
MyRinkebyTokenJSON.networks[networkId].address
)
}
async function getExtdevTokenContract(web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(
MyTokenJSON.abi,
MyTokenJSON.networks[networkId].address,
)
}
module.exports = {
Address: Address,
Contracts: Contracts,
MyRinkebyTokenJSON: MyRinkebyTokenJSON,
MyTokenJSON: MyTokenJSON,
rinkebyGatewayAddress: rinkebyGatewayAddress,
loadRinkebyAccount: loadRinkebyAccount,
loadExtdevAccount: loadExtdevAccount,
getRinkebyTokenContract: getRinkebyTokenContract,
getExtdevTokenContract: getExtdevTokenContract,
loadExtdevAccount: loadExtdevAccount
};
loadRinkebyAccount() … メインチェーン(rinkeby)側のアカウント情報を取得します
loadExtdevAccount() … サイドチェーン(ExtDev)側のアカウント情報を取得します
getRinkebyTokenContract() … コントラクト MyRinkebyToken の情報を取得します
getExtdevTokenContract() … コントラクト MyToken の情報を取得します
extdev コントラクトを Rinkebyコントラクトにマップする
チュートリアルでは、以下のコマンドでマッピングを行っています。
node ./gateway-cli.js map-contracts token
今回は、「cli_map-contracts.js」を作成します。
const {
MyRinkebyTokenJSON, MyTokenJSON, loadRinkebyAccount, loadExtdevAccount
} = require('./cli_lib.js')
const {
Client, NonceTxMiddleware, SignedTxMiddleware, Address, LocalAddress, CryptoUtils, LoomProvider,
Contracts, Web3Signer, soliditySha3
} = require('loom-js')
const { OfflineWeb3Signer } = require('loom-js/dist/solidity-helpers')
const TransferGateway = Contracts.TransferGateway
//.command('map-contracts <contract-type>')
async function callMapContracts() {
let client
try {
const rinkeby = loadRinkebyAccount()
const extdev = loadExtdevAccount()
client = extdev.client
const rinkebyNetworkId = await rinkeby.web3js.eth.net.getId()
const extdevNetworkId = await extdev.web3js.eth.net.getId()
let tokenRinkebyAddress, tokenExtdevAddress, rinkebyTxHash
tokenRinkebyAddress = MyRinkebyTokenJSON.networks[rinkebyNetworkId].address
rinkebyTxHash = MyRinkebyTokenJSON.networks[rinkebyNetworkId].transactionHash
tokenExtdevAddress = MyTokenJSON.networks[extdevNetworkId].address
const signer = new OfflineWeb3Signer(rinkeby.web3js, rinkeby.account)
await mapContracts({
client,
signer,
tokenRinkebyAddress,
tokenExtdevAddress,
ownerExtdevAddress: extdev.account,
rinkebyTxHash
})
console.log(`Submitted request to map ${tokenExtdevAddress} to ${tokenRinkebyAddress}`)
} catch (err) {
console.error(err)
} finally {
if (client) {
client.disconnect()
}
}
}
async function mapContracts({
client,
signer,
tokenRinkebyAddress,
tokenExtdevAddress,
ownerExtdevAddress,
rinkebyTxHash
}) {
const ownerExtdevAddr = Address.fromString(`${client.chainId}:${ownerExtdevAddress}`)
const gatewayContract = await TransferGateway.createAsync(client, ownerExtdevAddr)
const foreignContract = Address.fromString(`eth:${tokenRinkebyAddress}`)
const localContract = Address.fromString(`${client.chainId}:${tokenExtdevAddress}`)
const hash = soliditySha3(
{ type: 'address', value: tokenRinkebyAddress.slice(2) },
{ type: 'address', value: tokenExtdevAddress.slice(2) }
)
const foreignContractCreatorSig = await signer.signAsync(hash)
const foreignContractCreatorTxHash = Buffer.from(rinkebyTxHash.slice(2), 'hex')
await gatewayContract.addContractMappingAsync({
localContract,
foreignContract,
foreignContractCreatorSig,
foreignContractCreatorTxHash
})
}
callMapContracts()
※mapContracts() の gatewayContract.addContractMappingAsync() でゲートウェイコントラクトの関数 addContractMappingAsync() を呼び出す事で、マッピングが行われます。
では、実行しましょう。
node cli_map-contracts.js
Submitted request to map 0xfd47bB959AB5C3e1a464DC61Ca27e62FA684Ba25 to 0xD265Cece7137d7D1031B2b93E649EcA0f27A29e9
これで、メインチェーンとサイドチェーンのコントラクトアカウントがマッピングされました。
extdevアカウントをRinkebyアカウントにマップする
次に、デプロイした2つのコントラクトをマッピングします。
同じくチュートリアルでは、以下のコマンドでコントラクトのマッピングを行っています。
node ./gateway-cli.js map-accounts
今回は、「cli_map-accounts.js」を作成します。
const {
loadRinkebyAccount, loadExtdevAccount, Address, Contracts
} = require('./cli_lib.js')
const { OfflineWeb3Signer } = require('loom-js/dist/solidity-helpers')
//.command('map-accounts')
async function callMapAccounts() {
let client
try {
const rinkeby = loadRinkebyAccount()
const extdev = loadExtdevAccount()
client = extdev.client
const signer = new OfflineWeb3Signer(rinkeby.web3js, rinkeby.account)
await mapAccounts({
client,
signer,
ownerRinkebyAddress: rinkeby.account.address,
ownerExtdevAddress: extdev.account
})
} catch (err) {
console.error(err)
} finally {
if (client) {
client.disconnect()
}
}
}
async function mapAccounts({ client, signer, ownerRinkebyAddress, ownerExtdevAddress }) {
const ownerRinkebyAddr = Address.fromString(`eth:${ownerRinkebyAddress}`)
const ownerExtdevAddr = Address.fromString(`${client.chainId}:${ownerExtdevAddress}`)
const mapperContract = await Contracts.AddressMapper.createAsync(client, ownerExtdevAddr)
try {
const mapping = await mapperContract.getMappingAsync(ownerExtdevAddr)
console.log(`${mapping.from.toString()} is already mapped to ${mapping.to.toString()}`)
return
} catch (err) {
// assume this means there is no mapping yet, need to fix loom-js not to throw in this case
}
console.log(`mapping ${ownerRinkebyAddr.toString()} to ${ownerExtdevAddr.toString()}`)
await mapperContract.addIdentityMappingAsync(ownerExtdevAddr, ownerRinkebyAddr, signer)
console.log(`Mapped ${ownerExtdevAddr} to ${ownerRinkebyAddr}`)
}
callMapAccounts()
※mapAccounts() の mapperContract.addIdentityMappingAsync() で、メインチェーン側アカウントとサイドチェーン側アカウントのマッピングが行われます。
では、実行しましょう。
node cli_map-accounts.js
mapping eth:0x9a27b0dfb5c61cb1beec46d0ba0448a87c1bf481 to extdev-plasma-us1:0xf8419a0404a4080d43c029eee42d45d00d516ecc
Mapped extdev-plasma-us1:0xf8419a0404a4080d43c029eee42d45d00d516ecc to eth:0x9a27b0dfb5c61cb1beec46d0ba0448a87c1bf481
メインチェーンとサイドチェーンの2つのアカウントがマッピングされました。
これでようやくサイドチェーンへトークンを転送する準備が整いました。
トークンの転送
いよいよ転送です。
チュートリアルでは、以下の手順となっています。
# トークンをextdevのPlasmaChainへ転送
node ./gateway-cli.js deposit-token 1
今回は、「cli_deposit-token.js」を作成します。
const {
loadRinkebyAccount, getRinkebyTokenContract, rinkebyGatewayAddress
} = require('./cli_lib.js')
//.command('deposit-token <uid>')
async function callDepositToken(uid) {
const { account, web3js } = loadRinkebyAccount()
try {
const tx = await depositTokenToGateway(web3js, uid, account.address, 350000)
console.log(`Token ${uid} deposited, Rinkeby tx hash: ${tx.transactionHash}`)
} catch (err) {
console.error(err)
}
}
async function depositTokenToGateway(web3js, tokenId, ownerAccount, gas) {
const contract = await getRinkebyTokenContract(web3js)
const gasEstimate = await contract.methods
.depositToGateway(rinkebyGatewayAddress, tokenId)
.estimateGas({ from: ownerAccount, gas })
if (gasEstimate == gas) {
throw new Error('Not enough enough gas, send more.')
}
return contract.methods
.depositToGateway(rinkebyGatewayAddress, tokenId)
.send({ from: ownerAccount, gas: gasEstimate })
}
callDepositToken(1)
※「.depositToGateway()」で、コントラクト MyRinkebyToken の関数 depositToGateway() を呼び出し、コントラクトアドレスのトランスファーゲートウェイで定義されている「Devnet Plasma (extdev-plasma-us1)」(0xb73C9506cb7f4139A4D6Ac81DF1e5b6756Fab7A2)へ、トークンの所有権を移しています。
トランスファーゲートウェイは、下記で定義しています。
:
const rinkebyGatewayAddress = '0xb73C9506cb7f4139A4D6Ac81DF1e5b6756Fab7A2'
:
では実行しましょう。
node cli_deposit-token.js
Token 1 deposited, Rinkeby tx hash: 0xc2990d84e433be767635945ce8adcf07caf4bb6ff367e550df28fbfbce12729a
これでトークン1がトランスファーゲートウェイ・コントラクトへデポジットされ、サイドチェーンで使用出来る様になりました。
トークン所有者チェック
チュートリアル通りに、3つのトークンの所有権を誰が保持しているのか、チェックしてみましょう。
チュートリアルでは、以下のコマンドで確認していますが、
# あなたがRinkebyにトークンをいくつ持っているかをチェックする
node ./gateway-cli.js token-balance -c eth# あなたがextdevにトークンをいくつ持っているかをチェックする
node ./gateway-cli.js token-balance# Rinkebyのゲートウェイがトークンをいくつ持っているかをチェックする
node ./gateway-cli.js token-balance -a gateway -c eth
今回は、「cli_token-balance.js」で、一括で確認します。
const {
loadRinkebyAccount, loadExtdevAccount, rinkebyGatewayAddress, getRinkebyTokenContract, getExtdevTokenContract
} = require('./cli_lib.js')
const extdevGatewayAddress = '0xE754d9518bF4a9C63476891eF9Aa7D91c8236a5d'
async function callTokenBalance() {
console.log("\r\n# あなたがRinkebyにトークンをいくつ持っているかをチェックする")
await callTokenBalance_Rinkeby(0)
console.log("\r\n# あなたがextdevにトークンをいくつ持っているかをチェックする")
await callTokenBalance_Extdev(0)
//console.log("\r\n# あなたがextdevにトークンをいくつ持っているかをチェックする(GateWay)")
//await callTokenBalance_Extdev(1)
console.log("\r\n# Rinkebyのゲートウェイがトークンをいくつ持っているかをチェックする")
await callTokenBalance_Rinkeby(1)
console.log("\r\n")
}
//.command('token-balance')
async function callTokenBalance_Rinkeby(flag) {
try {
let ownerAddress, balance
const { account, web3js } = loadRinkebyAccount()
if(flag == '0') ownerAddress = account.address
else ownerAddress = rinkebyGatewayAddress
balance = await getRinkebyTokenBalance(web3js, ownerAddress)
console.log(`${ownerAddress} owns ${balance.total} tokens.`)
if (balance.tokens.length > 0) {
console.log(`First ${balance.tokens.length} token(s): ${balance.tokens}`)
}
} catch (err) {
console.error(err)
}
}
// Returns an object containing the total number of tokens owned by the given account,
// and up to 5 token IDs.
async function getRinkebyTokenBalance(web3js, accountAddress) {
const contract = await getRinkebyTokenContract(web3js)
const total = await contract.methods
.balanceOf(accountAddress)
.call()
const tokens = []
for (let i = 0; i < Math.min(total, 5); i++) {
const tokenId = await contract.methods
.tokenOfOwnerByIndex(accountAddress, i)
.call()
tokens.push(tokenId)
}
return { total, tokens }
}
//.command('token-balance')
async function callTokenBalance_Extdev(flag) {
try {
let ownerAddress, balance
const { account, web3js, client } = loadExtdevAccount()
if(flag == '0') ownerAddress = account
else ownerAddress = extdevGatewayAddress
try {
balance = await getExtdevTokenBalance(web3js, ownerAddress)
} catch (err) {
throw err
} finally {
client.disconnect()
}
console.log(`${ownerAddress} owns ${balance.total} tokens.`)
if (balance.tokens.length > 0) {
console.log(`First ${balance.tokens.length} token(s): ${balance.tokens}`)
}
} catch (err) {
console.error(err)
}
}
async function getExtdevTokenBalance(web3js, accountAddress) {
const contract = await getExtdevTokenContract(web3js)
const addr = accountAddress.toLowerCase()
const total = await contract.methods
.balanceOf(addr)
.call({ from: addr })
const tokens = []
for (let i = 0; i < Math.min(total, 5); i++) {
const tokenId = await contract.methods
.tokenOfOwnerByIndex(addr, i)
.call({ from: addr })
tokens.push(tokenId)
}
return { total, tokens }
}
callTokenBalance()
では、実行してみましょう。
node cli_token-balance.js
# あなたがRinkebyにトークンをいくつ持っているかをチェックする
0x9a27B0dfb5C61cB1BeeC46D0Ba0448a87C1bF481 owns 2 tokens.
First 2 token(s): 3,2
# あなたがextdevにトークンをいくつ持っているかをチェックする
0xf8419a0404a4080d43c029eee42d45d00d516ecc owns 1 tokens.
First 1 token(s): 1
# Rinkebyのゲートウェイがトークンをいくつ持っているかをチェックする
0xb73C9506cb7f4139A4D6Ac81DF1e5b6756Fab7A2 owns 1 tokens.
First 1 token(s): 1
サイドチェーン(extdev)側アカウントがトークン1を所持している事が確認出来ました。
また、メインチェーン側でも、Rinkebyゲートウェイコントラクトが同トークンを所有している事も分かりました。
サイドチェーン側にトークンを転送したといっても、メインチェーンからトークンが消えてなくなる訳では無く、メインチェーンに用意されたゲートウェイコントラクトが保有・管理する仕組みである事が伺えます。
詳しくはこちらの図が分かり易いです。
最後に
チュートリアルの中から ERC721 部分のみを抽出し、サイドチェーンへの転送までを行ってみました。
こうしてリポジトリから必要コードのみを抽出し確認して行けば、Loom SDK の具体的な実装方法、SDKを使う為にどの様な手順が必要なのかを整理出来て、理解が深まると思います。
気になった点:
・openzeppelin-solidityのバージョンを最新版(2.0.0以上)にアップデートすると、サイドチェーンextdevへの転送が失敗する。まだ対応していないのか、書き方が悪いのか不明。
・truffle 5.0.11 で実行しても、サイドチェーン"extdev"への転送が失敗する。
・サイドチェーンへの転送確認(extdev側アカウントで所有確認)には時間が掛かる事がある(遅い時は数分)。
・INFURAバージョンアップにより、URLに「v3」を加えなければエラーになる。