Akawa

ETY001的博客

引子

最近真的是跟 Webpack 作上了,又在 Webpack 上花费了宝贵的两天的工作时间。

上次好不容易把 iview-admin 集成进 Laravel,本来以为可以开开心心的开发了,结果在收尾的功能中需要加入富文本编辑器。由于 iview-admin 中集成了一个富文本编辑器,于是就把集成的这个 wangeditor 引入过来使用了。

结果,webpack 对于文件的编译速度瞬间从几秒变成了一分半钟。

WTF!

找原因

Laravel Mix默认的开发环境的编译命令如下:

1
cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js

但是这个坑爹的配置,根本看不出来编译慢的问题出在哪里好吗?!(如下图,加入 wangedit 编辑器后,每次在70%位置就会卡主大约70多秒,甚至更多)

怎么才能找到是具体哪个文件编译慢呢?

最先想到的是看看 webpack.js 的可用参数,最终找到了一个 --profile 参数,加上这个参数后,稍微多了点信息,但是完全不够啊。。。还是没法确认问题在哪啊。。。

接下来的大部分时间都花在了搜索引擎上,主要围绕的搜索关键词是 webpack build slow 之类的。但是完全没有什么用。

又切换了个思路,看看有没有什么好的调试工具。群里有人推荐我 speed-measure-webpack-plugin 。

看了下说明,是个好插件,可惜我这个项目的 webpack 套在 laravel-mix 里面,我只能通过 laravel-mix 提供的方法来配置 webpack 的参数,而 speed-measure-webpack-plugin 的使用方法是下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
把原来的
const webpackConfig = {
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
}
改为
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
});

如果我想要用 speed-measure-webpack-plugin 的话,需要在 node_modules 目录下找到 laravel-mix 包,然后修改里面的代码,让 laravel-mix 在 merge 完用户自定义的配置后,调用 speed-measure-webpack-plugin ,再传给 webpack。

我看了下 laravel-mix 的代码,感觉就目前我的水平要想实现的话,需要耗费不止两天的时间吧,这样方向也有点偏。于是这个方案也抛弃了。

柳暗花明

虽然 speed-measure-webpack-plugin 不能用,但是给了我一个启发,那就是有没有可以直接写在 webpack 的 plugins 配置中调用的调试插件呢?

功夫不负有心人,在大半天之后,偶然在 Stack Overflow 的一个问题的回复中看到了 simple-progress-webpack-plugin 这个插件。不过当时心是灰的,因为找了很久没找到,打开这个插件的文档,里面也没有个截图啥的,完全就是死马当活马医,试了试。按照文档提示,先安装 npm install simple-progress-webpack-plugin --save-dev,然后修改 laravel-mix 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const SimpleProgressWebpackPlugin = require( 'simple-progress-webpack-plugin' ); // 这是新增的配置

mix.js('resources/js/admin.js', 'public/static')
.sass('resources/sass/admin.scss', 'public/static')
.webpackConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'resources/js/'),
'_c': path.resolve(__dirname, 'resources/js/components'),
},
},
output: {
chunkFilename: 'static/chunks/[name].js',
},
plugins: [
new SimpleProgressWebpackPlugin() // 这是新增的配置
],
})
.babelConfig({
persets: ['@vue/app']
})
.version();

这次打印出来的信息有些用处了,如下图所示

这次知道是在 Optimize modules 阶段速度慢了,具体是在 Module and chunk tree optimization 这里。于是以 webpack optimize modules slow 为关键词搜索,结果第一条就解决了问题:

妈蛋的原来是在异步加载优化这里慢啊。按照 issue 里提到的 babel-plugin-dynamic-import-node 插件,搜索了下,找到安装部署方法 npm install babel-plugin-dynamic-import-node --save-dev,然后配置下 laravel-mix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const SimpleProgressWebpackPlugin = require( 'simple-progress-webpack-plugin' );

mix.js('resources/js/admin.js', 'public/static')
.sass('resources/sass/admin.scss', 'public/static')
.webpackConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'resources/js/'),
'_c': path.resolve(__dirname, 'resources/js/components'),
},
},
output: {
chunkFilename: 'static/chunks/[name].js',
},
plugins: [
new SimpleProgressWebpackPlugin()
],
})
.babelConfig({
plugins: ['dynamic-import-node'], // 新增配置
})
.version();

