美文网首页区块链DAPP
【DAPP开发三】发布合约及实践

【DAPP开发三】发布合约及实践

作者: GeniusWong | 来源:发表于2018-09-25 15:49 被阅读48次

4.1.1 语法介绍 block/msg/now

  • block 在调用某个方法的时候,solidity 会提供一个block 的变量,把当前块的信息返回。
    block.blockhash(uint blockNumber) returns (bytes32) 给定块的哈希 - 仅适用于256个不包括当前最新块
    block.coinbase (address) 当前块矿工地址
    block.difficulty (uint) 当前块难度
    block.gaslimit (uint) 当前块gaslimit
    block.number (uint) 当前数据块号
    block.timestamp (uint) 当前块时间戳从unix纪元开始为秒
  • msg 在调用某个方法的时候,会给方法传递一个msg的属性,用来传递消息
    msg.data (bytes) 完整的 calldata
    msg.gas (uint) 剩余gas
    msg.sender (address) 该消息(当前呼叫)的发送者
    msg.sig (bytes4) 呼叫数据的前四个字节(即功能标识符)
    msg.value (uint) 发送的消息的数量
  • now (uint) 当前块时间戳(block.timestamp的别名)

4.1.2 存储 storage / memory

Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,
内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。大多数时候你都用不到这
些关键字,默认情况下 Solidity 会自动处理它。 状态变量(在函数之外声明的变量)默认为“存储”形式,
并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。  

4.1.3 随机数

生成一个0-100 之间的随机数

function randomUtils() public view returns(uint) {
      //定义数字。
      uint random=1;
      //基于数字的基础上生成随机数.
      //keccak256 生成数.
      return uint(keccak256(now,msg.sender,random)) % 100;
}

4.1.4 require 关键字

     require(keccak256(_name) == keccak256("Vitalik"));
     require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:
     用来比较两个字符串是否相等.
     function equals(string str, string str1) public pure returns (bool) {
          return (keccak256(str) == keccak256(str1));
    }

4.1.5 modify


pragma solidity^0.4.19;
contract ModifyContract {
     address owner;  
     //定义占位符,可以把需要判断的条件共性的地方进行抽取。          
     modifier onlyOwner() {
            require(owner == msg.sender);
            _;      
     }
     //在调用方法之前会先进行检查.
     function getRightToVote() public  onlyOwner {

     }
}

4.2发布合约

使用 Remix IDE,写入代码后,选择 solc 版本,再进行编译。然后到 Run Tab页,选择好 环境,账户 等信息,设置 Gas Limit (一般 Remix 会自动设置一个值),点击 Create 发布合约。发布合约需要花费 Gas。
合约发布之后,用户可以点击合约函数,执行函数。有一些函数需要 Gas,而有一些函数则不需要 Gas。

4.5调试

进入 Debugger Tab 页,可以进行调试。

5 solidity 急行军

案例1 转账给智能合约账户

当一个智能合约运行时,它运行在以太坊上,任何人都可以调用函数,向智能合约转钱

pragma solidity^0.4.24;
contract Money {
     function Money(){
     }
     //向智能合约账户转钱
     function paymoney () payable public{    
     }  
     function getBalance() public view returns (uint) {
            return address(this).balance;
     }   
}

案例2 从智能合约账户取钱

这个例子,展示如何从智能合约账户取钱,在这个例子里,取钱没有任何条件,
只要合约账户中有钱,就可以取出这部分钱来

pragma solidity^0.4.19;

contract GetMoney {
      //合约发布者
      address owner;
      //发布合约的时候会调用构造函数.
      function GetMoney() public {
           owner = msg.sender;
      }
      //向合约账户转钱。
      function payMoney() payable public {

      }
      //查看智能合约账户的余额  
      function getBalance() public view returns(uint) {
             return address(this).balance;
      }
      //谁调用就往谁的账户打钱。从智能转化里面转钱.
      function getMoney() public {
            address who = msg.sender;
            if(getBalance() > 2 ether) {
                 who.transfer(2 ether);
            }
      }
      //销毁合约.
      function kill() public {
           //判断操作,如果合约的发布者是调用着,则有权限销毁合约.
           if(owner ==  msg.sender) {
                 selfdestruct(msg.sender);
           }
      }              
}

