基于以太坊为女儿发一枚数字货币

基于以太坊为女儿发一枚数字货币

0 写在前面

家有2娃,姐妹相差3岁,姐姐刚上幼儿园,妹妹终于可在家愉快的玩耍了,不用再担心被姐姐抢玩具,或者由于姐姐的过度亲密,而被抓上一爪或咬上一口了:(

言归正传,下面将通过给姐妹俩发一枚以她俩名字命名的XYC(Xuan Yuan Coin),来实践下:

(1)基于Docker环境构建以太坊多节点私有网络

(2)以太坊智能合约的开发、部署、调用

(3)Go语言对以太坊智能合约的调用

1 基于Docker的多节点私有网络搭建

1.1 构建开发基础镜像

该镜像包含开发常用工具和库

1.1.1 编写Dockerfile文件

FROM ubuntu:18.04
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
RUN apt-get update
RUN apt-get install -y wget 
RUN mkdir /opt/java && \
    wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u181-b13/96a7b8442fe848ef90c96a2fad6ed6d1/jdk-8u181-linux-x64.tar.gz -P /opt/java
 
RUN tar zxvf /opt/java/jdk-8u181-linux-x64.tar.gz -C /opt/java &&\ 
    JAVA_HOME=/opt/java/jdk1.8.0_181 &&\ 
    sed -i '$a export PATH=$PATH' /root/.bashrc && \    
    sed -i "/^export PATH/i export JAVA_HOME=$JAVA_HOME" /root/.bashrc &&\ 
    sed -i "s%^export PATH.*$%&:$JAVA_HOME/bin%g" /root/.bashrc &&\ 
    source /root/.bashrc
 
RUN apt-get install -y vim lsof wget tar bzip2 unzip passwd sudo net-tools rsync man git make automake cmake patch python python-pip telnet ruby inetutils-ping
 
ENV GO_VERSION "1.11"
RUN mkdir /opt/go && mkdir -p /workspace/go && \
        wget https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz -P /opt/go
RUN tar -zxvf /opt/go/go$GO_VERSION.linux-amd64.tar.gz -C /opt/go 
RUN GOPATH=/workspace/go && GOROOT=/opt/go/go && \
        sed -i "/^export PATH/i export GOROOT=$GOROOT" /root/.bashrc &&\ 
        sed -i "/^export PATH/i export GOPATH=$GOPATH" /root/.bashrc &&\ 
        sed -i "s%^export PATH.*$%&:$GOROOT/bin%g" /root/.bashrc && \
        source /root/.bashrc
 
ENV TZ "Asia/Shanghai"
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata
RUN echo $TZ > /etc/timezone
RUN ln -fs /usr/share/zoneinfo/$TZ /etc/localtime
RUN dpkg-reconfigure tzdata -f noninteractive

1.1.2 构建镜像

$ sudo docker build -t js/ubuntu:dev -f js.Dockerfile .

1.2 构建以太坊go-ethereum镜像

1.2.1 编写创世区块文件

准备该文件,用于创世区块的创建

coinbase的内容是最想对女儿说的话:)

{
    "nonce": "0x0000000000000042",
    "timestamp": "0x0",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "extraData": "0x00",
    "gasLimit": "0x80000000",
    "difficulty": "0x1",
    "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "coinbase": "0x62616269206c6f7665207875616e267975616e0a",
    "alloc": {}, 
    "config": {}
}   

1.2.2 编写Dockerfile文件

FROM js/ubuntu:dev
 
ENV GETH_VERSION "1.8.15"
 
RUN mkdir /opt/geth && \
        wget https://github.com/ethereum/go-ethereum/archive/v$GETH_VERSION.tar.gz -P /opt/geth/v$GETH_VERSION.tar.gz -C /opt/geth
        
WORKDIR /opt/geth/go-ethereum-$GETH_VERSION
 
ENV GOROOT=/opt/go/go
ENV PATH=$PATH:$GOROOT/bin
RUN make all 
RUN sed -i "s%^export PATH.*$%&:/opt/geth/go-ethereum-$GETH_VERSION/build/bin%g" /root/.bashrc && \
        source /root/.bashrc
 
