EOS实践篇(续) - 合约一键部署
在调试合约的时候,发现部署合约是一件比较麻烦的事情,所以写了个脚本实现一键部署合约,写下这篇文章来介绍其中的一些命令。
想要一键部署合约,编写脚本是必不可少的,这里以Mac的Docker环境部署为例,脚本为shell脚本。本文将信息介绍部署的步骤,这样的话,在linux以及Windows上部署也可以作为参考。
前言
如果不了解EOS,可以先看:EOS实践篇
另外还有使用Scatter插件的教程:使用Scatter创建自己的账号
部署
步骤简介
- 下载docer
- 下载、启动以及配置keosd
- 创建钱包、导入密钥
- 打开钱包,解锁钱包
- 创建账号
- 获取eos
- 购买cpu、net资源,购买ram资源
- 下载合约、将合约复制到docker实例目录下
- 设置合约
- 授权
步骤讲解
1. 下载docer
下载地址 https://www.docker.com/get-started
下载后根据提示进行安装,然后点击应用图标,即可启动docker服务
2. 下载、启动以及配置keosd
只有第一启动是需要执行一下步骤,下一次启动,只需要执行docker start keosd
命令即可。
这里以shell脚本所在目录($curpath
)为例,端口号为8900,也可以是其他的端口号:
docker pull eosio/eos
docker stop keosd
docker rm keosd
docker start keosd
curpath=$(cd "$(dirname "$0")"; pwd)
mkdir $curpath/eosio-wallet
docker run -d --restart=unless-stopped --name keosd \
-v $curpath/eosio-wallet:/opt/eosio/bin/data-dir \
-v $curpath/eosio-wallet:$home/eosio-wallet \
-t eosio/eos /opt/eosio/bin/keosd \
--wallet-dir /opt/eosio/bin/data-dir \
--http-server-address=127.0.0.1:8900
连接到测试网:
测试网接口可以为:https://jungle.eosn.io:443,同时接口可能会变动,可以访问如下网址,选择其中一个就行:
https://monitor.jungletestnet.io/#apiendpoints
另外,shopt -s expand_aliases
这个命令的作用是使alias
命令在脚本中也能生效。如果实在控制台下执行,就不需要改命令了。
alias
命令设置后,就不需要每次执行命令都需要设置-u
参数了,直接使用cleos
命令即可。
docker start keosd
shopt -s expand_aliases
alias cleos="docker exec -i keosd /opt/eosio/bin/cleos --wallet-url http://127.0.0.1:8900 -u <测试网接口>"
3. 创建钱包、导入密钥
这里需要提前生成私钥,可以使用命令cleos create key
生成。另外创建钱包时,需要将生成的密码保存到文件中,以免忘记密码而导致钱包无法解锁。
wallet=<钱包名称>
pwd_file=<保存密码的文件>
private_key=<私钥>
cleos wallet create -n $wallet --to-console > $pwd_file
password=$(cat $pwd_file |grep "\"" |awk -F "\"" '{ print $2 }')
cat $password > $pwd_file
cleos wallet import -n $wallet $private_key
4. 打开钱包,解锁钱包
这是一个很烦恼的问题,每次交易的时候都会弹出需要解锁的提示,当你下一次交易时,你可以先解锁的时候,又会报错:提示已经解锁。虽然没什么,但是很烦。。。
以下是命令:
cleos wallet open -n <钱包名称>
cleos wallet unlock -n <钱包名称> --password <密码>
如果是默认(default
)钱包,就不需要加-n
参数了
5. 创建账号
https://monitor.jungletestnet.io/#account
进入网站后,只需要填写已注册的合约账号和公钥,然后验证,最后点击确认就可以
这里作为测试,owner
和active
的公钥可以填写为一样的。
当然,也可以通过命令来创建账号:
$ cleos system newaccount \
--stake-net '<网络资源要抵押的EOS数量> EOS' \
--stake-cpu 'cpu资源要抵押的EOS数量 EOS' \
--buy-ram-kbytes <要购买的ram字节数量> \
<金主账号> <新账号> <新账号公钥>
其中buy-ram-kbytes
是字节,不需要带单位
6. 获取eos
https://monitor.jungletestnet.io/#faucet
进入网站后,只需要填写已注册的合约账号,然后验证,最后点击确认就可以获取EOS了,当然这是测试网的。
值得注意的是,同一个账号6小时内只有一次成功获取到EOS。这个可以提前获取,这样才能够已经部署合约,脚本执行过程中,不可能去获取EOS,这样就算不上是一键部署
了。
7. 购买cpu、net资源,购买ram资源
如果要部署合约,需要购买cpu等资源,否则的话,会以部署失败告终,同时需要注意,购买的资源不够,同样会部署失败。
另外购买资源的账号与合约账号不一定是同一个账号,所以可以专门提供一个提供购买资源的账号,也就是金主
,提前充值EOS到合约账号,这样就不需要中途去获取EOS,从而达到一键部署
的目的。
购买cpu、net资源
如果只需要购买net资源,cpu资源抵押的EOS数量可为:0.0000
,同理,只需要购买cpu资源类似。
$ cleos system delegatebw <金主账号> <新账号> "<网络资源要抵押的EOS数量> EOS" "<cpu资源要抵押的EOS数量> EOS"
购买ram
这里要提的一点是,填写的是bytes
数量,所以不需要带单位。
$ cleos system buyram -k <金主账号> <新账号> <bytes数量>
8. 下载和编译合约、将合约复制到docker实例目录下
下载合约:
如果合约在本地,就不需要下载合约了,这里以git
为例:
if [ -d "<合约目录>" ];then
cd <合约目录>/<合约名称>
git pull
else
mkdir <合约目录>
cd <合约目录>
git clone <git地址>
fi
如果需要切换分支:
这里的分支名称
是指本地的分支名称
git checkout <分支名称>
由于采用的是docker,所以需要考虑到合约路径的问题。需要将合约复制到docker目录下。
首先需要获取实例ID:
$ docker inspect -f '{{.ID}}' keosd
复制合约:
doc_id=$(docker inspect -f '{{.ID}}' keosd)
docker cp <当前合约路径> $doc_id:<实例目标合约路径>
9. 设置合约
设置新账号有一个关键的地方,就是合约路径。
这里的合约路径并不是本地的路径,而是docker实例中的路径,可以使用 docker exec -it keosd bash
命令,进入实例中查看合约所在的具体路径。
合约目录中需要包含*.wasm
以及*.abi
文件。
$ cleos set contract <新账号> <合约路径> -p <新账号>@active
10. 授权
这里的“授权”,是指使用户的账号能够拥有提现的权限。如果不需要提现功能的话,该步骤可以不做。
由于合约账号才拥有将EOS转出的权限,因此,合约如果要实现提现功能,似乎不可能,总不能把合约账号的私钥提供给用户吧。但是以下命令可以解决这个问题,合约提供提现的action
方法,用户只需要自己的权限就可以提现。
$ cleos set account permission <新账号> active \
'{ \
"threshold": 1, \
"keys": [{"key": "<新账号>", "weight": 1}], \
"accounts": [ \
{ \
"permission": { "actor":"<新账号>","permission":"eosio.code" },
"weight":1 \
} \
] \
}' \
owner -p ${新账号}
脚本具体代码
代码放在github上了:
https://github.com/xiaoyifan6/soeth/blob/master/tools/eos/build.sh
另外介绍一下用法:
创建deploy.sh
, 以下为空字符串的都是需要配置的变量,其中*_money
为0,则表示不会进行此项购买或者抵押操作。
另外account
和new_account
可以是同一个,前提是账号已经创建并且有充足的EOS。另外第二次部署合约,则为更新合约,若合约有改表或者action
方法名称,可能需要消耗额外的ram,若abi文件没有变化,则不会消耗额外的ram。
contract_path
这里是指合约的相对路径,相对于脚本所在目录的contracts
目录。
#!/bin/bash
curpath=$(cd "$(dirname "$0")"; pwd)
account="" # 金主账号
new_account="" # 合约账号名称
contract_name="" # 合约名称
wallet="" # 钱包名称
ram_money="0" # 部署钱包需要的金额 1400
cpu_money="0" # 部署钱包需要的金额 4
net_money="0" # 部署钱包需要的金额 2
git_url="" # 合约git地址
private_key="" # 私钥
public_key="" # 公钥
contract_path="" # 合约相对路径
password="" #钱包密码
url="https://jungle.eosn.io:443"
# 切换到master分支
cd $curpath/contracts/$contract_name/
git checkout master
bash $curpath/build.sh \
-a $account \
-n $new_account \
-g $git_url \
-w $wallet \
-C $contract_name \
-P $password \
-p $contract_path \
-k $private_key \
-K $public_key \
-u $url \
-r $ram_money \
-c $cpu_money \
-N $net_money
# -i #初始化
延伸
1. 获取chainId
以下命令可以直接得到当前节点的信息,其中包括chain_id
$ cleos get info
2. Scatter
:切换账号
切换账号之前,需要先忘记账号。
var _scatter = window['scatter'];
_scatter && _scatter.forgetIdentity();
var defaultAccount = null;
var netwok = {
blockchain: 'eos',
host: <IP地址或url>,
port: <端口号>,
protocol: "<http|https>",
chainId: "<ChainID>"
};
setTimeout(function () {
_scatter.getIdentity({
accounts: [netwok]
}).then(res => {
if (!res) return reject();
var identity: Identity = res;
var _account = identity.accounts.find((accound) => {
return accound.blockchain == "eos";
});
defaultAccount = _account;
}).catch(res => { });
}, 1000);
3. eosjs
:获取账号信息,并计算net
、cpu
、ram
价格
首先初始化eos
:
var netwok = {
blockchain: 'eos',
host: <IP地址或url>,
port: <端口号>,
protocol: "<http|https>",
chainId: "<ChainID>"
};
var eos =_scatter.eos(netwok, Eos, {}, <http|https>);
formatEos
方法:
这里涉及到精度问题,这里只保留了6位小数。
function formatEos(value) {
return parseFloat(parseFloat(value).toFixed(6));
}
getTableRows
方法参考下面提到的分页查询。
计算net、cpu价格
account
为账号,由于接收账号和付款账号可以是同一个,所以这里都填写为account
。这里可以是defaultAccount.name
。
async function getNetResourceGetBandwidthPrice(account) {
var res await eos.getAccount(account);
var netPrice = 0;
var cpuPrice = 0;
if (res) {
//1. 计算NET价格
//抵押NET的EOS数量
var netBalance = res.net_weight / 10000;
//NET贷款的总量
var netTotal = res.net_limit.max / 1024;
//(netBalance / netTotal)获取到的是过去3天内的平均消耗量,除以3获取每天的平均消耗量,即价格
netPrice = ((netBalance / netTotal) / 3);
console.log(netBalance, netTotal, netPrice)
//1. 计算CPU价格
//抵押CPU的EOS数量
var cpuBalance = res.cpu_weight / 10000;
//CPU贷款的总量
var cpuTotal = res.cpu_limit.max / 1024;
//(cpuBalance / cpuTotal)获取到的是过去3天内的平均消耗量,除以3获取每天的平均消耗量,即价格
cpuPrice = ((cpuBalance / cpuTotal) / 3);
}
return {
netPrice: netPrice,
cpuPrice: cpuPrice,
};
}
计算ram价格
这里价格单位为:EOS/KB
async function getRamPrice() {
var res = await getTableRows("eosio", "rammarket", "eosio",1,0);
if (res && res.rows && res.rows.length > 0) {
return formatEos(res.rows[0].quote.balance) / formatEos(res.rows[0].base.balance) * 1024;
}
return 0;
}
4. eosjs
:购买net
、cpu
、ram
首先,得写一个调用活动的方法:
defaultAccount
:前面的账号切换
提到defaultAccount
的获取方法。
function doAction(contractName, actionName, ...param) {
return new Promise((resolve, reject) => {
const account = defaultAccount;
const options = {
authorization: [`${account.name}@${account.authority}`]
};
eos.contract(contractName).then(contract => {
contract[actionName].apply(window, param.concat(options)).then(res2 => {
resolve(res2);
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
});
}
购买net
、cpu
这里的net_amount
为购买网络资源需要抵押的EOS数量,cpu_amount
为购买cpu资源需要抵押的EOS数量
account
为账号,由于接收账号和付款账号可以是同一个,所以这里都填写为account
。这里可以是defaultAccount.name
。
async function delegatebw(account, net_amount, cpu_amount) {
let flag = true;
try {
var res = await doAction("eosio", "delegatebw", account, account,
`${net_amount.toFixed(4)} EOS`, `${cpu_amount.toFixed(4)} EOS`, 0);
return res;
} catch (e) {
throw e;
}
}
购买ram
这里ramAmount
的单位是字节,即要购买的字节数量。
首先初始化eos
:上面的获取账号信息
有提到。
account
为账号,由于接收账号和付款账号可以是同一个,所以这里都填写为account
。这里可以是defaultAccount.name
。
async function buyRam(account, ramAmount) {
let flag = true;
try {
var res = await doAction("eosio", "buyrambytes", account, account, ramAmount);
return res;
} catch (e) {
throw e;
}
}
5. eosjs
:关于分页获取合约表格数据
分页查询一般有两种方式,一个是知道第几页
以及每页大小
或者其起始索引
以及每页大小。
而今天讲的是后者。
首先初始化eos
:上面的获取账号信息
有提到。
然后编写函数,其中limit
为每页大小,index_position
为起始位置
async function getTableRows(contractName, table, scope, limit, index_position) {
try {
var start = new Date().getTime();
var flag = true;
var param = {
code: contractName,
scope: scope,
table: table,
json: true,
lower_bound: index_position,
};
limit && (param["limit"] = limit);
if (!eos) {
return {
rows: [],
more: false
};
}
return await eos.getTableRows(param);
} catch (e) {
throw e;
}
}
6. eosjs
:关于uint64_t
的编码与解码
uint64_t
在合约中可以将字符串类型存储到表中,而这时候客户端获取到的是一个数值,通过解码后,可以得到相应的字符串。
function encode(value) {
return Eos["modules"].format.encodeName(value, false);
}
function decode(value) {
return Eos["modules"].format.decodeName(value.toString(), false)
}