案例3 土豪发红包

```JavaScript
pragma solidity^0.4.19;

contract RedPacket {
     //设置土豪.
     address tuhao;
     //初始化红包的个数.
     int number;
     //初始化相关数据.
     function RedPacket(int _number) public payable{
          tuhao = msg.sender;
          number = _number;
     }
     //获取合约的余额.
     function getBalance() public view returns(uint){
           return address(this).balance;  
     }
     //抢红包
     function stakeMoney() public payable returns(bool) {
           address who = msg.sender;
           if(number > 0) {
               number --;
                uint random = uint(keccak256(now,msg.sender,10)) % 100;
               uint balance = getBalance();
               who.transfer(balance * random / 100);
               return true;
           }
           return false;
     }
     //destory contract
     function kill() public {
          if(tuhao == msg.sender) {
                selfdestruct(tuhao);
          }
     }
}
```

案例4 博彩赌大小.

pragma solidity^0.4.19;

contract Bet {

     address owner; //contract manager

     struct Player {
          address addr;
          uint money;
     }

     Player [] inbig;
     Player [] insmall;

     uint blockNumber;
     uint totalBig;
     uint totalSmall;

     function Bet() public {
          owner = msg.sender;
          blockNumber = block.number;
          totalBig = 0;
          totalSmall = 0;
     }

     function getBalance() public view returns(uint) {
          return address(this).balance;
     }

     function getBlockNumber() public view returns(uint,uint){
           return (blockNumber,block.number);
     }

     //押注 大小.
     function stake(bool flag) public view returns (bool){
           //先结构化玩家.
           Player memory player  = Player(msg.sender,msg.value);
           //玩家是否带钱过来.  
           if(player.money > 0){
                return false;
           }
           //押注大的
           if(flag){
                 inbig.push(player);
                 totalBig +=player.money;
           }else{ //押注小的.
                 insmall.push(player);
                 totalSmall +=player.money;
           }
           return true;
     }

     //开奖。
     function open()  payable public  returns (bool) {
            //设置开奖限制,必须最少得有两个人押注.
            if(block.number < 2+blockNumber) {
                return false;
            }
            //押注大大金额以及押注小大金额大比例。
            if(totalSmall == 0 && totalBig == 0){
                return false;
            }
            //计算开大开小的规则,根据当前块的hash 值去定.
            uint hash =  uint(block.blockhash(block.number));
            uint points = hash % 18;     
            uint i=0;    
            uint count;  
            Player memory player;
            if(points>9) {  // big winer
                  count=inbig.length;
                  for(i=0;i<count;i++){
                         player = inbig[I];
                         player.addr.transfer(player.money+totalSmall*player.money/totalBig);
                  }
            }else { // small winer
                  count = insmall.length;
                  for(i=0;i<count;i++){
                       player = insmall[I];
                       player.addr.transfer(player.money+totalBig*player.money/totalSmall);
                  }
            }
            return true;
     }
     //销毁合约
     function kill() public {
          if(msg.sender == owner){
              selfdestruct(owner);
          }
     }
}

案例5 社区投票

需求:
投票,委托代理人投票,给某个voter 赋予投票的权利
获取到最高的选题Prososal的name