ENV PATH=$PATH:/opt/geth/go-ethereum-$GETH_VERSION/build/bin
RUN geth version
 
ADD ./CustomGenesis.json /
RUN geth --datadir '/opt/geth/datadir' init /CustomGenesis.json                         
WORKDIR /opt/geth/datadir

1.2.3 构建镜像

$ sudo docker build -t js/geth:dev -f bc.Dockerfile .

1.3 容器编排

这里构建3个以太坊节点

1.3.1 创建数据文件挂载目录

这里挂载的数据文件目录中,存放以太坊节点的私钥、区块、cache等。

$ sudo docker volume create --name=geth-peer1-datadir
#geth-peer1-datadir
$ sudo docker volume create --name=geth-peer2-datadir
#geth-peer2-datadir
$ sudo docker volume create --name=geth-peer3-datadir
#geth-peer3-datadir

1.3.2 编写docker-compose文件

version: "3"

services:
  peer1:
    image: js/geth:dev
    command: geth --datadir '/opt/geth/datadir' --ethash.dagdir '/opt/geth/datadir/ethash' --port 30303 --nodiscover --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --rpccorsdomain "*" --identity "peer1"
    container_name: peer1
    hostname: peer1
    ports: 
      - 30303:30303
      - 8545:8545
    expose:
      - "30303"
      - "8545"
    volumes:
      - geth-peer1-datadir:/opt/geth/datadir
    networks:
      bcnetwork:
        ipv4_address: 172.33.0.2
  peer2:
    image: js/geth:dev
    command: geth --datadir '/opt/geth/datadir' --ethash.dagdir '/opt/geth/datadir/ethash' --port 30303 --nodiscover --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --rpccorsdomain "*" --identity "peer2"
    container_name: peer2
    hostname: peer2
    ports: 
      - 40404:30303
      - 48545:8545
    expose:
      - "40404"
      - "48545"
    volumes:
      - geth-peer2-datadir:/opt/geth/datadir
    networks:
      bcnetwork:
        ipv4_address: 172.33.0.3
  peer3:
    image: js/geth:dev
    command: geth --datadir '/opt/geth/datadir' --ethash.dagdir '/opt/geth/datadir/ethash' --port 30303 --nodiscover --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --rpccorsdomain "*" --identity "peer3"
    container_name: peer3
    hostname: peer3
    ports: 
      - 50505:30303
      - 58545:8545
    expose:
      - "50505"
      - "58545"
    volumes:
      - geth-peer3-datadir:/opt/geth/datadir
    networks:
      bcnetwork:
        ipv4_address: 172.33.0.4

networks:
  bcnetwork:
    driver: bridge
    ipam:
      config:
        - subnet: 172.33.0.0/16 

volumes:
  geth-peer1-datadir:
    external: true
  geth-peer2-datadir:
    external: true
  geth-peer3-datadir:
    external: true

1.3.3 geth参数说明

command里面是执行的geth命令,参数很多,这里仅对使用到的进行介绍。

geth --datadir '/opt/geth/datadir' --ethash.dagdir '/opt/geth/datadir/ethash' --port 30303 --nodiscover --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --rpccorsdomain "*" --identity "peer2"
参数 含义
datadir 数据库和keystore密钥的数据目录
ethash.dagdir 存ethash DAGs目录
port P2P发现监听端口
nodiscover 禁用节点发现机制
rpc 启用HTTP-RPC服务器
rpcaddr HTTP-RPC服务器接口地址
rpcport HTTP-RPC服务器监听端口
rpcapi 基于HTTP-RPC接口提供的API
rpccorsdomain 允许跨域请求的域名列表(逗号分隔)(浏览器强制)
identity 自定义节点名

1.3.4 特别说明

为了避免节点被其他以太坊网络中的节点搜索到,使用了--nodiscover,关闭了自动发现,就需要通过配置静态节点文件的方式,手工进行节点的关联,为避免每次重启容器时IP重新分配而发生变化带来的不便,故在docker-compose文件的编写中,通过自定网络的方式,来固定IP。

1.4 启动以太坊多节点网络

1.4.1 启动命令