再次编译,瞬间完成!!撒花!!

区别

但是看到有提到是在 dev 环境使用 babel-plugin-dynamic-import-node 插件,这是为啥呢?用 require 和 import() 有啥区别呢?

初步探索了下发现使用 babel-plugin-dynamic-import-node 把 import() 替换成 require 编译的话,就没有 chunk files 了,相当于是跳过了 code split 之类的阶段吧(我猜是这样)。

可以看下下面的两个截图,第一个是不使用 babel-plugin-dynamic-import-node 进行生产环境 build,第二个是使用的情况进行 build。

从最终的文件大小和编译所需要的时间来看,目前不用 chunk 功能,并没有什么问题,于是最终选择使用 babel-plugin-dynamic-import-node 以此来解决编译时间过长的原因。

具体深入进去还有什么区别没时间探索了,目前编译后,使用正常,那就先这样吧。。。

最近需要开发一套后台,挑来挑去,觉得 iview-admin 不错,决定要使用这个后台前端框架。上面就是最终的效果图。

但是由于我们的后台是 Laravel 框架用的 Laravel Mix, 与 iview-admin 用的 vue-cli-service 不一样,于是整合起来有些麻烦,虽然最简单的方法是开两个库独立开来,但是这会让部署什么的增加一些环节,毕竟这个项目开发就我自己,没必要折腾那么多,于是就想要整合。

从周二开始折腾,折腾来折腾去,将近一周过去了,终于搞定了(虽然解决了最后一个问题,但是原理是啥我也不知道),现在已经可以使用 Laravel Mix 来直接替代 vue-cli-service 编译啦。

为了方便自己以后再次使用,所以记录下关键的几个环节。

首先需要的就是调整下 package.json,我的代码最终版如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"@vue/babel-preset-app": "^3.5.0",
"axios": "^0.18",
"bootstrap": "^4.0.0",
"chai": "^4.1.2",
"clipboard": "^2.0.0",
"codemirror": "^5.38.0",
"countup": "^1.8.2",
"cropperjs": "^1.2.2",
"cross-env": "^5.1",
"dayjs": "^1.7.7",
"echarts": "^4.0.4",
"html2canvas": "^1.0.0-alpha.12",
"iview": "^3.3.0",
"iview-area": "^1.5.17",
"jquery": "^3.2",
"js-cookie": "^2.2.0",
"laravel-mix": "^4.0.14",
"less": "^2.7.3",
"less-loader": "^4.0.5",
"lodash": "^4.17.5",
"mockjs": "^1.0.1-beta3",
"popper.js": "^1.12",
"resolve-url-loader": "2.3.1",
"sass": "^1.17.2",
"sass-loader": "7.*",
"simplemde": "^1.11.2",
"sortablejs": "^1.7.0",
"tree-table-vue": "^1.1.0",
"v-org-tree": "^1.0.6",
"vue": "^2.5.17",
"vue-i18n": "^7.8.0",
"vue-router": "^3.0.2",
"vue-template-compiler": "^2.5.13",
"vuedraggable": "^2.16.0",
"vuex": "^3.0.1",
"wangeditor": "^3.1.1",
"xlsx": "^0.13.3"
}
}

这里是在 Laravel 原有的 package.json 基础上,把 iview-admin 需要的 依赖 一一手动添加进去的。

其中命令中的 node_modules/webpack/bin/webpack.js 应该是调用的 laravel-mix 包中的依赖。

完成 package.json 之后,我们使用 npm install 安装一下所有的依赖,然后把 iview-admin 项目克隆到本地,把其中的 src 目录下的所有文件复制到 Laravel 项目的 resources/js 目录下。如果你想变更入口名称,可以把 main.js 改成你自己想要的名字,这里我改为了 admin.js。

之后修改一下 Laravel 项目根目录下的 webpack.mix.js 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const mix = require('laravel-mix');

