最近在做一个简单的捐赠平台的DAPP,做一下笔记。DAPP的需求如下:
- 用户可捐赠指定数量的ZJUBCA(协会token的symbol)和EOS数量。先存入denote合约(本合约)账户,在满足一定数量后自动转入协会基金账户。
- 显示当前已收到捐赠的ZJUBCA和EOS总数。
- 显示当前用户已捐赠的ZJUBCA数量和EOS数量。
- 捐赠排行榜。对已进行捐赠的用户按捐赠数量进行排名。
基本思路
合约需要做的事比较简单,现在对上图做简单的解释。
- 用户可以捐赠ZJUBCA和EOS给donate合约,将捐赠不同的token会触发不同的合约内action(donatezjubca和donateeos),触发action之后,合约会更新区块链上相应的表(multi_index)并且向合约进行一次转账。
- 合约定期检查自己账户内ZJUBCA和EOS数量是否达到一定数量,将其转出到协会基金会中(ZJUBCA),并且更新区块链中的表。
代码详解
zjubca.donate.hpp
#pragma once
//包含需要的头文件
#include <eosiolib/eosio.hpp>
#include <eosiolib/asset.hpp>
#include <eosiolib/symbol.hpp>
#include <string>
//宏定义 MAX_ZJUBCA_QUANTITY用于检查合约账户中ZJUBCA数量是否超过10000.0000 ZJUBCA
//MAX_EOS_QUANTITY 用于检查合约账户中EOS数量是否超过1000.000 EOS (此处ZJUBCA精度是4位,EOS是三位)
#define MAX_ZJUBCA_QUANTITY 100000000
#define MAX_EOS_QUANTITY 1000000
using namespace eosio;
using namespace std;
namespace eosiosystem {
class system_contract;
}
//定义zjubca.donate合约这个类
class [[eosio::contract("zjubca.donate")]] Donate : public contract
{
private:
//定义donator表的结构,该表用于存储捐赠者的基本信息
//主键是donator_name,类型是name,ZJUBCA_amount和EOS_amount用于保存该用户总共捐赠的ZJUBCA和EOS数量,类型是assset
// @abi table doantor
struct [[eosio::table]] donator {
name donator_name;
asset ZJUBCA_amount;
asset EOS_amount;
auto primary_key() const {return donator_name.value;} //声明主键是donator_name.value
};
//定义recipient表的结构,该表用于存储该合约的基本信息
//主键是recipient_name,类型是name,(其实这个数据只有本合约)ZJUBCA_amount和EOS_amount用于保存本合约总共收到捐赠的ZJUBCA和EOS数量,类型是assset
// @abi table recipient
struct [[eosio::table]] recipient {
name recipient_name;
asset ZJUBCA_amount;
asset EOS_amount;
auto primary_key() const {return recipient_name.value;} 声明主键是recipient_name.value
};
//定义foundation表的结构,该表用于存储基金会的基本信息,和recipient类似
// @abi table foundation
struct [[eosio::table]] foundation {
name foundation_name;
asset ZJUBCA_amount;
asset EOS_amount;
auto primary_key() const {return foundation_name.value;}
};
//mutil_index<"donators"_n, donator>是mutil_index的初始化, (_donators是一个类)现在我们可以拿着_donators去实例化一张表
typedef multi_index<"donators"_n, donator> _donators;
typedef multi_index<"recipients"_n, recipient> _recipients;
typedef multi_index<"foundation"_n, foundation> _foundation;
_donators donator; //实例化donator这张表
_recipients recipient; //实例化recipent这张表
_foundation foundation; //实例化foundation这张表
public:
using contract::contract;
//构造函数以及三张表的初始化
Donate(name receiver, name code, datastream<const char*> ds):contract(receiver, code, ds),
donator(receiver, code.value), recipient(receiver, code.value), foundation(receiver, code.value){}
//声明各个action
[[eosio::action]] void start(); //合约开始的时候要执行start, 向recipient和foundation这两张表中插入数据
[[eosio::action]] void end(); //删除表,需要时可以执行
[[eosio::action]] void donatezjubca(name from, name to, asset quantity, string memo); //用户捐赠ZJUBCA给合约
[[eosio::action]] void donateeos(name from, name to, asset quantity, string memo); //用户捐赠EOS给合约
[[eosio::action]] void sendtofound(name from, name to, string memo); //检查合约账户中EOS数量是否超过1000.000 EOS,以及ZJUBCA数量是否超过10000.0000 ZJUBCA,然后将其转到基金会
};
因为合约中捐赠操作涉及到token的转账,转账就涉及到合约的安全性问题,在现在的合约中,已经不使用eosio.code这个授权的方式进行转账了,大部分合约都是使用通知(require_recipient)的方式来进行转账。
具体操作如下:
- 用户直接向合约账户转账,memo用于传递指定参数,如下所示,chen这个用户通过transfer操作向合约地址转了100.000 EOS,并且memo参数为donateeos。
cleos push action eosio.token transfer '["chen", "donate", "100.000 EOS", "donateeos"]' -p chen@active
- eosio.token的transfer中包含以下两行代码,其中require_recipient(to)就是用来通知合约,也是合约的入口,会进入到合约代码中的apply函数中,apply函数见下面的代码。
require_recipient(from);
require_recipient(to);
- 进入到合约 的apply函数中后,通过判断即可以进入相应的action。
画了一张过程图,如下所示:
zjubca.donate.cpp
#include "zjubca.donate.hpp"
void Donate::start()
{
//确保只有本合约才有权力执行start这个action
require_auth(_self);
name recipient_account = name("donate");
name foundation_account = name("zjubca");
//查找recipient表中是否已经存在donate这个字段
auto existing1 = recipient.find(recipient_account.value);
//查找foundation表中是否已经存在zjubca这个字段
auto existing2 = foundation.find(foundation_account.value);
if (existing1 == recipient.end())
{
//如果没找到,插入初始化数据
recipient.emplace(_self, [&](auto &new_recipient) {
new_recipient.recipient_name = recipient_account;
new_recipient.ZJUBCA_amount = asset(0, symbol("ZJUBCA", 4));
new_recipient.EOS_amount = asset(0, symbol("EOS", 3));
});
}
if (existing2 == foundation.end())
{
//如果没有找到,插入初始化数据
foundation.emplace(_self, [&](auto &new_foundation) {
new_foundation.foundation_name = foundation_account;
new_foundation.ZJUBCA_amount = asset(0, symbol("ZJUBCA", 4));
new_foundation.EOS_amount = asset(0, symbol("EOS", 3));
});
}
}
void Donate::donatezjubca(name from, name to, asset quantity, string memo)
{
//建议先看apply函数(代码最后)中的注释代码
//检查memo是否是"donatezjubca",防止其他转账操作而进入本action
if (memo == "donatezjubca")
{
//进行一些assert操作
eosio_assert(from != to, "cannot transfer to self");
require_auth(from);
//接收ZJUBCA的地址只能是本合约
eosio_assert(to == _self, "only can donate to the contract");
auto sym = quantity.symbol;
eosio_assert(sym.is_valid(), "invalid symbol name");
eosio_assert(sym == symbol("ZJUBCA", 4), "invalid symbol name");
eosio_assert(quantity.is_valid(), "invalid quantity");
eosio_assert(quantity.amount > 0, "must transfer positive quantity");
eosio_assert(memo.size() <= 256, "memo has more than 256 bytes");
//先从donators表中查询捐赠者
auto existing1 = donator.find(from.value);
if (existing1 == donator.end())
{
//不存在就在新增一个用户
donator.emplace(_self, [&](auto &new_donator) {
new_donator.donator_name = from;
new_donator.ZJUBCA_amount = quantity;
symbol sym = symbol("EOS", 3);
auto zero = asset(0, sym);
new_donator.EOS_amount = zero;
});
}
else
{
//若已经存在,则修改用户数据
donator.modify(existing1, same_payer, [&](auto &content) {
content.ZJUBCA_amount += quantity;
});
}
auto existing2 = recipient.find(to.value);
if (existing2 != recipient.end())
{
//修改recipient表中数据
recipient.modify(existing2, same_payer, [&](auto &content) {
content.ZJUBCA_amount += quantity;
});
}
}
}
void Donate::donateeos(name from, name to, asset quantity, string memo)
{
//和doantezjubca逻辑类似
if (memo == "donateeos")
{
eosio_assert(from != to, "cannot transfer to self");
require_auth(from);
eosio_assert(to == _self, "only can donate to the contract");
auto sym = quantity.symbol;
eosio_assert(sym.is_valid(), "invalid symbol name");
eosio_assert(sym == symbol("EOS", 3), "invalid symbol name");
eosio_assert(quantity.is_valid(), "invalid quantity");
eosio_assert(quantity.amount > 0, "must transfer positive quantity");
eosio_assert(memo.size() <= 256, "memo has more than 256 bytes");
auto existing1 = donator.find(from.value);
if (existing1 == donator.end())
{
donator.emplace(_self, [&](auto &new_donator) {
new_donator.donator_name = from;
new_donator.EOS_amount = quantity;
symbol sym = symbol("ZJUBCA", 4);
auto zero = asset(0, sym);
new_donator.ZJUBCA_amount = zero;
});
}
else
{
donator.modify(existing1, same_payer, [&](auto &content) {
content.EOS_amount += quantity;
});
}
auto existing2 = recipient.find(to.value);
if (existing2 != recipient.end())
{
recipient.modify(existing2, same_payer, [&](auto &content) {
content.EOS_amount += quantity;
});
}
}
}
void Donate::end()
{
//删除表操作
auto donator_begin_it = donator.begin();
while (donator_begin_it != donator.end())
{
donator_begin_it = donator.erase(donator_begin_it);
}
auto recipient_begin_it = recipient.begin();
while (recipient_begin_it != recipient.end())
{
recipient_begin_it = recipient.erase(recipient_begin_it);
}
auto foundation_begin_it = foundation.begin();
while (foundation_begin_it != foundation.end())
{
foundation_begin_it = foundation.erase(foundation_begin_it);
}
}
void Donate::sendtofound(name from, name to, string memo)
{
//assert
eosio_assert(from != to, "cannot transfer to self");
require_auth(from);
eosio_assert(from == _self, "invalid transfer account");
eosio_assert(to == name("zjubca"), "invalid transfer account");
asset ZJUBCA_quantity = asset(MAX_ZJUBCA_QUANTITY, symbol("ZJUBCA", 4));
asset EOS_quantity = asset(MAX_EOS_QUANTITY, symbol("EOS", 3));
auto existing1 = recipient.find(from.value);
if (existing1 != recipient.end())
{
const auto &st1 = *existing1;
if (st1.ZJUBCA_amount >= ZJUBCA_quantity)
{
//查表并且检查合约地址中token数量是否发超过一定限额
recipient.modify(st1, from, [&](auto &content) {
content.ZJUBCA_amount -= ZJUBCA_quantity;
});
auto existing2 = foundation.find(to.value);
if (existing2 != foundation.end())
{
const auto &st2 = *existing2;
foundation.modify(st2, from, [&](auto &content) {
content.ZJUBCA_amount += ZJUBCA_quantity;
});
}
action(
permission_level{from, "active"_n},
"zjubca.token"_n,
"transfer"_n,
std::make_tuple(from, to, ZJUBCA_quantity, memo))
.send();
}
if (st1.EOS_amount >= EOS_quantity)
{
//逻辑同上
recipient.modify(st1, from, [&](auto &content) {
content.EOS_amount -= EOS_quantity;
});
auto existing2 = foundation.find(to.value);
if (existing2 != foundation.end())
{
const auto &st2 = *existing2;
foundation.modify(st2, from, [&](auto &content) {
content.EOS_amount += EOS_quantity;
});
}
//此处由于是合约将自己的token进行转出,所以可以直接使用inline action操作,不存在安全性问题
action(
permission_level{from, "active"_n},
"eosio.token"_n,
"transfer"_n,
std::make_tuple(from, to, EOS_quantity, memo))
.send();
}
}
}
// EOSIO_DISPATCH(Donate, (start)(donatezjubca)(donateeos)(sendtofound)(end))
extern "C"
{
//apply函数是所有EOS合约的入口
//在EOS中,合约之间是可以相互调用的,假设A合约在B这个action中调用了C合约,那么receiver就是C合约,code就是A合约,action就是B这个action
void apply(uint64_t receiver, uint64_t code, uint64_t action)
{
//判断是action的入口是本合约还是别的合约
//如果调用方和被调用方式相同的,并且不是从transfer这个action进入的
if (code == receiver && action != "transfer"_n.value)
{
switch (action)
{
//进入不同的action
case "start"_n.value:
execute_action(name(receiver), name(code), &Donate::start); //使用execute_action进入相应的action
break;
case "end"_n.value:
execute_action(name(receiver), name(code), &Donate::end);
break;
case "sendtofound"_n.value:
execute_action(name(receiver), name(code), &Donate::sendtofound);
break;
default:
break;
}
}
//code == zjubca.token和code == eosio.token意为调用本合约的合约为zjubca.token和eosio.token,并且是从transfer这个action进入的
else if ((code == "zjubca.token"_n.value || code == "eosio.token"_n.value) && action == "transfer"_n.value)
{
switch (code)
{
//如果是zjubca.token进入本合约,进行donatezjubca这个action
case "zjubca.token"_n.value:
execute_action(name(receiver), name(receiver), &Donate::donatezjubca);
break;
//如果是eosio.token进入本合约,进行donateeos这个action
case "eosio.token"_n.value:
execute_action(name(receiver), name(receiver), &Donate::donateeos);
break;
default:
break;
}
}
}
};