//合约名
contract Ballot {
    // 投票者结构体
    struct Voter {
        uint weight; // 份额(既拥有多少票)
        bool voted;  // 是否已经投票
        address delegate; // 委任谁进行投票
        uint vote;   // 第几个投票议案
    }

    // 议案机构体
    struct Proposal {
        bytes32 name;   // 议案名(最多32字节)
        uint voteCount; // 累计投票数
    }

    address public chairperson; //投票主持人

    // 投票者与其地址的映射
    mapping(address => Voter) public voters;

    // 提案指针
    Proposal[] public proposals;

    /// Ballot函数,创建新投票,输入多个议案名
    function Ballot(bytes32[] proposalNames) public {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        for (uint i = 0; i < proposalNames.length; i++) {
            // `Proposal({...})` 创建一个临时Proposal对象
            proposals.push(Proposal({
                name: proposalNames[I],
                voteCount: 0
            }));
        }
    }

    //输入投票者地址,给予投票者投票权限
    function giveRightToVote(address voter) public {
        // require防止函数被错误调用,判断为错误时终止调用
        // 并恢复到调用前的状态,但是注意会消耗gas
        require(
            (msg.sender == chairperson) &&
            !voters[voter].voted &&
            (voters[voter].weight == 0)
        );
        voters[voter].weight = 1;
    }

    /// 输入他人地址,委任他人投票
    function delegate(address to) public {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted);

        // 不允许委任自己
        require(to != msg.sender);

        // 循环委任直至被委任人不再委任他人,
        // 但注意这种循环是危险的,有可能耗尽gas来进行计算。
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            // 避免循环委任,形成委任环链
            require(to != msg.sender);
        }

        // 此处sender为voters[msg.sender]
        sender.voted = true;
        sender.delegate = to;
        Voter storage delegate_ = voters[to];
        if (delegate_.voted) {
            // 如果被委任人已经投票,直接增加该议案票数
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // 如果被委任人未投票,增加被委任人持有票数
            delegate_.weight += sender.weight;
        }
    }
    /// 向议案投票
    function vote(uint proposal) public {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted);
        sender.voted = true;
        sender.vote = proposal;

        // 提案超出数组范围时自动中断并恢复所以修改
        proposals[proposal].voteCount += sender.weight;
    }
    /// 返回获得票数最高的议案索引
    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }
    // 返回获得票数最高的议案名
    function winnerName() public view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

案例6 竞拍 (代码)

contract SimpleAuction {
    
    address public beneficiary;
    //竞拍开始
    uint public auctionStart;
    uint public biddingTime;

    //当前的拍卖状态
    address public highestBidder;
    uint public highestBid;

   //在结束时设置为true来拒绝任何改变
    bool ended;

   //当改变时将会触发的Event
    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    //下面是一个叫做natspec的特殊注释,
    //由3个连续的斜杠标记,当询问用户确认交易事务时将显示。

    ///创建一个简单的合约使用`_biddingTime`表示的竞拍时间,
   /// 地址`_beneficiary`.代表实际的拍卖者
    function SimpleAuction(uint _biddingTime,
                           address _beneficiary) {
        beneficiary = _beneficiary;
        auctionStart = now;
        biddingTime = _biddingTime;
    }

    ///对拍卖的竞拍保证金会随着交易事务一起发送,
   ///只有在竞拍失败的时候才会退回
    function bid() {

       //不需要任何参数,所有的信息已经是交易事务的一部分
        if (now > auctionStart + biddingTime)
           //当竞拍结束时撤销此调用
            throw;
        if (msg.value <= highestBid)
           //如果出价不是最高的,发回竞拍保证金。
            throw;
        if (highestBidder != 0)
            highestBidder.send(highestBid);
        highestBidder = msg.sender;
        highestBid = msg.value;
        HighestBidIncreased(msg.sender, msg.value);
    }

   ///拍卖结束后发送最高的竞价到拍卖人
    function auctionEnd() {
        if (now >= auctionStart + biddingTime)
            throw; 
            //拍卖还没有结束
        if (ended)
            throw; 
     //这个收款函数已经被调用了
        AuctionEnded(highestBidder, highestBid);
        //发送合约拥有所有的钱,因为有一些保证金可能退回失败了。

        beneficiary.send(this.balance);
        ended = true;
    }

    function () {
        //这个函数将会在发送到合约的交易事务包含无效数据
        //或无数据的时执行,这里撤销所有的发送,
        //所以没有人会在使用合约时因为意外而丢钱。
        throw;
    }
}

博彩游戏

需求部分:

玩法规则

【缺图】

合约需求
  • 押注
  • 开奖
  • 计算赔率
  • 领取奖励

合约代码实例