$ sudo docker-compose -f docker-compose-geth.yaml up -d
Creating peer1 ... done
Creating peer3 ... done
Creating peer2 ... done

1.4.2 数据文件查看

  • 容器启动后,通过如下命令,可以查看到容器在本机的挂载目录:
$ sudo docker inspect --format='{{json .Mounts}}' peer1 peer2 peer3 | jq
[
  {
    "Type": "volume",
    "Name": "geth-peer1-datadir",
    "Source": "/var/lib/docker/volumes/geth-peer1-datadir/_data",
    "Destination": "/opt/geth/datadir",
    "Driver": "local",
    "Mode": "rw",
    "RW": true,
    "Propagation": ""
  }
]
[
  {
    "Type": "volume",
    "Name": "geth-peer2-datadir",
    "Source": "/var/lib/docker/volumes/geth-peer2-datadir/_data",
    "Destination": "/opt/geth/datadir",
    "Driver": "local",
    "Mode": "rw",
    "RW": true,
    "Propagation": ""
  }
]
[
  {
    "Type": "volume",
    "Name": "geth-peer3-datadir",
    "Source": "/var/lib/docker/volumes/geth-peer3-datadir/_data",
    "Destination": "/opt/geth/datadir",
    "Driver": "local",
    "Mode": "rw",
    "RW": true,
    "Propagation": ""
  }
]
  • 进入目录后,可以查看到对应的文件内容:
/var/lib/docker/volumes/geth-peer1-datadir/_data/
├── geth
│   ├── chaindata
│   │   ├── 000002.ldb
│   │   ├── 000003.log
│   │   ├── CURRENT
│   │   ├── CURRENT.bak
│   │   ├── LOCK
│   │   ├── LOG
│   │   └── MANIFEST-000004
│   ├── lightchaindata
│   │   ├── 000001.log
│   │   ├── CURRENT
│   │   ├── LOCK
│   │   ├── LOG
│   │   └── MANIFEST-000000
│   ├── LOCK
│   ├── nodekey
│   └── transactions.rlp
├── geth.ipc
└── keystore

1.4.3 启动日志查看

以peer1为例:

$ sudo docker-compose -f docker-compose-geth.yaml logs -f peer1
Attaching to peer1
peer1    | INFO  Maximum peer count                       ETH=25 LES=0 total=25
peer1    | INFO  Starting peer-to-peer node               instance=Geth/v1.8.15-stable/linux-amd64/go1.11
peer1    | INFO  Allocated cache and file handles         database=/opt/geth/datadir/geth/chaindata cache=768 handles=1024
peer1    | INFO  Initialised chain configuration          config="{ChainID: <nil> Homestead: <nil> DAO: <nil> DAOSupport: false EIP150: <nil> EIP155: <nil> EIP158: <nil> Byzantium: <nil> Constantinople: <nil> Engine: unknown}"
peer1    | INFO  Disk storage enabled for ethash caches   dir=/opt/geth/datadir/geth/ethash count=3
peer1    | INFO  Disk storage enabled for ethash DAGs     dir=/root/.ethash                 count=2
peer1    | INFO  Initialising Ethereum protocol           versions="[63 62]" network=1
peer1    | INFO  Loaded most recent local header          number=0 hash=eef349…8aeba8 td=1
peer1    | INFO  Loaded most recent local full block      number=0 hash=eef349…8aeba8 td=1
peer1    | INFO  Loaded most recent local fast block      number=0 hash=eef349…8aeba8 td=1
peer1    | INFO  Regenerated local transaction journal    transactions=0 accounts=0
peer1    | INFO  Starting P2P networking 
peer1    | INFO  IPC endpoint opened                      url=/opt/geth/datadir/geth.ipc
peer1    | INFO  RLPx listener up                         self="enode://bdf07fa1c2aa550005e13fd68b1ab0ccb1026669d93da1aee430124277f5e76f4f116b45c4dcefada9ff23b8fe89c2b8c2bab6e4b3c5ec92447164c5b06c18bf@[::]:30303?discport=0"

1.4.4 各节点enode信息