mix.js('resources/js/admin.js', 'public/static')
.sass('resources/sass/admin.scss', 'public/static')
.webpackConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'resources/js/'),
'_c': path.resolve(__dirname, 'resources/js/components'),
},
},
output: {
chunkFilename: 'static/chunks/[name].js',
},
})
.babelConfig({
"presets": [
"@vue/app",
],
})
.version();

在原有的基础上增加了 resolve 配置,为了能够正确解析 @ 和 _c 开头的引入包路径。增加了 output 配置,让所有的 chunk 文件与主文件同目录(这条配置可不加)。增加了 babelConfig 配置,为了解决 动态import 语法解析的问题(这个就是目前还没有弄明白的那个地方)。

完成上面的步骤后,就完成了大半了。

之后,在 Laravel 项目中增加一个后台的路由,我是直接加在 routers/web.php 中的,如下

1
Route::get('admin', 'AdminController@index');

创建 AdminController,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AdminController extends Controller
{
public function index(Request $request) {
return view('admin/index');
}
}

创建视图 resources/view/admin/index.blade.php,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BACC</title>
<meta name="csrf-token" content="{{ csrf_token() }}" api_token="{{ \Auth::check() ? 'Bearer ' . \Auth::user()->api_token : 'Bearer ' }}">
<link rel="stylesheet" href="{{ mix('static/admin.css') }}">
<!-- <script src="{{ asset('/js/tinymce.min.js') }}"></script> -->
</head>
<body>
<div id="app"></div>

<script src="{{ mix('static/admin.js') }}"></script>
</body>
</html>

最后要修改的就是 iview-admin 的路由模式了。iview-admin 默认使用的是 history,而这个配置将会让 iview-admin 的路由与 Laravel 的路由冲突,因此需要去掉这个配置。这个配置项在 resources/js/router/index.js 中,把下面的代码中的 mode: 'history'去掉即可。

1
2
3
4
const router = new Router({
routes,
mode: 'history'
})

所有配置完成,执行 npm run watch,如果一切顺利的话,所有的前端文件会编译完成,直接访问 http://localhost:8000/admin 就能看到 iview-admin 已经成功整合完成了!

撒花!

最近新开了一个钱包的App项目,直接把之前的代码从 Github 上拖回来,但是执行 cordova prepare -d 一直不成功,显示 current working directory is not a cordova-based project.。

经过查看源码,发现在 https://github.com/apache/cordova-lib/blob/master/src/cordova/util.js#L102 这个地方的代码有问题。

正常情况下,这个函数返回 2 就可以了,由于我的 .gitignore 中把 www 和 platforms 两个目录都给忽略了,这就导致了一直返回 0,进而导致另外一个位置获取到的项目目录一直是 /,这显示不是项目根目录。。。

于是手动创建了 www 和 platforms 两个空目录后,一切就正常了。。。。。坑爹。。。。。。

在开发 SteemTools 服务号的文章单独页面的时候,遇到的一个问题就是有些人的图片存放在了被墙的服务器上了,导致文章中的图片无法正常显示。

从网上搜索了下,找到了一个用 node 写的图片代理,我已经 fork 到了自己的库中,https://github.com/ety001/node-image-proxy 。

为了方便部署和管理,把这个服务封装进了 Docker 中。

运行命令如下:

1
docker run -itd --name node-image-proxy -p 9091:9091 -v /data/node-image-cache:/app/cache --restart always ety001/node-image-proxy

其中 /data/node-image-cache 目录是用来存储缓存的,自己手动建立一个目录就好了。

启动成功后,可以用 nginx 的反向代理来实现 https,也可以直接使用,只需要把要代理的图片地址放到 url 后面就好了,例如这样:

1
https://img.steemtools.top/http://newappaz.oss-cn-hongkong.aliyuncs.com/wherein_images/post/20190224/89a2802d622c4940b2ece51eea04aecd.jpg

这样只需要把服务部署在国外服务器上,然后在解析 markdown 的时候把原来的图片地址加上代理地址后,就可以正常的访问文章中的图片了。

起因

由于最近在开发恒星币的钱包,其中有部分数据,如果直接通过恒星的 API 获取则效率太低,影响页面展示体验。
于是用 hapijs 来开发一些定制的接口,批量获取并缓存数据,来提升页面访问效果。