pragma solidity ^0.4.19;
contract LotteryBase {
    address public manager; // 彩票管理员
    string public name; // 彩票名称
    uint public baseBlock; // 当前售卖到的彩票期数 基准区块,也是彩票的期数 10
    uint public stopBlock; // 彩票从当前块数+到特定块数这一期间为售卖期间 关闭彩票售卖通道区块 100
    uint public openBlock; // 到了特定期间后再+到特定期间为开奖期间 开奖区块 110
    //开奖期数号码记录,用于彩民备查
    struct OpenCode {
        uint block; // 等于 baseBlock
        uint8[] opencode; // 开奖号码
    }
    // 所有开奖号码集合
    OpenCode[] public allOpenCodes;
    // 奖金账本,记录每一个用户的奖金
    mapping(address=>uint) public bonus; // 奖金
    // 所有中奖用户的奖金综合
    uint public totalBonus; // 总奖金金额,奖池金额 = 余额 - 奖金总金额

    function LotteryBase () public {
        manager = msg.sender;
    }

    modifier onlyManager () {
        require (manager == msg.sender);
        _;
    }

    // 两个抽象接口
  //  function chipin(uint8[] number, uint8 multi) public payable;
    //function open() public;

    // 未分配奖池的奖金金额
    function getBonusPool() public view returns (uint) {
        return address(this).balance - totalBonus;
    }
}