记录下各个节点的enode信息,以便进行多节点互联静态配置文件的编写。

节点名 enode
peer1 enode://bdf07fa1c2aa550005e13fd68b1ab0ccb1026669d93da1aee430124277f5e76f4f116b45c4dcefada9ff23b8fe89c2b8c2bab6e4b3c5ec92447164c5b06c18bf@[::]:30303
peer2 enode://ae02f18ad58e96a309761e85df8dbceef6c5efe47facc10608ac4e0b002cf3901222a389b44a169bf09058c2b17cebe3d0f96078ebe58c6d52801a800b6b540f@[::]:30303
peer3 enode://1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52@[::]:30303

1.5 帐号创建

以peer1为例

1.5.1 进入peer1容器

$ sudo docker exec -it peer1 bash
root@peer1:/opt/geth/datadir# 

1.5.2 attach进节点

root@peer1:/opt/geth/datadir# geth attach ./geth.ipc 
Welcome to the Geth JavaScript console!

instance: Geth/v1.8.15-stable/linux-amd64/go1.11
 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

1.5.3 创建帐号

括号中是帐号解锁密码

> personal.newAccount("123456")
"0x8330c2a5c1399af554fbb5c231c696525f6c0ea9"

1.5.4 备份钱包私钥文件

创建帐号后,在主机挂载目录或容器指定的数据目录中均能看到生成的钱包私钥文件,需要备份好改文件,若丢失该文件,该钱包里的以太币及其他token就都玩儿完了:(

/var/lib/docker/volumes/geth-peer1-datadir/_data/keystore/
└── UTC--2018-09-13T08-17-21.665798564Z--8330c2a5c1399af554fbb5c231c696525f6c0ea9

/opt/geth/datadir/keystore/
`-- UTC--2018-09-13T08-17-21.665798564Z--8330c2a5c1399af554fbb5c231c696525f6c0ea9

1.5.5 各节点帐号信息

同样在其他2个节点创建帐号

这里仅在每个节点上创建了一个账号,当然你可以在每个节点上创建任意多个账号

节点名 帐号 IP
peer1 0x8330c2a5c1399af554fbb5c231c696525f6c0ea9 172.33.0.2
peer2 0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95 172.33.0.3
peer3 0x562433b23bf24060db587f8c1ef288ac57eab2b9 172.33.0.4

1.6 节点关联

1.6.1 关联拓扑

因为节点启动时,使用了--nodiscover,关闭了自动发现,故需要手工进行关联。

节点手工关联有多种方式,这里只介绍两种:命令式和配置式

1.6.2 命令式节点关联

peer1

  • 执行命令
web3.admin.addPeer('enode://ae02f18ad58e96a309761e85df8dbceef6c5efe47facc10608ac4e0b002cf3901222a389b44a169bf09058c2b17cebe3d0f96078ebe58c6d52801a800b6b540f@172.33.0.3:30303')

web3.admin.addPeer('enode://1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52@172.33.0.4:30303')
  • 查看连接
> web3.admin.peers
[{
    caps: ["eth/62", "eth/63"],
    id: "1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52",
    name: "Geth/v1.8.15-stable/linux-amd64/go1.11",
    network: {
      inbound: false,
      localAddress: "172.33.0.2:33674",
      remoteAddress: "172.33.0.4:30303",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 68609515,
        head: "0x649bd5691fa63397dbf2040fdd839802cdeb12060da7eb9e7021093b7ba4f3aa",
        version: 63
      }
    }
}, {
    caps: ["eth/62", "eth/63"],
    id: "ae02f18ad58e96a309761e85df8dbceef6c5efe47facc10608ac4e0b002cf3901222a389b44a169bf09058c2b17cebe3d0f96078ebe58c6d52801a800b6b540f",
    name: "Geth/v1.8.15-stable/linux-amd64/go1.11",
    network: {
      inbound: false,
      localAddress: "172.33.0.2:52858",
      remoteAddress: "172.33.0.3:30303",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 84596986,
        head: "0x3735f8a9e9c572dc23e2013f8aed439f8e5494008d3403161922858fa19a929c",
        version: 63
      }
    }
}]

peer2

peer2已于peer1相连,只需要关联peer3即可

  • 执行命令
web3.admin.addPeer('enode://1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52@172.33.0.4:30303')
  • 查看连接
> web3.admin.peers
[{
    caps: ["eth/62", "eth/63"],
    id: "1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52",
    name: "Geth/v1.8.15-stable/linux-amd64/go1.11",
    network: {
      inbound: false,
      localAddress: "172.33.0.3:54994",
      remoteAddress: "172.33.0.4:30303",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 84765477,
        head: "0x0f763d91b688dec472e0887f7993caae36030c75c9c7a271e8a1ffe3c1254066",
        version: 63
      }
    }
}, {
    caps: ["eth/62", "eth/63"],
    id: "bdf07fa1c2aa550005e13fd68b1ab0ccb1026669d93da1aee430124277f5e76f4f116b45c4dcefada9ff23b8fe89c2b8c2bab6e4b3c5ec92447164c5b06c18bf",
    name: "Geth/v1.8.15-stable/linux-amd64/go1.11",
    network: {
      inbound: true,
      localAddress: "172.33.0.3:30303",
      remoteAddress: "172.33.0.2:52858",
      static: false,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 84596986,
        head: "0x3735f8a9e9c572dc23e2013f8aed439f8e5494008d3403161922858fa19a929c",
        version: 63
      }
    }
}]

peer3

无需执行命令,在peer1和peer2上已经建立了关联

  • 查看连接
> web3.admin.peers
[{
    caps: ["eth/62", "eth/63"],
    id: "ae02f18ad58e96a309761e85df8dbceef6c5efe47facc10608ac4e0b002cf3901222a389b44a169bf09058c2b17cebe3d0f96078ebe58c6d52801a800b6b540f",
    name: "Geth/v1.8.15-stable/linux-amd64/go1.11",
    network: {
      inbound: true,
      localAddress: "172.33.0.4:30303",
      remoteAddress: "172.33.0.3:54994",
      static: false,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 84765477,
        head: "0x0f763d91b688dec472e0887f7993caae36030c75c9c7a271e8a1ffe3c1254066",
        version: 63
      }
    }
}, {
    caps: ["eth/62", "eth/63"],
    id: "bdf07fa1c2aa550005e13fd68b1ab0ccb1026669d93da1aee430124277f5e76f4f116b45c4dcefada9ff23b8fe89c2b8c2bab6e4b3c5ec92447164c5b06c18bf",
    name: "Geth/v1.8.15-stable/linux-amd64/go1.11",
    network: {
      inbound: true,
      localAddress: "172.33.0.4:30303",
      remoteAddress: "172.33.0.2:33674",
      static: false,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 84596986,
        head: "0x3735f8a9e9c572dc23e2013f8aed439f8e5494008d3403161922858fa19a929c",
        version: 63
      }
    }
}]

1.6.3 配置式节点关联

分别在peer1和peer2指定的数据文件目录datadir(/opt/geth/datadir)中,创建静态配置文件static-nodes.json,亦即在docker本地映射目录/var/lib/docker/volumes/geth-peerX-datadir/_data中去创建

/var/lib/docker/volumes/geth-peer1-datadir/_data]# cat static-nodes.json
[
"enode://ae02f18ad58e96a309761e85df8dbceef6c5efe47facc10608ac4e0b002cf3901222a389b44a169bf09058c2b17cebe3d0f96078ebe58c6d52801a800b6b540f@172.33.0.3:30303",
"enode://1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52@172.33.0.4:30303"
]

/var/lib/docker/volumes/geth-peer2-datadir/_data]# cat static-nodes.json
[
"enode://1294e59212c3b80d731040a1a1aa9a21efe2aaccbb1f01978ad717494ef091a92a04e206d1e39bd80f0ab80043f4bd0998c7728af4ca0d5eabfa11736f162b52@172.33.0.4:30303"
]

节点启动后,便会自动加载静态配置文件,进行指定节点的关联,连接关系同上,这里就省略展示。

1.7 以太坊geth客服端的基本操作

1.7.1 启动挖矿

开启挖矿后,方能实现合约的部署及转账等操作

start括号里面的值代表线程数

若需要停止挖矿,使用命令:miner.stop()

> miner.start(1)
null

1.7.2 查看余额

挖一会儿后,便能通过以下命令查看到账户以太余额

  • peer1
> web3.fromWei(eth.getBalance(eth.accounts[0]))
1890
  • peer2
> web3.fromWei(eth.getBalance(eth.accounts[0]))
1325

1.7.3 转账

以peer1的帐号向peer2的帐号转账1000个以太币为例

> personal.unlockAccount(eth.accounts[0])
Unlock account 0x8330c2a5c1399af554fbb5c231c696525f6c0ea9
Passphrase: 
true

> eth.sendTransaction({from:eth.accounts[0], to:"0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95", value:web3.toWei(1000, "ether")})
"0xb672882e6fa63be9c0e7d5973bcb3dff054bdd759a0821c7b77eb54c63ff1806"
  • 注:转账之前,需先对帐号解锁,否则报错:
> eth.sendTransaction({from:eth.accounts[0], to:"0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95", value:web3.toWei(1000, "ether")})
Error: authentication needed: password or unlock
    at web3.js:3143:20
    at web3.js:6347:15
    at web3.js:5081:36
    at <anonymous>:1:1

待挖矿确认后,可查看到账户的变化:

注:实际上账户金额,已不止这么多,因为有新的区块产生,为了便于理解,对数值做了下调整

  • peer1
> web3.fromWei(eth.getBalance(eth.accounts[0]))
890
  • peer2
> web3.fromWei(eth.getBalance(eth.accounts[0]))
2325

2 ERC20代币合约编写

2.1 ERC20标准介绍

2.1.1 为什么需要ERC20?

目的是为统一合约标准,以利于交易所或钱包开发者支持你的代币发行及储存,否则交易所及钱包开发者就需要再写一份自定义代码来适配你的智能合约。

2.1.2 ERC20标准

ERC20定义了一些标准的接口函数:balanceOf 、 totalSupply 、transfer 、transferFrom 、approve和allowance 。 以及一些可选的字段,例如通证名称、符号以及小数保留位数等。

详见:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md

2.1.3 ERC20接口

contract ERC20 {
   function totalSupply() constant returns (uint theTotalSupply);
   function balanceOf(address _owner) constant returns (uint balance);
   function transfer(address _to, uint _value) returns (bool success);
   function transferFrom(address _from, address _to, uint _value) returns (bool success);
   function approve(address _spender, uint _value) returns (bool success);
   function allowance(address _owner, address _spender) constant returns (uint remaining);
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}
  • 功能介绍:
函数名 功能
totalSupply 返回存在于流通中的通证(Token)总量
balanceOf 返回指定账户地址的通证余额
transfer 让调用方将指定数量的通证发送到另一个地址,即转账
transferFrom 允许智能合约自动执行转账流程并代表所有者发送给定数量的通证
approve 调用方授权给定的地址可以从其地址中提款
allowance 返回被允许转移的余额数量
event Transfer 事件通知,当token被转移时,必须调用触发,类似回调,当事件发生时,会得到通知
event Approval 事件通知,当任何成功调用approve后,必须调用触发

2.2 代币合约编写与执行

2.2.1 代币属性

属性
代币名称 XuanYuan Coin(萱媛币)
代号 xyc
发行量 406647615087857(俩妞8位生日相乘)

2.2.2 合约编写

使用在线编辑器:http://remix.ethereum.org

直接附上完整合约代码,限于篇幅,具体合约代码实现的细节就不展开介绍,比较简单的实现,相信也比较容易看明白。

pragma solidity ^0.4.25;

contract ERC20 {
   function totalSupply() public constant returns (uint theTotalSupply);
   function balanceOf(address _owner) public constant returns (uint balance);
   function transfer(address _to, uint _value) public returns (bool success);
   function transferFrom(address _from, address _to, uint _value) public returns (bool success);
   function approve(address _spender, uint _value) public returns (bool success);
   function allowance(address _owner, address _spender) public constant returns (uint remaining);
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}

contract xyc is ERC20 {
    string name = "XuanYuan Coin";
    string symbol = "xyc";
    uint private xyTotalSupply;
    mapping(address=>uint) balances;
    mapping(address=>mapping(address=>uint)) allowances;
    address public owner;
    
    constructor(address _owner, uint _total) public {
        owner = _owner;
        xyTotalSupply = _total;
        balances[owner] = _total;
    }
    
   function totalSupply() public constant returns (uint theTotalSupply) {
       theTotalSupply = xyTotalSupply;
       return theTotalSupply;
   }
   
   function balanceOf(address _owner) public constant returns (uint balance) {
       return balances[_owner];
   }
   
   function transfer(address _to, uint _value) public returns (bool success) {
       require(_to != address(0));
       require(_value <= balances[msg.sender]);
       require(balances[_to] + _value >= balances[_to]);
       
       balances[msg.sender] -= _value;
       balances[_to] += _value;
       emit Transfer(msg.sender, _to, _value);
       
       return true;
   }
   
   function transferFrom(address _from, address _to, uint _value) public returns (bool success) {
       require(_to != address(0));
       require(_value <= balances[_from]);
       require(_value <= allowances[_from][msg.sender]);
       require(balances[_to] + _value >= balances[_to]);
       
       balances[_from] -= _value;
       balances[_to] += _value;
       
       allowances[_from][msg.sender] -= _value;
       emit Transfer(_from, _to, _value);
       
       return true;
   }
   
   function approve(address _spender, uint _value) public returns (bool success) {
       require(_value <= balances[msg.sender]);
       allowances[msg.sender][_spender] = _value;
       emit Approval(msg.sender, _spender, _value);
       return true;
   }
   
   function allowance(address _owner, address _spender) public constant returns (uint remaining) {
       return allowances[_owner][_spender];
   }
   
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}

2.2.3 合约编译

  • 运行环境选择Web3 Provider
  • 连接任意一个私有节点
  • 切换到Compile页,点击Start to compile按钮,可以勾选Auto compile以便在保存时就自动编译:

2.3.4 解锁账户

首要的是开启挖矿,和账户解锁,方能进行合约的部署和执行

> miner.start(1)
null

> personal.unlockAccount(eth.accounts[0], "123456", 1000000)
true

2.3.5 合约部署

  • 选择要部署的合约,填写构造方法中的所有人地址和发行量后,点击transact
  • 部署日志输出
  • 查看到已部署好的合约

2.3 智能合约的基本操作

2.3.1 查看所有者账户XYC余额

2.3.2 向peer2的一个账号转账

  • 查看peer2上的帐号
> eth.accounts
["0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95"]
  • 当前该帐号XYC余额为0
  • 执行转账操作
  • 再次查看余额,peer2的这个账户已经多了个2018个XYC了

3 使用Go语言调用智能合约实践

3.1 将智能合约编译成Go源码

目的是为生成智能合约各个方法的Go语言实现接口

  • 步骤示意
  • 在remix中拷贝合约的abi文件,存入xyc.abi文件中
  • 执行命令
$ abigen --abi xyc.abi --pkg xyc --type xyc --out xyc.go
参数 含义
pkg 指定编译成go文件的package名称
abi 指定abi文件,该文件在remix部署合约后可得到
type go文件的入口函数,可以认为是类名
out 指定输出的go文件名
  • 关键代码片段
// Xyc is an auto generated Go binding around an Ethereum contract.
type Xyc struct {
    XycCaller     // Read-only binding to the contract
    XycTransactor // Write-only binding to the contract
    XycFilterer   // Log filterer for contract events
}  

// NewXyc creates a new instance of Xyc, bound to a specific deployed contract.
func NewXyc(address common.Address, backend bind.ContractBackend) (*Xyc, error) {
    contract, err := bindXyc(address, backend, backend, backend)
    if err != nil {
        return nil, err 
    }   
    return &Xyc{XycCaller: XycCaller{contract: contract}, XycTransactor: XycTransactor{contract: contract}, XycFilterer: XycFilterer{contract:   contract}}, nil
}     

3.2 新建账户

在用Go调用智能合约前,我们先看看如何创建账户

package main

import (
    "github.com/ethereum/go-ethereum/rpc"
    "fmt"
    "log"
)

func newAccount(pass string)  {
    cli, err := rpc.Dial("http://localhost:8545")
    if err != nil {
        log.Fatal("failed to conn to eth", err)
    }

    var accid string
    cli.Call(&accid, "personal_newAccount", pass)

    fmt.Println(accid)
    //0x503f332e28c0eb56f456aa05ded6f52a9d45a868
}

func main() {
    newAccount("123456")
}
  • 在peer1上查看账号新增帐号
> eth.accounts
["0x8330c2a5c1399af554fbb5c231c696525f6c0ea9", "0x503f332e28c0eb56f456aa05ded6f52a9d45a868"]

3.3 查询账户余额

package main

import (
    "fmt"
    "log"
    "XYC/xyc"
    "os"
    "math/big"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/rpc"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
)

var contractAddr = "0xaf959e95fb23c666c05e374290517b4723b847d6"

func getBalance(accid string) {
    cli, err := ethclient.Dial("http://localhost:8545")
    if err != nil {
        log.Fatal("failed to conn to eth", err)
    }

    token, err := xyc.NewXyc(common.HexToAddress(contractAddr), cli)
    if err != nil {
        log.Fatal("get contract err", err)
    }

    val, err := token.BalanceOf(nil, common.HexToAddress(accid))
    if err != nil {
        log.Fatal("get balance err", err)
    }
    fmt.Println("get val:", val)
}

func main() {
    getBalance("0x8330c2a5c1399af554fbb5c231c696525f6c0ea9")
    getBalance("0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95")
}
//OUTPUT:
//get val: 406647615085839
//get val: 2018

3.3 转账操作

节点2 a5c账号有2018XYC,给节点3 562账号转1000XYC

比较关键的是要打开钱包文件以授权转账

package main

import (
    "fmt"
    "log"
    "XYC/xyc"
    "os"
    "math/big"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/rpc"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
)

var contractAddr = "0xaf959e95fb23c666c05e374290517b4723b847d6"

func transfer(from, to, pass string, val int64) {
    cli, err := ethclient.Dial("http://localhost:8545")
    if err != nil {
        log.Fatal("err to connect geth")
    }
    token, err := xyc.NewXyc(common.HexToAddress(contractAddr), cli)
    if err != nil {
        log.Fatal("get contract err", err)
    }

    y, err := token.BalanceOf(nil, common.HexToAddress(from))
    if err != nil {
        log.Fatal("get balance err", err)
    }

    fmt.Println("get val:", y)

    fd, err := os.Open("/var/lib/docker/volumes/geth-peer2-datadir/_data/keystore/UTC--2018-09-13T08-26-10.647670256Z--a5cfb51caede4c8fb2634d88e6ac860bfa048c95")
    auth, err := bind.NewTransactor(fd, pass)
    if err != nil {
        log.Fatal("get NewTransactor err", err)
    }

    _, err = token.Transfer(auth, common.HexToAddress(to), big.NewInt(val))
    if err != nil {
        log.Fatal(" Transfer err", err)
    }
}

func main() {
    transfer("0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95",
        "0x562433b23bf24060db587f8c1ef288ac57eab2b9",
        "123456", 1000)
}
  • 执行前:
归属节点 账号 XYC
peer2 0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95 2018
peer3 0x562433b23bf24060db587f8c1ef288ac57eab2b9 0
  • 执行后:

可见peer2的a5c帐号减少1000XYC,peer3的562帐号增加1000XYC,转账完成。

归属节点 账号 XYC
peer2 0xa5cfb51caede4c8fb2634d88e6ac860bfa048c95 1018
peer3 0x562433b23bf24060db587f8c1ef288ac57eab2b9 1000

4 写在后面

本文通过为女儿发一枚数字货币,准确说是基于以太坊ERC20标准的通证(Token),以此来介绍了基于Docker环境搭建以太坊多节点私有网络的过程,以及如何通过remix网页工具编写solidity代币智能合约,及其部署和调用的方法,最后介绍了通过Go语言进行以太坊智能合约调用的方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容