之前在 《使用 hapijs 快速构建自己的 api 服务》 这篇文章中有写到 hapijs 默认使用 catbox 的 catbox-memory 组件来缓存数据。

我这次还是按照上次的方法来使用缓存,但是却没有成功,每次提交请求,都是重新去获取数据,而没有走缓存。

祸起文档和配置

翻了好几遍 hapijs 的文档,也使用搜索引擎查了,折腾了3个多小时无果,最终决定一边读源码一边在代码加标记输出。

在每个关键位置都打了输出,但是没有看到任何报错和问题。无意中看到了 hapijs 文档中的一个配置参数 generateOnReadError,作用是当在读取缓存时遇到错误时是否报错,而默认则是关掉的!!!WTF!!!!

配置这个参数为 false 后,得再配合 try {} catch () {} 才显示了错误信息,原来是缓存的 key 非法。

比较这次和上次使用 cache 功能的代码,找到了一个不同点,就是上次是使用的 string 作为 key,而这次使用的是 object 作为 key。

不过在 hapijs 关于 cache 的 文档 中,关于 generateFunc 参数的解释里有一句 the \'id\' string or object provided to the \'get()\' method.,看上去 id 用 object 也没有问题啊。

于是我又去读了 catbox 的源码,最终看到 catbox 的一段关于获取缓存信息的 代码。

在第88行,原来特么的只接受 {id: id} 这种样子的对象啊?!!!!并且上面82行的注释也列出来了。。。

又去看了下 catbox 的文档中关于 get() 方法的介绍,有这么一句 id - the unique item identifier (within the policy segment). Can be a string or an object with the required 'id' key.。

这就恍然大悟了。。。。。。。。。

原来对象中必须包含 id 这个字段啊,原来要是提交对象的话,需要自己来定义索引啊?!!!

修改下代码,这个坑过了。

不容易不容易啊。。

虽然平时主要用 Shadowsocks,但是架不住有时候没法安装 Shadowsocks 的客户端,那么就还是需要 PPTP VPN 或者 L2TP VPN。

最早的时候,是使用的各种一键安装脚本,但是由于系统版本差异,每次需要安装的时候,都要现找可用的一键脚本,太费劲了。于是从网上找了别人封装好的 Docker 镜像,这篇文章总结下,基本上就是一条语句就搞定了。

PPTP VPN

使用的镜像是 mobtitude/vpn-pptp,首先需要把用户名和密码配置一下,打开 /etc/ppp/chap-secrets,

1
2
3
# Secrets for authentication using CHAP
# client server secret IP addresses
ety001 * 123456 *

上面的就是配置了一个用户名 ety001 和 密码 123456 的用户,然后执行下面的命令就可以了,

1
docker run -d --name pptp --restart always  --privileged -p 1723:1723 -v /etc/ppp/chap-secrets:/etc/ppp/chap-secrets mobtitude/vpn-pptp

最后检查下 tcp 1723 端口在防火墙上是否打开就可以了。

L2TP + IPSEC VPN

使用的镜像是 hwdsl2/ipsec-vpn-server,需要先配置下用户名、密码和PSK,新建一个环境变量的文件 /etc/l2tp-env,内容如下

1
2
3
VPN_IPSEC_PSK=abcdef
VPN_USER=ety001
VPN_PASSWORD=123456

上面的就是配置了一个用户名 ety001,密码 123456,PSK 为 abcdef 的用户,然后执行下面的命令就可以了,

1
2
3
4
5
6
7
8
9
docker run --name ipsec-vpn-server \
--env-file /etc/l2tp-env \
--restart=always \
-p 500:500/udp \
-p 4500:4500/udp \
-p 1701:1701/udp \
-v /lib/modules:/lib/modules:ro \
-d --privileged \
hwdsl2/ipsec-vpn-server

最后检查下 udp 500 和 udp 4500 端口在防火墙上是否打开就可以了。

Shadowsocks

最后再附带上一个一句话部署 Shadowsocks 的命令,先创建个配置文件 /etc/shadowsocks.json,内容如下

1
2
3
4
5
6
7
8
9
{
"server":"0.0.0.0",
"server_port": 10000,
"local_address":"127.0.0.1",
"local_port":1080,
"password":"ety001",
"timeout":60,
"method":"aes-256-cfb"
}