contract KuaiSan is LotteryBase {

     //不同下注不同赔率列表
    uint8[19] public odds=[0,0,0,240,80,40,25,16,12,10,9,9,10,12,16,25,40,80,240];
    //定义下注方式的枚举类型
    enum InjectionType {TypeSum, TypeSameThree,TypeSameThreeSingle,TypeSameTwo,TypeSameTwoSingle,TypeNoSameThree,TypeNoSameTwo,TypeThreeConsecutive }
    //购买彩票的结构体
    struct Order {
        address player;
        uint8[] number; // 彩票号码
        uint8 multi;
        InjectionType injectionType;//投注类型
        uint8 sumVal;//如果是和值类型,那么该值为下注的和值
    }
    // 订单集合
    Order[] orders;

    // 彩票价格,大约2人民币
    uint constant fee = 0.001 ether;
    uint constant stopInterval = 30; //设置停止间隔
    uint constant openInterval = 40; //设置开奖间隔
    uint constant codeAOffset = 33; //这只当前区块+33的hash值给A数
    uint constant codeBOffset = 34;//这只当前区块+33的hash值给B数
    uint constant codeCOffset = 35;//这只当前区块+33的hash值给C数

    function KuaiSan() public {  //构造函数,开始竞猜
        // 开奖将基于 baseBlock + 33, baseBlock + 34, baseBlock + 35
        baseBlock = block.number; //将当前区块设置到baseBlock中
        stopBlock = baseBlock + stopInterval; //设置停止区块为当前区块+开奖间隔 about 450 seconds
        openBlock = baseBlock + openInterval; //设置开奖区块为当前区块+开奖区块 about 150 seconds

        name = "中国福利彩票北京快三";
    }

    // 下注,玩家提供一个号码和倍数,记录到订单中
    // number是玩家购买的号码,multi是玩家要购买的倍数,injType为玩家下注方式,_sumVal是和值类型的和值数
    function chipin(uint8[] number, uint8 multi, InjectionType injType, uint8 _sumVal) public payable {
        require(msg.value >= fee * multi); //检查下玩家投注的金额和倍数是不是一样
        require(block.number <= stopBlock); //检查当前块数是可以下注的块数

        if(injType == InjectionType.TypeSum ){ //判断玩家下注方式是不是和值类型
            require(_sumVal >= 3 && _sumVal <= 18); //检查和值数不能小于3并且不能大于18
        }
        Order memory order = Order(msg.sender, number, multi, injType, _sumVal); //新建一个Order结构体
        orders.push(order);//将Order结构体存储起来
    }
    //计算奖金函数-通过下注code信息计算奖金情况,返回奖金值
    //number是玩家购买的号码,sumVal是值数,multi是玩家要购买的倍数,injType为玩家下注方式,openCode是开奖号码
    function calcBonus(uint8[] number,uint8 sumval, uint8 multi, InjectionType injectionType,uint8[] opencode) public view returns (uint){

        //对于和值类型投注,下注金额不能小于1大于18

        //如果是和值类型,计算和值是否相等
        if(injectionType == InjectionType.TypeSum){ //如果玩家选择的是和值类型
            uint injVal = opencode[0]+opencode[1]+opencode[2]; //开奖号码总和
            if(sumval == injVal){ //如何玩家的sumval和开奖号码和值相等的话
                return fee * odds[injVal] * multi / 2; //返回该获得的奖金
            }
            return 0;
        }
        //三同号通选 要求三个号码相同即可
        if(injectionType == InjectionType.TypeSameThree){
            if(opencode[0]  == opencode[2]){
               return fee * 40 * multi / 2;
            }
            return 0;
        }
        //三同号单选
        if(injectionType == InjectionType.TypeSameThreeSingle){
            if(opencode[0] == opencode[2]  && number[0] == opencode[0]){
                return fee * 240 * multi / 2;
            }
            return 0;
        }
        //二同号复选 只要任意两个号码相同即可
        if(injectionType == InjectionType.TypeSameTwo){
           if((opencode[0] == opencode[1] || opencode[1] == opencode[2]) && opencode[1] == number[0] ){
                return fee * 15 * multi / 2;
            }
            return 0;
        }
        //二同号单选 要求指定的对子和单都相同
        if(injectionType == InjectionType.TypeSameTwoSingle) {
            if(opencode[0] == opencode[1] || opencode[1] == opencode[2]) {
                if(opencode[0] == number[0] && opencode[1] == number[1] && opencode[2] == number[2]) {
                    return fee * 80 * multi / 2;
                }
            }
            return 0;
        }
        //三不同号 要求开奖号码是三个不同的
        if(injectionType == InjectionType.TypeNoSameThree){
            if(opencode[0] == number[0] && opencode[1] == number[1] && opencode[2] == number[2]) {
                return fee * 40 * multi / 2;
            }
            return 0;
        }
        //二不同号 指定2个不同号码和一个任意号码
        if(injectionType == InjectionType.TypeNoSameTwo){
            if( (opencode[0] == number[0] && opencode[1] == number[1]) ||
            (opencode[1] == number[0] && opencode[2] == number[1]) ) {
                 return fee * 8 * multi / 2;
            }
            return 0;
        }
        //三连号 可能情况 123 234 345 456
        if(injectionType == InjectionType.TypeThreeConsecutive){
            if(opencode[1]-opencode[0] == 1 && opencode[2]-opencode[1] == 1){
                return fee * 10 * multi / 2;
            }
            return 0;
        }

        return 0;
    }
    //开盘函数
    function open() public {
        require(block.number > openBlock);//检查一下当前块数大于设置的可开盘块数

        uint8[] memory openCode; //openCode是存储开奖号码

        uint8 a = uint8(uint(block.blockhash(baseBlock + codeAOffset)) % 6 + 1); //将当前区块的hash值+codeAOffset,运算出一个hash值与6取模并且+1
        uint8 b = uint8(uint(block.blockhash(baseBlock + codeBOffset)) % 6 + 1); //将当前区块的hash值+codeBOffset,运算出一个hash值与6取模并且+1
        uint8 c = uint8(uint(block.blockhash(baseBlock + codeCOffset)) % 6 + 1); //将当前区块的hash值+codeCOffset,运算出一个hash值与6取模并且+1
        uint8 t;
        //将开奖号码排序
        if (a > b) {
            t = a;
            a = b;
            b = t;
        }
        if (b > c)
        {
            t = c;
            c = b;
            b = t;
        }
        if (a > b) {
            t = a;
            a = b;
            b = t;
        }
        OpenCode memory code = OpenCode(baseBlock, openCode); //新建一个代码号码记录
        code.opencode[0] = a; //将记录中的号码赋值
        code.opencode[1] = b;
        code.opencode[2] = c;
        // 记录中奖号码
        allOpenCodes.push(code);
        //计算奖金
        uint i = 0;
        uint bonusMoney; //记录该获得的奖金
        for(i=0; i<orders.length; ++i) { //循环遍历每一个竞猜用户
            Order memory o = orders[i]; //o是遍历的orders寄存器
            //uint8[] number,uint8 sumval, uint8 multi, InjectionType injectionType,uint8[] opencode
            bonusMoney = calcBonus(o.number, o.sumVal, o.multi, o.injectionType, code.opencode); //调用calcBonus函数取得应该获得的奖金给bonusMoney
            if (bonusMoney > 0){
                bonus[o.player] += bonusMoney; //奖金映射,将获得奖金映射到地址中
                totalBonus += bonusMoney; //增加总奖金金额
            }
        }
        uint bonusMoney = fee * 120;
        uint i = 0;
        for(i=0; i<orders.length; ++i) {
            Order memory o = orders[I];
            if(o.number[0] == code.opencode[0]
                && o.number[1] == code.opencode[1]
                && o.number[2] == code.opencode[2]) {

                bonus[o.player] += bonusMoney * o.multi;
                totalBonus += bonusMoney * o.multi;
            }
        }
        // 重置数据,开始下一轮竞猜
        delete orders;
        baseBlock = block.number;
        stopBlock = baseBlock + 30; // about 450 seconds
        openBlock = stopBlock + 10; // about 150 seconds

    }

    // 彩民获取奖金
    function withdraw() public {
        uint money = bonus[msg.sender]; //奖金寄存器
        require(money > 0); //检查money大于0

        delete bonus[msg.sender]; //清楚该地址映射
        totalBonus -= money; //总奖金金额减少
        msg.sender.transfer(money); //给该地址返还奖金
    }
}

