# solidity-demo **Repository Path**: binny1024/solidity-demo ## Basic Information - **Project Name**: solidity-demo - **Description**: No description available - **Primary Language**: NodeJS - **License**: Unlicense - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-09-01 - **Last Updated**: 2022-02-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一 参考资料 - 以太坊介绍 https://ethereum.org/zh/ - Solidity语言介绍 https://solidity.readthedocs.io/en/v0.5.13/ - Solidity编程教学 https://openzeppelin.com/starter-kits/ https://cryptozombies.io/zh/course - 合约开发框架Truffle https://www.trufflesuite.com/docs/truffle/overview - 本地以太坊模拟 https://www.trufflesuite.com/ganache - 一些很好的合约代码范例 https://github.com/OpenZeppelin/openzeppelin-contracts 一 环境搭建 ## 1.1 install ganache Install and start [Ganache](https://www.trufflesuite.com/ganache) as a local Ethereum test node. ## 1.2 Install truffle ```sh npm install -g truffle ``` ## 1.3 Install Nodejs dependencies ```sh npm install -g solc solc-cli --save-dev ``` # 二 truffle 命令 ## 2.1 编译命令 1. Compile contracts ```sh truffle compile ``` 带参数 `--all` ```sh truffle compile --all ``` ## 2.2 部署 Run migrations ```sh truffle migrate ``` 带参数 `--reset` ```sh truffle migrate --reset ``` 输出到文件 ```sh sudo truffle migrate --reset >> ./log/migrate.log && echo "--------add payable----" >>./log/migrate.log ``` 重新部署,就是发一个新的合约,旧的合约还在链上 # 三 合约语法 ## 3.1 数据类型 ### 3.1.1 address 以太坊地址,20 个字节,使用uint160来存储的,而且他们之间可以相互强制转换: ```js address payable ap = address(uint160(addr)); ``` 地址之间是可以直接进行比较大小:**<=, <, >= , >,==, !=** 地址类型有两种形式,他们大致相同: - address:保存一个20字节的值(以太坊地址的大小)。 - address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。 image-20201223183101124 ### 3.1.2 整型 `int/uint` 变长的有符号或无符号整形。支持以8递增,uint8到uint256,uint默认为uint256。 ### 3.1.3 布尔型 ### 3.1.4 映射 键值对映射,例如:`mapping(address => uint)` ### 3.1.5 结构体 ```solidity struct Solidity { address addr; uint amount; } ``` ## 3.2 存储 三种类型:**storage**,**memory**,**calldata**。 默认存储类型 - 变量定义:**storage** - 函数参数及返回值:**memory** ### 3.2.1 storage 永久存储,费用相对较高。 ### 3.2.2 memory 临时变量存储,可以理解为`栈空间` ## 3.3 访问权限 合约与合约之间以及合约与其子类之间的访问权限。类比 Java 的**public**、**protected**、**private**,用法相似,略有不同。 image-20201223181546436 ### 3.3.1 public 该修饰符修饰变量既可以被 **internal** 方式的访问,也可以通过 **external** 方式的调用。可以理解为既能够被内部合约访问,也能被外部合约调用。 ### 3.3.2 external 能够被外部合约调用 ### 3.3.3 internal 只允许通过 **internal** 方式调用,不能被外部合约调用。 ### 3.3.3 private 与 `internal`一样,都不能被外部合约访问,唯一不同的是 `private`函数不能被子类调用。 ### 3.3.4 constant、pure、view 在 **Solidity v4.17**之前.只有 **constant**,后来有人嫌 **constant**这个词本身代表变量中的常量,不适合用来修饰函数,所以将**constant**拆成了**view**和**pure**。 当函数有返回值时,可以添加这三种定义.,用这三种方式定 义的函数都只执行读操作,不会进行编译执行。即用了这三 种方式定义的函数,不会执行函数里的逻辑.只会执行一个返回的读操作。所以执行这些函数不需要消耗gas费用。 **pure** 则更为严格,**pure** 修饰的函数不能改也不能读变量状态,否则编译通不过。 **view** 的作用和 **constant**一模一样,可以读取状态变量但是不能改。 image-20201223182113493 ## 3.4 函数修改器 相当于自定义函数的修饰符。 修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。 image-20201223182520591 ## 3.5 回退函数 **fallback function**回退函数,每一个合约有且仅有一个没有名字的函数,往合约发送消息时,会执行该函数。如果合约要正常接受**ether**,需要加上**payable**声明。声明后,当有用户往合约转账时,将触发该函数,可以在里面写相应的逻辑。 - 1.当外部账户或其他合约向该合约地址发送 ether 时; - 2.当外部账户或其他合约调用了该合约一个不存在的函数时 image-20201223182701971 ## 3.6 异常处理 使用状态恢复来处理异常,就是说当抛出异常时将恢复到调用前的状态。抛出异常的方式有assert,require,revert,throw。 ### 3.6.1 assert assert函数,用于条件检查,只能测试内部错误和检查常量。 ### 3.6.1 require require函数,也是用于条件检查,用于测试调用的输入或者合约状态变量。 ### 3.6.3 revert revert 函数用于标记错误并恢复当前调用。 ### 3.6.4 throw **throw** 和 **revert** 一样,但是**throw**在0.4.13被弃用,将来会被淘汰。 ```solidity pragma solidity >=0.4.22 <0.6.0; contract HelloWorld { //检查内部计算是否会溢出 function add(uint256 a, uint256 b) internal pure returns (uint256){ uint256 c = a + b; assert(c >= a); return c; } function send() payable public returns (uint balance){ require(msg.value % 2 == 0); //只允许偶数 return msg.value; msg.value;} function buy(uint amount) payable public { if (amount > msg.value / 2 ether) { revert("not enough ether provided."); } } } ``` ## 3.7 合约相关 ### 3.7.1 this `this`(当前合约的类型): 表示当前合约,可以显式的转换为 Address ### 3.7.2 selfdestruct `selfdestruct(address recipient)`: 销毁当前合约,并把它所有资金发送到给定的地址。 ## 3.8 几种转币方法对比 ```js pragma solidity >=0.4.22<0.6.0; contract Helloworld { address payable public winner; function sendTowinner(uint256 winAmount) public { winner.send(winAmount); winner.transfer(winAmount); winner.call.value(winAmount); } } ``` ### 3.8.1 transfer() `
.transfer()` - 当发送失败时会 throw; 回滚状态; - 只会传递 2300 Gas 供调用,防止重入攻击; ### 3.8.2 send() `
.send()` - 当发送失败时会返回 false 布尔值; - 只会传递 2300 Gas 供调用,防止重入; ### 3.8.3 gas().call.value() `
.gas().call.value()` - 当发送失败时会返回 false 布尔值; - 传递所有可用 Gas 进行调用(可通过 gas(gas_value) 进行限制),不能有效防止重入; ## 3.9 常用 API ### 3.9.1 区块相关 - `block.blockhash(uint blockNumber) returns (bytes32)`:返回给定区块号的哈希值,只支持最近256个区块,且不包含当前区块。 - `block.coinbase (address)`: 当前块矿工的地址。 - `block.difficulty (uint)`:当前块的难度。 - `block.gaslimit (uint)`:当前块的gaslimit。 - `block.number (uint)`:当前区块的块号。 - `block.timestamp (uint)`: 当前块的Unix时间戳(从1970/1/1 00:00:00 UTC开始所经过的秒数) ### 3.9.2 调用者相关 - `msg.sender (address)`: 当前调用发起人的地址。 - `msg.value (uint)`: 这个消息所附带的以太币,单位为wei。 - `msg.sig (bytes4)`:调用数据(calldata)的前四个字节(例如为:函数标识符)。 - `msg.data (bytes)`: 完整的调用数据(calldata)。 - `tx.origin (address)`: 交易的发送者 - `tx.gasprice (uint)` : 交易的 gas 价格。 - `now (uint)`: 当前块的时间戳 (block.timestamp 的别名) # 四 truffle https://learnblockchain.cn/docs/truffle/reference/configuration.html#id2 ## 4.1 目录结构 - **contracts**:智能合约目录 - **migrations**:部署文件的 **js** 脚本目录 - **test**:测试用例目录 - **truffle-config.js**:**truffle** 的配置文件 ## 4.2 依赖 可以使用 **import** 关键字声明依赖 - 通过文件路径 ``` import "./AnotherContract.sol"; ``` - 通过外包 ``` import "somepackage/SomeContract.sol"; ``` ## 4.3 编译合约 - 命令 ```sh truffle compile [--all] ``` 每次编译只会涉及上一次修改后的代码,如果要强制重新编译,可以使用 `—all`选项.编译结果保存在 `build/contracts/`文件夹中. ## 4.4 部署脚本 部署脚本在 **migrations** 文件夹下 - 命令 ```shell truffle migrate [--reset] ``` ```shell truffle migrate --network live ``` **Migrations.sol**合约会记录部署的历史,如果需要忽略部署历史,可以使用`--reset`参数 ### 4.4.1 部署文件 ```js const Migrations = artifacts.require("Migrations"); module.exports = function(deployer) { deployer.deploy(Migrations); }; ``` #### `artifacts.require` 使用该方法获取合约的实例,传入的参数名应该与合约代码定义的名字一致(不是文件名). ```js contract ContractOne{ //... } contract ContractTwo{ //... } ``` 如果只是用使用其中一个合约,可以是这样的: ```js var ContractTwo = artifacts.require("ContractTwo"); ``` 如果只是用使用两个合约,可以是这样的: ```js var ContractOne = artifacts.require("ContractOne"); var ContractTwo = artifacts.require("ContractTwo"); ``` #### `module.exports` 部署脚本的函数必须用**module.exports**语法兼容**truffle**框架,**truffle**在函数中注入了一个**deployer**对象,**deployer**对象提供了部署合约所需的所有API。 ### 4.4.2 部署 - Deploy A,then deploy B,passing in A`s newly address deployed address ```javascript deployer.deploy(A).then( function(){ return deployer.deploy(B,A.address); } ); ``` - network-beased ```js module.exports=function(deployer,network){ if (network=="live"){ //Do something specific to the network named "live". }else{ //Perform a different step otherwise. } } ``` - with accoints ```js module.exports = function(deployer,network,accounts){ // use the accounts whitin your migrations } ``` ## 4.5 部署案例 ### 4.5.1 合约源码 ```js pragma solidity ^0.6.11; contract SimpleStorage { int _age; constructor () public { } function setAge(int age) public { _age = age; } function getAge() public view returns (int){ return _age; } } ``` ### 4.5.2 迁移脚本 ```js const SimpleStorage = artifacts.require("SimpleStorage"); module.exports = function(deployer) { deployer.deploy(SimpleStorage); }; ``` ### 4.5.3 编译合约 ```sh truffle compile ``` ### 4.5.4 部署 ```sh truffle migrate ``` # 五 truffle 测试框架 truffle 使用 [Mocha](https://mochajs.org)测试框架和 **Chai** 断言为我们提供一个简便的,稳固的JS版本的测试框架,使用 **contract()**替代了 **describe()**. 结构上来说,我们的测试基本上遵循 **Mocha**的测试风格: - 测试脚本必须存放在 `./test`目录中. - 必须以 **.js**为扩展名. - 必须被 **Mocha**识别. 与 **Mocha** 不同的是 **contract()**函数,该函数跟 **describe()** 非常像,不同之处在于: - 每次 **contract()**函数运行之前,智能合约都会被重新部署并运行在以太坊节点上,以保证测试用例相关的合约状态的纯净. - 此外, **contract()**函数还提供了一些列的以太坊客户端提供的测试账号.供我们在测试用例中使用. 当然,在我们不需要一个纯净的测试合约状态的时候,依然可以使用 **describe()**. ## 5.1 artifacts.require 由于 **Truffle**没有方法检测到我们将使用哪一个合约进行交互式测试, **Contract abstractions **使得 **js**可以与智能合约交互.可以使用 truffle 提供的 `artifacts.require("")`方法. 在测试脚本中使用`artifacts.require("")`就如同在迁移脚本中使用一样, ## 5.2 web3 **web3** 实例在每一个测试文件中都是可用的,可以直接使用 `web3.eth.getBalance`. ## 5.3 使用 then - `./test/metacoin.js` ```js const MetaCoin = artifacts.require("MetaCoin"); contract("MetaCoin", accounts => { it("should put 10000 MetaCoin in the first account", () => MetaCoin.deployed() .then(instance => instance.getBalance.call(accounts[0])) .then(balance => { assert.equal( balance.valueOf(), 10000, "10000 wasn't in the first account" ); })); it("should call a function that depends on a linked library", () => { let meta; let metaCoinBalance; let metaCoinEthBalance; return MetaCoin.deployed() .then(instance => { meta = instance; return meta.getBalance.call(accounts[0]); }) .then(outCoinBalance => { metaCoinBalance = outCoinBalance.toNumber(); return meta.getBalanceInEth.call(accounts[0]); }) .then(outCoinBalanceEth => { metaCoinEthBalance = outCoinBalanceEth.toNumber(); }) .then(() => { assert.equal( metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpected function, linkage may be broken" ); }); }); it("should send coin correctly", () => { let meta; // Get initial balances of first and second account. const account_one = accounts[0]; const account_two = accounts[1]; let account_one_starting_balance; let account_two_starting_balance; let account_one_ending_balance; let account_two_ending_balance; const amount = 10; return MetaCoin.deployed() .then(instance => { meta = instance; return meta.getBalance.call(account_one); }) .then(balance => { account_one_starting_balance = balance.toNumber(); return meta.getBalance.call(account_two); }) .then(balance => { account_two_starting_balance = balance.toNumber(); return meta.sendCoin(account_two, amount, { from: account_one }); }) .then(() => meta.getBalance.call(account_one)) .then(balance => { account_one_ending_balance = balance.toNumber(); return meta.getBalance.call(account_two); }) .then(balance => { account_two_ending_balance = balance.toNumber(); assert.equal( account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender" ); assert.equal( account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver" ); }); }); }); ``` - output > ``` > Contract: MetaCoin > √ should put 10000 MetaCoin in the first account (83ms) > √ should call a function that depends on a linked library (43ms) > √ should send coin correctly (122ms) > > > 3 passing (293ms) > ``` ### 5.1.4 使用 async-await ```js const MetaCoin = artifacts.require("MetaCoin"); contract("2nd MetaCoin test", async accounts => { it("should put 10000 MetaCoin in the first account", async () => { let instance = await MetaCoin.deployed(); let balance = await instance.getBalance.call(accounts[0]); assert.equal(balance.valueOf(), 10000); }); it("should call a function that depends on a linked library", async () => { let meta = await MetaCoin.deployed(); let outCoinBalance = await meta.getBalance.call(accounts[0]); let metaCoinBalance = outCoinBalance.toNumber(); let outCoinBalanceEth = await meta.getBalanceInEth.call(accounts[0]); let metaCoinEthBalance = outCoinBalanceEth.toNumber(); assert.equal(metaCoinEthBalance, 2 * metaCoinBalance); }); it("should send coin correctly", async () => { // Get initial balances of first and second account. let account_one = accounts[0]; let account_two = accounts[1]; let amount = 10; let instance = await MetaCoin.deployed(); let meta = instance; let balance = await meta.getBalance.call(account_one); let account_one_starting_balance = balance.toNumber(); balance = await meta.getBalance.call(account_two); let account_two_starting_balance = balance.toNumber(); await meta.sendCoin(account_two, amount, { from: account_one }); balance = await meta.getBalance.call(account_one); let account_one_ending_balance = balance.toNumber(); balance = await meta.getBalance.call(account_two); let account_two_ending_balance = balance.toNumber(); assert.equal( account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender" ); assert.equal( account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver" ); }); }); ``` ### 5.1.5 运行测试 ```js truffle test ./test/metacoin.js ``` 结合 **contract()**,每次执行上述命令都会重新部署合约(貌似不会上链). # 六 基于 eth的测试代码 不使用 truffle 提供的测试框架 而是基于以太坊接口,来编写测试代码调试合约. 接口文档:https://web3js.readthedocs.io/en/v1.3.1/web3-eth-contract.html ## 6.1 合约对象 使用 **web3.eth.Contract** 对象与以太坊上的智能合约交互 ```js Contract: new ( jsonInterface: AbiItem[] | AbiItem, address?: string, options?: ContractOptions ) => Contract; ``` | 参数 | 含义 | | :---------------: | :----------------------------------------------------------: | | **jsonInterface** | The json interface for the contract to instantiate | | **address** | The address of the smart contract to call. | | **options** | The options of the contract. Some are used as fallbacks for calls and transactions | | 返回值 | 含义 | | :--------: | :---------------------------------------------------: | | **Object** | The contract instance with all its methods and events | - ContractOptions ```js export interface ContractOptions { // Sender to use for contract calls, //The address transactions should be made from. from?: string; // Gas price to use for contract calls, // The gas price in wei to use for transactions. gasPrice?: string; // Gas to use for contract calls // The maximum gas provided for a transaction (gas limit). gas?: number; // Contract code // The byte code of the contract. Used when the contract gets deployed. data?: string; } ``` | 参数 | 含义 | | ------------ | ------------------------------------------------------------ | | **from** | The address transactions should be made from. | | **gasPrice** | The gas price in wei to use for transactions. | | **gas** | The maximum gas provided for a transaction (gas limit). | | **data** | The byte code of the contract. Used when the contract gets [deployed](https://web3js.readthedocs.io/en/v1.3.0/web3-eth-contract.html#contract-deploy). | 使用方式: ```js var myContract = new web3.eth.Contract([...], '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', { from: '0x1234567890123456789012345678901234567891', // default from address gasPrice: '20000000000' // default gas price in wei, 20 gwei in this case }); ``` ## 6.2 属性 ### 6.2.1 defaultAccount - Property | 类型 | 含义 | | ---------- | ------------------------------------------------------------ | | **String** | 20 Bytes: Any ethereum address. You should have the private key for that address in your node or keystore. (Default is `undefined`) | - 设置默认账户 ``` web3.eth.defaultAccount = accounts[1]; ``` 以下方法在没有设置 **from** 属性的时候,该属性的值就会作为 **from** 的值来使用. ```js web3.eth.sendTransaction() web3.eth.call() new web3.eth.Contract() -> myContract.methods.myMethod().call() new web3.eth.Contract() -> myContract.methods.myMethod().send() ``` ### 6.2.2 options.address The address used for this contract instance. All transactions generated by web3.js from this contract will contain this address as the `"to"`. - Property | 类型 | 含义 | 备注 | | ---------------- | -------- | --------------------- | | **String\|null** | 合约地址 | 没有指定则为 **null** | ### 6.2.3 defaultBlock The default block is used for certain methods. You can override it by passing in the defaultBlock as last parameter. The default value is `"latest”`. - Property:The default block parameters can be one of the following: | 取值 | 类型 | 备注| | ------------------------- | -------------- |--------| | **数值** | Number\|BN\|BigNumber |A block number| | **earliest** | String |The genesis block| | **latest** | String |The latest block (current head of the blockchain)| | **pending** | String |The currently mined block (including pending transactions)| 使用 `NodeJs` 进行合约的调试 数据的读写 transaction:写数据 - 会改变以太坊的状态 - 需要消耗 gas - 不会立即执行,需要等待区块确认 - 不能定义返回值 call:读数据 - 不会改变以太坊状态 - 不需要消耗 gas - 立即执行 - 能定义返回值 # 七 事件监听 如果合约需要监听业务,则可以使用 **事件监听** 来获取每次业务的执行. ```js const Web3 = require("web3"); const shelljs = require("shelljs"); (async () => { // 合约abi let loansAbi; // 合约实例 let instance; // 链上账户信息 let accounts; // 合约地址,可以从链上查看或者从 Ganche 客户端查看 const loansAddress = '0x31b9dE9548200E200EB3d32671169fB3b9c394af' //创建一个 web3 示例 const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://localhost:7545')) accounts = await web3.eth.getAccounts(); console.log(accounts); loansAbi = JSON.parse(shelljs.cat('./instance/abi.txt').stdout); instance = new web3.eth.Contract(loansAbi, loansAddress); // console.log(instance); /** * 此方式只输出一次事件 */ instance.getPastEvents('set_age',{fromBlock:168},function (error, result) { if (error) { console.error(error); } else { console.log(result) } }); /** * 持续监听事件 */ instance.events.set_age( {fromBlock: 168}, function (error, result) { if (error) { console.error(error); } else { console.log(result) } }) })() ``` # 八 测试案例 https://gitee.com/xubinbin1024/solidity-demo.git ## 8.1 测试账号 ``` three proof decorate oil solution injury knock weather proof copper join rabbit ``` ## 8.2 案例一 ### 8.2.1 合约代码 ```js pragma solidity ^0.6.11; contract SimpleStorage { int _age; constructor () public { } function setAge(int age) public { _age = age; } function getAge() public view returns (int){ return _age; } } ``` ### 8.2.2 部署脚本 - 2_simple_storage:**文件名**一定要有数字前缀 ```js const SimpleStorage = artifacts.require("SimpleStorage"); module.exports = function(deployer) { deployer.deploy(SimpleStorage); }; ``` ### 8.2.3 测试代码 #### 8.2.3.1 truffle 测试框架 ```js const SimpleStorage = artifacts.require("SimpleStorage"); contract("SimpleStorage", function (accounts) { it('所有账户信息', async function () { console.log(accounts); }); it('合约实例 ', async function () { let instance = await SimpleStorage.deployed(); console.log('合约地址', instance.address); }); it('账户余额 ', async function () { let balance = await web3.eth.getBalance(accounts[0]); console.log(balance); }); }); ``` 1, 从以上代码的 **合约实例**的输出中,可以看出每次输出的合约地址是不同的,正如第五章所说,每次执行测试都会重新部署合约. 2, 可以在此这中测试代码中直接使用 **web3**对象. #### 8.2.3.2 基于 web3 接口 上述代码看似方便,但是不便于调试. 本测试代码使用单纯的 **Mocha**框架,也因此不会每次都重新部署合约.也便于调试.这种方式的特点: - 不会每次重新部署合约 - 需要自己引入 **web3**对象,需要它的话 - 对合约的操作理解会更透彻 ```js const Web3 = require("web3") const shelljs = require("shelljs"); /** * 用于离线签名 * @type {Transaction} */ const Tx = require('ethereumjs-tx').Transaction; /** * 合约地址:合约部署后,可以从 ganache 客户端获取 * @type {number} */ const address = '0xAa51C85b57b65f46B1b37A95C42C0B4532b317a9' describe("SimpleStorage", function () { let web3; it('get web3', async function () { web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:7545')) }); let accounts; it('所有账户信息', async function () { accounts = await web3.eth.getAccounts(); console.log(accounts); }); it('检查地址的合法性', async function () { let myiban = web3.eth.Iban.fromAddress(address) console.log(myiban) }); let abi; it('合约abi ', async function () { abi = JSON.parse(shelljs.cat('./instance/abi.txt').stdout); console.log(abi); }); let instance; it('获取合约实例', async function () { instance = new web3.eth.Contract(abi, address, { from: accounts[0] }); shelljs.echo(instance).to('./instance/instance1.log'); }); let from; it('设置默认 from 账户', async function () { from = accounts[2]; web3.eth.defaultAccount = from; console.log(from); }); it('合约默认账户', async function () { console.log(web3.eth.Contract.defaultAccount); }); it('address', async function () { console.log(instance.options.address); }); it('账户余额 ', async function () { let balance = await web3.eth.getBalance(accounts[0]); console.log(balance); }); it.skip('转账(在线签)', async function () { let transactionReceipt = await web3.eth.sendTransaction({ to: accounts[0], value: web3.utils.toWei('1', 'ether') }); console.log(transactionReceipt); }); let nonce; it('当前账户的 nonce', async function () { nonce = await web3.eth.getTransactionCount(from); console.log(nonce); }); const token_hex = '0x'; it.skip('离线签', async function () { let priv = "69d3f76ddb441c41547caabcc5dbdaacedebc389ce0cc3eb3a240778384ee9a9"; let privateKey = Buffer.from(priv, 'hex'); let priceWei = await web3.eth.getGasPrice(); priceWei = token_hex + Number(priceWei).toString(16); const gasLimitCounts = token_hex + Number(30000).toString(16); const amountWei = web3.utils.toWei('1', 'ether'); // log('amountWei = ' + amountWei); const amount = token_hex + Number(amountWei).toString(16); let rawTx = { nonce: nonce, gasPrice: priceWei.toString(), gasLimit: gasLimitCounts.toString(), to: '0x0000000000000000000000000000000000000000', value: amount.toString(), data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057' }; let tx = new Tx(rawTx); tx.sign(privateKey); let serializedTx = tx.serialize(); let transactionReceipt = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex')); console.log(transactionReceipt); }); it('获取 from 余额', async function () { let balance = await web3.eth.getBalance(from); console.log(balance); }); it('当前块高', async function () { let number = await web3.eth.getBlockNumber(); console.log(number); }); it('读数据', async function () { let age = await instance.methods.getAge().call(); console.log(age); }); it('写数据', async function () { let newVar = await instance.methods.setAge(10).send(); console.log(newVar); }); it('读数据', async function () { let age = await instance.methods.getAge().call(); console.log(age); }); it('获取 from 余额', async function () { let balance = await web3.eth.getBalance(from); console.log(balance); }); }); ``` ## 8.3 案例二 ### 8.3.1 合约代码 ```js pragma solidity >=0.5.0 <0.7.0; /* 场景说明: 银行提供企业贷款服务,申请企业需要提交公司授信资质(法人征信正常 与否)以及其供应链上交易信息(是否有合约纠纷和违约逾期), 银行需要通过 银行人工审核和机审才可以给企业放款。机审时若碰到法人征信不正常、企业产 生供应链上有合约纠纷、违约逾期等情况,则判断审核不通过。 */ contract Loans { address company;//申请企业的地址 address owner;//合约的创建者,即审核人员的地址信息 struct Application { bool credit;//法人征信是否正常 bool dispute;//是否有合约纠纷 bool offlineMac;//是否通过线下机审 bool offlineMan;//是否通过线下人工审核 bool overdue;//是否逾期 } constructor () public { owner = msg.sender; } mapping(address => Application) applicationInfo; event ApplicationSubmit(address addr, bool credit, bool dispute, bool overdue); //申请企业提交信息 function applicationSubmit(bool credit, bool dispute, bool overdue) public { applicationInfo[msg.sender].credit = credit; applicationInfo[msg.sender].dispute = dispute; applicationInfo[msg.sender].overdue = overdue; emit ApplicationSubmit(msg.sender, credit, dispute, overdue); } event OffLineStuffSubmit(address addr, bool offlineMan, bool offlineMac); function offlineStuffSubmit(bool offlineMan, bool offlineMac, address addr) public { require(msg.sender == owner, 'you dont have access to do this'); applicationInfo[addr].offlineMan = offlineMan; applicationInfo[addr].offlineMac = offlineMac; emit OffLineStuffSubmit(addr, offlineMan, offlineMac); } function getApplicationResult() public view returns (bool){ if (applicationInfo[msg.sender].credit == true && applicationInfo[msg.sender].overdue == true && applicationInfo[msg.sender].offlineMac == true && applicationInfo[msg.sender].offlineMan == true && applicationInfo[msg.sender].dispute == true) { return true; } return false; } function getApplicationCredit() public view returns (bool){ return applicationInfo[msg.sender].credit; } function getApplicationDispute() public view returns (bool){ return applicationInfo[msg.sender].dispute; } function getApplicationOverdue() public view returns (bool){ return applicationInfo[msg.sender].overdue; } function getApplicationMan() public view returns (bool){ return applicationInfo[msg.sender].offlineMan; } function getApplicationMac() public view returns (bool){ return applicationInfo[msg.sender].offlineMac; } } ``` ### 8.3.2 事件订阅 ```js const Web3 = require("web3"); (async () => { // 合约abi let loansAbi; // 合约实例 let instance; // 链上账户信息 let accounts; // 合约地址,可以从链上查看或者从 Ganche 客户端查看 const loansAddress = '0x8ebce3764E3b0442af914C4b6d9587512839901A' //创建一个 web3 示例 const web3 = new Web3(new Web3.providers.WebsocketProvider('http://localhost:7545')) accounts = await web3.eth.getAccounts(); console.log(accounts); loansAbi = require("./build/contracts/Loans.json").abi; instance = new web3.eth.Contract(loansAbi, loansAddress); // console.log(instance); instance.events.ApplicationSubmit({fromBlock:0},function (error, result) { if (error) { console.error(error); } else { console.log(result) } }) })() ``` 1. Run a demo script,执行合约调用 ```sh truffle exec mytoken.js ``` 3. 获取所有的账户 ```js let accounts = await web3.eth.getAccounts(); ``` 8 . 事件及订阅 event : ```js /** * @dev Emitted when `value` tokens are moved from one account (`from`) to * another (`to`). * * Note that `value` may be zero. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ event Approval(address indexed owner, address indexed spender, uint256 value); ``` 抛出事件: ```javascript /** * @dev Moves tokens `amount` from `sender` to `recipient`. * * This is internal function is equivalent to {transfer}, and can be used to * e.g. implement automatic token fees, slashing mechanisms, etc. * * Emits a {Transfer} event. * * Requirements: * * - `sender` cannot be the zero address. * - `recipient` cannot be the zero address. * - `sender` must have a balance of at least `amount`. */ function _transfer(address sender, address recipient, uint256 amount) internal { require(sender != address(0), "ERC20: transfer from the zero address"); require(recipient != address(0), "ERC20: transfer to the zero address"); _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); _balances[recipient] = _balances[recipient].add(amount); emit Transfer(sender, recipient, amount); } ``` 订阅: ```js //监听代币交易事件 module.exports = async (callback) => { try { const MyToken = artifacts.require("MyToken"); let instance = await MyToken.deployed(); let BN = require("bn.js"); let frac = new BN(10).pow(new BN(18)); instance.Transfer().on('data', function (event) { console.log('-----------------'); console.log('from', event.args.from); console.log('to', event.args.to); console.log('value', event.args.value.div(frac).toString()); }); } catch (e) { console.log("e = " + e.stack); callback(e) } }; ``` ```js //获取所有区块信息 module.exports = async (callback) => { try { const MyToken = artifacts.require("MyToken"); let instance = await MyToken.deployed(); let BN = require("bn.js"); let frac = new BN(10).pow(new BN(18)); let events = await instance.getPastEvents('allEvents',{ 'fromBlock': 0, 'toBlock': 'latest' }, function (error) { console.log('error', error); }); for (let i = 0; i < events.length; i++) { let event = events[i]; console.log('-----------------------------------'); console.log(JSON.stringify(event,null,4)); // console.log('Transfer' + i); // console.log('from', event.args.from); // console.log('to', event.args.to); // console.log('value', event.args.value.div(frac).toString()); } callback(); } catch (e) { console.log("e = " + e.stack); callback(e) } }; ``` # 附录 ## iban 国际银行账号,简单地说,以太坊中的iban账号是以太坊为了和传统的银行系统对接而引入的概念, **web3.js**中提供了以太坊地址和**iban**地址之间的转换方法。 **iban**这个概念源于传统的银行系统,其英文全称为 **International Bank Account Number**, 即国际银行帐号。iban的作用是为全球任意一家银行中的任意一个账户生成一个全球唯一的账号,以便进行跨行交易。一个iban账号看起来像这样: ``` XE7338O073KYGTWWZN0F2WZ0R8PX5ZPPZS ``` iban地址最多可以包含34个字母和数字,其中的字母大小写不敏感。在iban 中包含以下信息: - 国别码,用来标识国家,遵循ISO3166-1 alpha-2标准 - 错误识别码,用来对地址进行校验,采用mod-97-10校验和协议,即ISO/IEC 7064:2003标准 - 基本银行账号,即`BBAN`(Basic Bank Account Number),用来标识银行机构、网点及 客户在该机构内的账号,这三部分信息的编码方案依赖于前面提及的国别码 ### 以太坊iban 新的国别码和BBAN编码方案. 以太坊引入了一个新的IBAN国别码:XE,其中E代表**Ethereum**,X代表**非法币**(non-jurisdictional currencies)。同时,以太坊提出了三种BBAN的编码格式:direct、basic和indirect。 ### direct 编码方案中的BBAN为30个字母/数字,只有一个字段:账户编号。例如,以太坊 地址`00c5496aee77c1ba1f0854206a26dda82a81d6d8`转换为direct方案的BBAN账号,就 得到`XE7338O073KYGTWWZN0F2WZ0R8PX5ZPPZS`。 ```js let myiban=web3.eth.Iban.fromAddress('0x00c5496aee77c1ba1f0854206a26dda82a81d6d8'); console.log(myiban) //XE7338O073KYGTWWZN0F2WZ0R8PX5ZPPZS ``` ### basic 编码方案与direct方案的唯一区别在于,其BBAN长度为31个字母/数字,因此该方案 不兼容IBAN。 ### indrect 编码方案中的BBAN长度为16个字母/数字,包含三个字段: - 资产编号,由3个字母/数字组成 - 机构编号,由4个字母/数字组成 - 机构内客户编号,由9个字母/数字组成 例如,一个采用indrect编码方案的以太坊iban账号,看起来是这样: ``` XE81ETHXREGGAVOFYORK1 ``` **XE81-ETH-XREG-GAVOFYORK1**前面的`XE`表示国别码,`81`为校验和,后面的16个字符就是**indrect**编码的**BBAN**,其中: - ETH:在本例中,表示客户账户内的资产编号。目前ETH是唯一有效的资产编号 - XREG:机构编号,XREG表示以太坊基本注册合约 - GAVOFYORK:机构内客户的编号 ### iban账号与以太坊地址的转换 如前所述,使用`web3.eth.Iban.fromEthereumAddress()`方法,可以将一个以太坊地址 转换为direct编码方案的iban账号。与之对应的,可以使用[web3.eth.Iban.toAddress](http://cw.hubwiz.com/card/c/web3.js-1.0/1/10/4/)方法, 将一个采用direct编码方案的iban账号,转换回以太坊地址。例如: ``` let myaddr = web3.eth.Iban.toAddress("XE7338O073KYGTWWZN0F2WZ0R8PX5ZPPZS") console.log(myaddr) //0x00c5496aEe77C1bA1f0854206A26DdA82a81D6D812 ``` ### 检查iban账号的有效性 iban账号中的校验和用来帮助核验一个给定字符串是否为有效的iban账号。可以使用 web3.js中的[web3.eth.Iban.isValid()](http://cw.hubwiz.com/card/c/web3.js-1.0/1/10/8/) 来进行执行校验。例如: ``` let isValid = web3.eth.Iban.isValid("XE81ETHXREGGAVOFYORK") console.log(isValid) // true isValid = web3.eth.Iban.isValid("XE82ETHXREGGAVOFYORK") console.log(isValid) // false,因为校验和无效 ``` ## abi ## 控制台 ### 连接到节点 ### 设置变量 - abi ``` abi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"int256","name":"age","type":"int256"}],"name":"setAge","outputs":[],"stateMutability":"nonpayable","type":"function","signature":"0xf193dc1d"},{"inputs":[],"name":"getAge","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function","constant":true,"signature":"0x967e6e65"}]; ``` - address ``` address = '0xAa51C85b57b65f46B1b37A95C42C0B4532b317a9'; ``` - contract ``` contract = web3.eth.contract(abi); ``` - instance ``` instance = contract.at(address); ``` - accounts ``` accounts = web3.eth.accounts; ```