然后执行下面的命令部署

1
$ docker run -d -p 10000:10000 -v /etc/shadowsocks.json:/conf/shadowsocks.json --restart=always --name ss ety001/ss

最近开始在开发 Cordova + Webpack + Vuejs 架构的 App,开发 Demo 的时候,最麻烦的就是测试了。

由于我是先使用 cordova 的工具创建 cordova 项目,然后用 vue-cli 再在 cordova 中创建 vuejs 项目。
这样在开发的时候,需要先使用 npm run dev 调试 vuejs ,然后开发差不多了,需要调用 cordova 插件的时候,再执行 cordova prepare browser 和 cordova run browser 调试 cordova 插件,每次改完代码还要再执行 npm run build && cordova prepare browser。

这样就很烦啊!

为了在正式开发项目前解决这个问题,我研究了萤火钱包的实现和 webpack 的文档。

萤火钱包的实现方式是自己用 Node 写了一个 Web 服务,代码在这里:https://github.com/StellarCN/firefly/blob/master/build/dev-server.js 。

由于 Webpack 可以使用 require 的方式和 webpack-dev-server 两种方式对代码进行打包,萤火钱包就是在自己写的 Server 中,先引用 webpack 打包代码(24行–41行),然后用 express 给 cordova 的 js bridge 提供解析(66行–88行),最后在 package.json https://github.com/StellarCN/firefly/blob/master/package.json#L18 中加入一行代码,完成 cordova prepare 后再启动自己写的 Node 服务器。

按照萤火的思路尝试了大半天,一直都搞不定 webpack 的配置,一直给我报错,如下

1
2
3
4
WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
- configuration misses the property 'entry'.
object { <key>: non-empty string | [non-empty string] } | non-empty string | [non-empty string] | function
-> The entry point(s) of the compilation.

最后只得再换个思路,用 webpack-dev-server 再试试。

由于 vuejs 默认就是用 webpack-dev-server 作为测试环境,所以需要改动的文件还是比较少,只需要修改 build/webpack.dev.conf.js 中的配置和 package.json 就好了。

在 build/webpack.dev.conf.js 中的 devServer 配置项里加上下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
before(app){
// serve Cordova javascript and plugins
const cordovaPlatformPath = path.join(__dirname, '../platforms/browser/www')
app.use('/plugins', express.static(path.join(cordovaPlatformPath, 'plugins')))
app.get(
[
'/cordova.js',
'/cordova_plugins.js',
],
function (req, res) {
try {
res.sendFile(path.join(cordovaPlatformPath, req.path))
} catch(err) {
console.log(err)
}
})
// serve Cordova config.xml
app.get('/config.xml',
function(req,res){
try{
res.sendFile(path.join(__dirname, '../'+req.path))
} catch(err){
consol.log(err)
}
})
},

另外再在该文件头部加入 express 包的引用。

通过 devServer.before 加入 cordova 的 js bridge,这样就可以用 webpack-dev-server 来解析 js bridge 了。

最后在 package.json 中修改下原有的 dev 命令如下:

1
"dev": "cordova prepare browser && cross-env PORT=3000 webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",

这样以后只需要执行 npm run dev 就可以了,只有当用 cordova 安装了新插件的时候,才需要重启下 npm run dev。


再插入个小修改。我在 src/main.js 中加入了如下的代码,可以让 cordova js bridge 动态插入进 APP:

1
2
3
4
5
6
7
// add cordova.js only if serving the app through file://
if (window.location.protocol === 'file:' || window.location.port === '3000') {
const cordovaScript = document.createElement('script');
cordovaScript.setAttribute('type', 'text/javascript');
cordovaScript.setAttribute('src', 'cordova.js');
document.body.appendChild(cordovaScript);
}

OVER!!

今天在进行demo的联调的时候,微信端的扫一扫相册里一直不显示app下载到本地的二维码。

这个二维码是服务端php生成的,最初以为微信相册的所有照片不显示,是因为格式的问题。

在尝试了更换格式后,发现问题仍然不能解决。