中心服务器部分

node.js 服务器 安装初始化
npm init  
安装web3.js到项目中:
npm install web3 --save  
在服务器使用web3.js
在web3test目录下新建index.js文件,在其中输入以下代码:
 var Web3 = require("web3");  
 var web3 = new Web3();  
 var web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"));  
获取已部署的智能合约实例
    var abi = /*编译器生成的abi代码*/;
    var contractAddress = '/*这里是合约的地址*/';  
    var hello = new web3.eth.Contract(abi,address);
调用节点
  • 查看节点所有账户
var faq = web3.eth.getAccounts(function(error,result){
    if(!error)
    resp.send(result);
});
console.log(faq);
  • 创建用户
var daq=web3.eth.personal.newAccount("1234",function(error,result){
    if(!error)
    resp.send(result);
});
  • 解锁
var faq=web3.eth.personal.unlockAccount("0xc40b465e28a386c56806058571d0baf303af079c","123",function(error,result){
    if(!error)
    resp.send(result);
});
    console.log(faq);
  • 调用合约
    函数下面是调用一个只读函数,后面参数call是表明此调用不会产生数据更改,只会在一个节点上处理数据,也不会消耗gas,function是回调函数,回调函数的返回值就是函数的返回值。
    下面是调用一个只读函数,后面参数call是表明此调用不会产生数据更改,只会在一个节点上处理数据,也不会消耗gas,function是回调函数,回调函数的返回值就是函数的返回值。
hello.methods.helloworld().call(function(error,result){
        if(!error)
        resp.send(result);
});
下面的是一个调用接受转账的函数,from是转账的账户,value是转账的数额,单位是wei,gas就是设定的gas,function是后面接的回调函数,回调函数的返回值是交易地址,还没有找到如何去查看函数的返回值。
hello.methods.hellomoney().send({from:"0xc40b465e28a386c56806058571d0baf303af079c",value: 200000000,gas:3000000},function(error,result){
        if(!error)
        resp.send(result);
});

hello.methods.helloPDJ().send({from:"0xc40b465e28a386c56806058571d0baf303af079c",gas:3000000},function(error,result){
if(!error)
resp.send(result);
})
上面调用会变更数据,但不接受转账的函数,from是发送调用的地址,
function是回调函数,回调函数的返回值是交易地址,还没有找到如何去查看函数的返回值。

相关文章

网友评论

    本文标题:【DAPP开发三】发布合约及实践

    本文链接:https://www.haomeiwen.com/subject/tdjloftx.html