于是找了一张微信能够识别的图片,然后分析它跟我用php生成的图片的 EXIF 信息。

又折腾了大约1–2个小时,把我php生成的图片的 EXIF 调整的跟正常图片一样,发现还是不行。

不是格式的问题,不是 EXIF 的问题,感觉整个人都不好了。。。

想来想去,想了好久,不知道怎么突然想起来文件大小。

可能是我发现我用php生成的图片都是小于4kb的吧。

于是我分别找了一张 21kb+ 的,一张 16kb+ 的,一张 11kb+ 的,一张 6kb+ 的图片。

然后传到手机上,用微信扫一扫相册打开,发现除了那个 6kb+ 的图片识别不出来,其他的都可以。

尼玛啊!!!!!弄了半天原来是文件大小的原因!!!!!!

看来微信在所有图片那个分类下优先显示大于 10kb 的图片啊。。。。

前言

ETH的多签是依靠智能合约实现的,也就是说持币的是智能合约,通过在智能合约里设置条件,来使转账之类的权限通过多人签名来集体控制。

从网上找到了一段简单的多签智能合约,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
pragma solidity ^0.4.21;

contract MultiSigWallet{
address private owner;
mapping (address => uint8) private managers;

modifier isOwner{
require(owner == msg.sender);
_;
}

modifier isManager{
require(
msg.sender == owner || managers[msg.sender] == 1);
_;
}

uint constant MIN_SIGNATURES = 3;
uint private transactionIdx;

struct Transaction {
address from;
address to;
uint amount;
uint8 signatureCount;
mapping (address => uint8) signatures;
}

mapping (uint => Transaction) private transactions;
uint[] private pendingTransactions;

function MultiSigWallet() public{
owner = msg.sender;
}

event DepositFunds(address from, uint amount);
event TransferFunds(address to, uint amount);
event TransactionCreated(
address from,
address to,
uint amount,
uint transactionId
);

function addManager(address manager) public isOwner{
managers[manager] = 1;
}

function removeManager(address manager) public isOwner{
managers[manager] = 0;
}

function () public payable{
emit DepositFunds(msg.sender, msg.value);
}

function withdraw(uint amount) isManager public{
transferTo(msg.sender, amount);
}
function transferTo(address to, uint amount) isManager public{
require(address(this).balance >= amount);
uint transactionId = transactionIdx++;

Transaction memory transaction;
transaction.from = msg.sender;
transaction.to = to;
transaction.amount = amount;
transaction.signatureCount = 0;
transactions[transactionId] = transaction;
pendingTransactions.push(transactionId);
emit TransactionCreated(msg.sender, to, amount, transactionId);
}

function getPendingTransactions() public isManager view returns(uint[]){
return pendingTransactions;
}

function signTransaction(uint transactionId) public isManager{
Transaction storage transaction = transactions[transactionId];
require(0x0 != transaction.from);
require(msg.sender != transaction.from);
require(transaction.signatures[msg.sender]!=1);
transaction.signatures[msg.sender] = 1;
transaction.signatureCount++;

if(transaction.signatureCount >= MIN_SIGNATURES){
require(address(this).balance >= transaction.amount);
transaction.to.transfer(transaction.amount);
emit TransferFunds(transaction.to, transaction.amount);
deleteTransactions(transactionId);
}
}

function deleteTransactions(uint transacionId) public isManager{
uint8 replace = 0;
for(uint i = 0; i< pendingTransactions.length; i++){
if(1==replace){
pendingTransactions[i-1] = pendingTransactions[i];
}else if(transacionId == pendingTransactions[i]){
replace = 1;
}
}
delete pendingTransactions[pendingTransactions.length - 1];
pendingTransactions.length--;
delete transactions[transacionId];
}

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

部署合约

我本地的测试网络是使用 Ganache 搭建的,如下图

我修改了我的 Ganache 的端口为 8545。

然后打开 Remix,在右边栏选择 Run,在 Environment 中选择 Web3 Provider,如下图

在弹出的层中,输入本地地址和刚才配置的8545端口,就可以连接到 Ganache 的环境了,如下图

连接成功后,会看到在 Environment 下面的 Account 栏中会列出所有 Ganache 中的测试账号。

我们选择第一个账号作为部署合约的主账号,即合约的 owner。

在编辑器中创建一个新的合约文件,把上面的合约代码复制进编辑器,右边栏选择 Compile ,
在 Select new compiler version 中选择 0.4.21 版本的编译器,等待编译完成。
完成后,最下面出现绿色的提示信息,如下图

再切换回 Run 标签,点击 Deploy 就可以部署了。
部署成功后,在 Deployed Contracts 中就能看到新部署的合约了,下拉打开可以看到合约的各个接口,如图

增加合约管理员

在右边栏找到合约的 addManager 接口,下拉打开输入一个以太钱包地址,然后点击 transact 即完成添加。
这里我把 Ganache 的第 2 到 4 的地址加入进管理员中。

给合约内转账

切换回 Ganache,在 Transactions 中找到我们刚才部署的合约地址,0x08ea5409922D4204C0E03d17243E77c3810C0CCe,

使用任意的其他账号转账任意个 ETH 进合约里。
我用的是 MyEtherWallet进行的转账,只需要把节点设置成本地网络,
然后用私钥模式登陆转账即可,这里就略过不说了。

转完账后,在 Remix 的 Run 标签下,找到合约的 walletBalance 接口,
点击调用即可显示当前合约的余额,注意单位!

多签从合约内转出金额

回到 Remix,在右边栏的 Run 标签下,先确认 Account 选择的是合约的 owner,
这个例子里就是第一个测试地址了。

在合约中找到 transferTo,下拉打开,输入一个本地测试网络中的以太钱包地址,输入待转账的金额,点击 transact。这里注意单位,下图为转 5 ETH,

成功执行后,就会有一个等待多签的 transaction 在 pendingTransactions 这个数组里。
点击 getPendingTransactions 可以得到当前等待多签的 transaction 在数组中的索引值。
下图则代表当前等待多签的 transaction 的索引值是 0 。

这时,在 Run 标签页的顶部,把 Account 依次切换为其他几个管理员,分别执行合约的 signTransaction 接口即可完成多签,其中 transactionId 就是上一步中的索引值,如下图

目前这个合约代码中,是当签名管理员的数量大于等于3个,就正式完成多签并发送交易。
所以当第三个管理执行完 signTransaction 后,查一下合约的余额就发现已经转出去 5ETH了。

总结

目前这个例子中的合约代码还是比较简单的,没有各个用户的权重关系。这个就可以根据自己的需要来写合约了。

Over!

这是这个系列的第二篇,主要讲一下各种设备接入配置。先放一张目前我已经配置好的管理界面图片

再放一张配置好的 HomeKit 截图

开始这篇之前,首先你要确保你已经安装了上一篇中提到的 SSH & Web Terminal 插件。

如何修改配置文件

如果已经安装好了 SSH & Web Terminal 插件并启动了,那么直接在 Terminal 中执行下面的命令就可以连接到树莓派上了。

1
ssh hassio@hassio.local

连接后 ,用 ls 查看目录

各个目录的作用如下:

  • addons 存储自定义插件
  • backup 备份目录
  • config 配置文件目录已经自定义组件
  • share 共享目录
  • ssl 证书目录

我们配置的时候,主要就是修改 config 目录下的 configuration.yaml 文件。每次修改保存后,要先在后台的“配置”=>“通用”中“检查配置”,配置检查通过后,再重载配置或者重启服务(有些配置的修改需要重启服务才能生效)。

接入小米多功能网关

打开 config/configuration.yaml,加入下面的配置(根据你的情况选择是多网关还是单网关配置):

1
2
3
4
5
6
7
8
9
10
11
12
# 使用单网关前提下,可不填 mac, mac 不带冒号
xiaomi_aqara:
gateways:
- mac:
key: xxxxxxxxxxxxxxxx
# 多个网关必须填入 mac,mac 不带冒号
xiaomi_aqara:
gateways:
- mac: xxxxxxxxxxxx
key: xxxxxxxxxxxxxxxx
- mac: xxxxxxxxxxxx
key: xxxxxxxxxxxxxxxx

其中 key 的获取,需要打开米家App,打开网关页面,点击右上角的三个点,打开菜单后,选择“关于”,之后“玩法教程”下面的空白处点击5次,就会出现新的列表选项,点击“局域网通信协议”,里面的密码就是 key 了,另外要把最上面的那个“局域网通信协议”的开关打开。

保存重启,接入网关成功后,通过网关接入的各种 ZigBee 设备都会被 HomeAssistant 识别出来。
目前的支持情况参见这里:https://home-assistant.cc/component/xiaomi/zigbee/

接入小米Wifi设备

插座类的配置如下:

1
2
3
4
5
switch:
- platform: xiaomi_miio
name: Original Xiaomi Mi Smart WiFi Socket
host: 192.168.130.59
token: YOUR_TOKEN

其中 name 可以自定义,host 是插座的IP地址,token则是通信用的凭证。

小米Wifi设备由于各个生产厂商的原因,并不像智能网关那么人性给提供设备的 token。获取 wifi 设备的 token 需要折腾一下。网上给出了几种方案,我使用的是用 Root 过的安卓手机。在手机上安装 5.0.19 版的米家(这是最后一个版本支持通过本地sqlite数据库查询 token 的版本),登陆米家,然后手机上打开文件管理器(比如ES文件管理器),把 /data/data/com.xiaomi.smarthome/databases/miio2.db 复制到电脑可访问的目录(比如SD卡),然后通过USB 把文件从手机转移到电脑上,用 sqlite 软件查询 devicerecord 表,其中有个 token 字段,那个就是了。

其他的一些Wifi设备参考这里:https://home-assistant.cc/component/xiaomi/wifi/

接入苹果HomeKit

配置这个的好处是,可以使用 Siri 来控制你的家庭智能场景和设备。

增加如下配置:

1
homekit:

保存重启服务后,会在主面板看到一个PIN码,这时候打开iPhone,找到 Home ,“添加配件”,“没有代码或无法扫描”,“输入代码”,把刚才的PIN码输入进去即可。

其他配置可参考这里:https://home-assistant.cc/homekit/builtin/

接入路由器

我的是网件路由刷的华硕的固件,因此找到华硕的配置如下:

1
2
3
4
5
6
7
8
asuswrt:
host: 路由器IP
username: 路由器账号
password: 路由器密码
protocol: 协议 ssh 或者 telnet
port: 端口,默认 22,telnet 模式为 23
mode: 模式,默认 router,可选 ap
ssh_key: ssh 秘钥路径,可选

其他路由参考:https://home-assistant.cc/component/router/

自定义组件

有时候,有些设备可能自带的组件并不支持,那么需要去搜索安装下第三方组件,比如小米直流电风扇,就需要去下载 https://github.com/syssi/xiaomi_fan 到 config 目录下面,

然后按照第三方插件给的说明增加配置即可。

位置和天气设置

查询下经纬度,可以把经纬度设置好,之后就能显示当地的日落日出时间了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
homeassistant:
# 名称,可为中文
name: 家
# 纬度
latitude: !secret latitude
# 经度
longitude: !secret longitude
# 海拔
elevation: 11
# 度量单位,这里选择“米”
unit_system: metric
# 时区
time_zone: Asia/Shanghai
# 设备个性化
customize:

我使用的是yahoo天气的数据,配置如下:

1
2
3
4
weather:
- platform: ywether
woeid: 2168257
name: Zibo

其中 woeid 需要自己去 https://www.yahoo.com/news/weather/ 查询城市天气,URL中的id即为 woeid。

个性化设置

个性化设置在 “配置” -> “自定义” 里面,在这里可以修改每个接入设备的名字等参数,并且可以设置是否隐藏(这个还是挺有用的,尤其是接入了路由器的情况下,会有很多的 device_tracker 类的设备,都显示在面板上显然是很乱的)。

一些可以参考的参数:https://home-assistant.cc/system/customize/

智能语言

配置这个的目的是为了能通过语音合成技术进行结果输出(播报)。

直接参考这里即可: https://home-assistant.cc/component/nlp/

结语

基本的配置就这些,目前家里所有的智能设备都加入到了 Home Assistant 的管理中了,之后就是配置各种智能逻辑了,等我试验过后,会在下一篇写一下。

Over!

0%