A => LICENSE +51 -0
@@ 1,51 @@
+SPDX-License-Identifier: Unlicense OR MIT
+
+
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
+
+
+
+The MIT License (MIT)
+
+Copyright (c) 2019 The Gio authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
A => btcd.sh +3 -0
@@ 1,3 @@
+#!/bin/sh
+
+btcd --testnet --rpcuser="Mk1Xfdws+n0OI6fARguxpLtZt48=" --rpcpass="zZT4QgeqwADcEKcrkOJr/2x7760=" --rpclisten 0.0.0.0
A => go.mod +11 -0
@@ 1,11 @@
+module eliasnaur.com/giobtc
+
+go 1.14
+
+require (
+ gioui.org v0.0.0-20191020230431-dafb18017651
+ github.com/btcsuite/btcd v0.0.0-20191011231409-07282a6656b8
+ github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
+ github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9
+ golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
+)
A => go.sum +64 -0
@@ 1,64 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+gioui.org v0.0.0-20191020230431-dafb18017651 h1:+0jQrLAVAgr0Pg3HRhDXnSSxW+tE0h4eDMLiLXfUQzo=
+gioui.org v0.0.0-20191020230431-dafb18017651/go.mod h1:KqFFi2Dq5gYA3FJ0sDOt8OBXoMsuxMtE8v2f0JExXAY=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
+github.com/btcsuite/btcd v0.0.0-20191011231409-07282a6656b8 h1:VtBbiBsPRCedNPZELoycBnfoMcpSPnc5ktPh+ORniJw=
+github.com/btcsuite/btcd v0.0.0-20191011231409-07282a6656b8/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
+github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
+github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
+github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
+github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
+github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
+github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
+github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
+github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
+github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495 h1:6IyqGr3fnd0tM3YxipK27TUskaOVUjU2nG45yzwcQKY=
+github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
+github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
+github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
+golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
+golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
A => golab-2019.slide +45 -0
@@ 1,45 @@
+Gio: Portable, immediate mode GUI
+GoLab 2019
+
+Elias Naur
+mail@eliasnaur.com
+@elias_naur
+https://gioui.org
+https://scatter.im
+https://eliasnaur.com/giobtc
+
+
+* Gio
+
+[[https://gioui.org][https://gioui.org]]
+
+Features
+
+- Immediate mode design.
+- Only depends on lowest-level platform libraries.
+- GPU accelerated vector and text rendering.
+- No garbage generated in drawing or layout code.
+- Cross platform (macOS, Linux, Windows, Android, iOS, tvOS, Webassembly).
+- Core is 100% Go. OS-specific native interfaces are optional.
+
+
+* Scatter
+
+
+* Scatter
+
+- [[https://scatter.im][https://scatter.im]]
+
+In app stores
+
+- [[https://play.google.com/apps/testing/im.scatter.app][Android Play Store Beta]]
+- [[https://testflight.apple.com/join/jsGgyJvC][iOS TestFlight]]
+
+
+* Workshop
+
+Gio workshop tomorrow (Tuesday) 11.30-13.30 in Workshop #2.
+
+Installation
+
+[[gioui.org][https://gioui.org]]. Look for "Installation".
A => golab-workshop-2019.slide +27 -0
@@ 1,27 @@
+Gio: Portable, immediate mode GUI
+GoLab 2019 Workshop
+
+Elias Naur
+mail@eliasnaur.com
+@elias_naur
+https://gioui.org
+https://scatter.im
+https://eliasnaur.com/giobtc
+
+
+* Gio
+
+Installation
+
+[[gioui.org][https://gioui.org]]. Look for "Installation".
+
+$ mkdir project
+$ cd project
+$ go mod init example.com
+$ go run gioui.org/example/kitchen
+
+Download kitchen example
+
+$ git clone git@git.sr.ht:~eliasnaur/gio
+$ cd gio/example/kitchen
+$ go run .
A => main.go +302 -0
@@ 1,302 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "log"
+ "strings"
+ "time"
+
+ "gioui.org/app"
+ "gioui.org/f32"
+ "gioui.org/io/system"
+ "gioui.org/layout"
+ "gioui.org/op"
+ "gioui.org/op/paint"
+ "gioui.org/text"
+ "gioui.org/unit"
+ "gioui.org/widget"
+ "gioui.org/widget/material"
+
+ "github.com/skip2/go-qrcode"
+
+ _ "gioui.org/font/gofont"
+)
+
+type fill color.RGBA
+
+var (
+ theme *material.Theme
+)
+
+func init() {
+ theme = material.NewTheme()
+ theme.Color.Primary = rgb(0x1b5e20)
+ //theme.TextSize = unit.Sp(20)
+}
+
+type App struct {
+ wallet *Wallet
+ trans []Transaction
+ transList *layout.List
+ qrBtn widget.Button
+ showQR bool
+ addrQR paint.ImageOp
+}
+
+func main() {
+ flag.Parse()
+ go func() {
+ runUI()
+ }()
+ app.Main()
+}
+
+func NewApp() *App {
+ wallet, err := NewWallet(*pubAddr, *host)
+ if err != nil {
+ log.Fatal(err)
+ }
+ qr, err := qrcode.New(strings.ToUpper(*pubAddr), qrcode.Medium)
+ if err != nil {
+ log.Fatal(err)
+ }
+ qr.BackgroundColor = rgb(0xe8f5e9)
+ return &App{
+ transList: &layout.List{
+ Axis: layout.Vertical,
+ },
+ addrQR: paint.NewImageOp(qr.Image(256)),
+ wallet: wallet,
+ }
+}
+
+func runUI() {
+ a := NewApp()
+ w := app.NewWindow()
+ gtx := &layout.Context{
+ Queue: w.Queue(),
+ }
+ for {
+ select {
+ case e := <-a.wallet.events:
+ switch e := e.(type) {
+ case TransactionEvent:
+ a.trans = append(a.trans, e.Trans)
+ w.Invalidate()
+ }
+ case e := <-w.Events():
+ switch e := e.(type) {
+ case system.DestroyEvent:
+ if err := e.Err; err != nil {
+ log.Fatal(err)
+ }
+ return
+ case system.FrameEvent:
+ gtx.Reset(e.Config, e.Size)
+ a.Layout(gtx)
+ e.Frame(gtx.Ops)
+ }
+ }
+ }
+}
+
+func (a *App) Layout(gtx *layout.Context) {
+ layout.Format(gtx, "stack(southeast, r(max(_)), r(inset(16dp, _)))",
+ func() {
+ a.layoutMain(gtx)
+ },
+ func() {
+ for a.qrBtn.Clicked(gtx) {
+ a.showQR = !a.showQR
+ }
+ theme.IconButton(qrIcn).Layout(gtx, &a.qrBtn)
+ },
+ )
+ if a.showQR {
+ layout.Format(gtx, "center(inset(16dp, _))",
+ func() {
+ Corners(unit.Dp(10)).Layout(gtx, func() {
+ layout.Format(gtx, "stack(center, r(_), e(min(inset(16dp, south(_)))))",
+ func() {
+ sz := gtx.Constraints.Width.Constrain(gtx.Px(unit.Dp(500)))
+ a.addrQR.Add(gtx.Ops)
+ paint.PaintOp{
+ Rect: f32.Rectangle{
+ Max: f32.Point{
+ X: float32(sz), Y: float32(sz),
+ },
+ },
+ }.Add(gtx.Ops)
+ gtx.Dimensions.Size = image.Point{X: sz, Y: sz}
+ },
+ func() {
+ theme.Body1(*pubAddr).Layout(gtx)
+ },
+ )
+ })
+ },
+ )
+ }
+}
+
+func (a *App) layoutMain(gtx *layout.Context) {
+ const f = `
+ vflex(
+ r(vcap(200dp, vmax(
+ stack(center,
+ e(max(_))
+ r(hflex(baseline, r(_), r(_)))
+ )
+ ))),
+ r(_)
+ )
+ `
+ layout.Format(gtx, f,
+ func() {
+ fill(rgb(0x1b5e20)).Layout(gtx)
+ },
+ func() {
+ bal := fmt.Sprintf("%d", a.wallet.Balance())
+ amt := theme.H2(bal)
+ amt.Color = rgb(0xffffff)
+ if len(a.trans) > 0 {
+ last := a.trans[len(a.trans)-1]
+ const duration = .2
+ if dt := time.Now().Sub(last.added).Seconds(); dt < duration {
+ dt /= duration
+ dt = dt * dt * dt
+ const scale = 0.1
+ amt.Font.Size.V *= float32(1 + scale - dt*scale)
+ op.InvalidateOp{}.Add(gtx.Ops)
+ }
+ }
+ amt.Layout(gtx)
+ },
+ func() {
+ sat := theme.H6(" sat")
+ sat.Color = rgb(0xffffff)
+ sat.Layout(gtx)
+ },
+ func() {
+ a.layoutTrans(gtx)
+ },
+ )
+}
+
+func (a *App) layoutTrans(gtx *layout.Context) {
+ now := time.Now()
+ a.transList.Layout(gtx, len(a.trans), func(i int) {
+ // Invert list
+ i = len(a.trans) - 1 - i
+ t := a.trans[i]
+ a := 1.0
+ const duration = 0.5
+ if dt := now.Sub(t.added).Seconds(); dt < duration {
+ op.InvalidateOp{}.Add(gtx.Ops)
+ a = dt / duration
+ a *= a
+ }
+ const f = `
+ inset(16dp,
+ vflex(
+ r(inset(0dp0dp4dp0dp, hflex(baseline,
+ r(_),
+ r(hmax(east(hflex(baseline, r(_), r(_)))))
+ ))),
+ r(_)
+ )
+ )`
+ layout.Format(gtx, f,
+ func() {
+ tim := theme.Body1(t.FormatTime())
+ tim.Color = alpha(a, tim.Color)
+ tim.Layout(gtx)
+ },
+ func() {
+ amount := theme.H5(t.FormatAmount())
+ amount.Color = rgb(0x003300)
+ amount.Color = alpha(a, amount.Color)
+ amount.Alignment = text.End
+ amount.Font.Variant = "Mono"
+ amount.Font.Weight = text.Bold
+ amount.Layout(gtx)
+ },
+ func() {
+ sat := theme.Body1(" sat")
+ sat.Color = alpha(a, sat.Color)
+ sat.Layout(gtx)
+ },
+ func() {
+ l := theme.Body2(t.Hash)
+ l.Color = theme.Color.Hint
+ l.Color = alpha(a, l.Color)
+ l.Layout(gtx)
+ },
+ )
+ })
+}
+
+func alpha(a float64, col color.RGBA) color.RGBA {
+ col.A = byte(float64(col.A) * a)
+ col.R = byte(float64(col.R) * a)
+ col.G = byte(float64(col.G) * a)
+ col.B = byte(float64(col.B) * a)
+ return col
+}
+
+type Corners unit.Value
+
+func (c Corners) Layout(gtx *layout.Context, w layout.Widget) {
+ var macro op.MacroOp
+ macro.Record(gtx.Ops)
+ w()
+ macro.Stop()
+ sz := gtx.Dimensions.Size
+ rr := float32(gtx.Px(unit.Value(c)))
+ var stack op.StackOp
+ stack.Push(gtx.Ops)
+ rrect(gtx.Ops, float32(sz.X), float32(sz.Y), rr, rr, rr, rr)
+ macro.Add(gtx.Ops)
+ stack.Pop()
+}
+
+func rgb(c uint32) color.RGBA {
+ return argb((0xff << 24) | c)
+}
+
+func argb(c uint32) color.RGBA {
+ return color.RGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)}
+}
+
+func (f fill) Layout(gtx *layout.Context) {
+ cs := gtx.Constraints
+ d := image.Point{X: cs.Width.Min, Y: cs.Height.Min}
+ dr := f32.Rectangle{
+ Max: f32.Point{X: float32(d.X), Y: float32(d.Y)},
+ }
+ paint.ColorOp{Color: color.RGBA(f)}.Add(gtx.Ops)
+ paint.PaintOp{Rect: dr}.Add(gtx.Ops)
+ gtx.Dimensions = layout.Dimensions{Size: d}
+}
+
+// https://pomax.github.io/bezierinfo/#circles_cubic.
+func rrect(ops *op.Ops, width, height, se, sw, nw, ne float32) {
+ w, h := float32(width), float32(height)
+ const c = 0.55228475 // 4*(sqrt(2)-1)/3
+ var b paint.Path
+ b.Begin(ops)
+ b.Move(f32.Point{X: w, Y: h - se})
+ b.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE
+ b.Line(f32.Point{X: sw - w + se, Y: 0})
+ b.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW
+ b.Line(f32.Point{X: 0, Y: nw - h + sw})
+ b.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW
+ b.Line(f32.Point{X: w - ne - nw, Y: 0})
+ b.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE
+ b.End().Add(ops)
+}
A => wallet.go +218 -0
@@ 1,218 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+package main
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "log"
+ "sync/atomic"
+ "time"
+
+ "gioui.org/widget/material"
+ "github.com/btcsuite/btcd/btcjson"
+ "github.com/btcsuite/btcd/chaincfg"
+ "github.com/btcsuite/btcd/chaincfg/chainhash"
+ "github.com/btcsuite/btcd/rpcclient"
+ "github.com/btcsuite/btcd/txscript"
+ "github.com/btcsuite/btcutil"
+ "golang.org/x/exp/shiny/materialdesign/icons"
+)
+
+type Wallet struct {
+ addr btcutil.Address
+ disc chan struct{}
+ done chan struct{}
+ events chan Event
+ balance btcutil.Amount
+}
+
+type Transaction struct {
+ New bool
+ Hash string
+ amount btcutil.Amount
+ time time.Time
+ added time.Time
+}
+
+type Event interface {
+ isWalletEvent()
+}
+
+type TransactionEvent struct {
+ Trans Transaction
+}
+
+type ErrorEvent struct {
+ Err error
+}
+
+var qrIcn *material.Icon
+
+func init() {
+ icn, err := material.NewIcon(icons.ContentAdd)
+ if err != nil {
+ log.Fatal(err)
+ }
+ qrIcn = icn
+}
+
+var (
+ pubAddr = flag.String("addr", "tb1qy9cem33xwpttzdh7a3nsmqsyz8ytz2jz28w860", "bitcoin address")
+ host = flag.String("host", "localhost:18334", "btcd host")
+)
+
+const startHeight = 1582962
+
+var bitcoinNet = &chaincfg.TestNet3Params
+
+const (
+ rpcUser = "Mk1Xfdws+n0OI6fARguxpLtZt48="
+ rpcPass = "zZT4QgeqwADcEKcrkOJr/2x7760="
+ rpcCert = `-----BEGIN CERTIFICATE-----
+MIICmTCCAfugAwIBAgIRAJwxVLc1YHY3A35LaTAsS3MwCgYIKoZIzj0EAwQwNDEg
+MB4GA1UEChMXYnRjZCBhdXRvZ2VuZXJhdGVkIGNlcnQxEDAOBgNVBAMTB3Rlc3Rt
+YWMwHhcNMTkxMDE2MjAyMzU0WhcNMjkxMDE0MjAyMzU0WjA0MSAwHgYDVQQKExdi
+dGNkIGF1dG9nZW5lcmF0ZWQgY2VydDEQMA4GA1UEAxMHdGVzdG1hYzCBmzAQBgcq
+hkjOPQIBBgUrgQQAIwOBhgAEASBx1IWFOr6/y82v6nJHYB7tGHjHk7WpbEHxqxi2
+raovxw4aM2d/gncuPNinMInP6JbRdvV30CYZ5/GrimZjuNRlARihFHbYQ6lkYLAy
+wncw4Y7rFOWbmbj9YsFOgnUkhuTkBm3r56UHfeqaO7LbG+zVZo2/mBewkfkhFyEi
+SXmpQPXmo4GqMIGnMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MIGD
+BgNVHREEfDB6ggd0ZXN0bWFjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAA
+AAAAAAGHEP6AAAAAAAAAAAAAAAAAAAGHEP6AAAAAAAAABMMVBQWbMICHBMCoVraH
+EP6AAAAAAAAAhHcl//6k7zSHEP6AAAAAAAAA27SKa9kr6WkwCgYIKoZIzj0EAwQD
+gYsAMIGHAkIBrl769uPROZS+KHnBS40kIIdGgwHcA88I8jv4udTphdGRrCOKNFsB
+FAu7fv/4YmTAafgmUX0s66Jmg81tKGQRnbUCQQxvDfJYIdPG4nO3piAoihZY7g6U
++zUCmQjcegjk0jY0UpBN3ire+iNEkFMLm2CDtTGTgHZJH7DinyGMgqXb0Kc5
+-----END CERTIFICATE-----`
+)
+
+func NewWallet(pubaddr string, host string) (*Wallet, error) {
+ addr, err := btcutil.DecodeAddress(pubaddr, bitcoinNet)
+ if err != nil {
+ return nil, err
+ }
+ w := &Wallet{
+ addr: addr,
+ }
+ ccfg := &rpcclient.ConnConfig{
+ Host: host,
+ Endpoint: "ws",
+ User: rpcUser,
+ Pass: rpcPass,
+ Certificates: []byte(rpcCert),
+ }
+ w.Connect(ccfg)
+ return w, nil
+}
+
+func (w *Wallet) Balance() btcutil.Amount {
+ return btcutil.Amount(atomic.LoadInt64((*int64)(&w.balance)))
+}
+
+func (w *Wallet) Connect(cfg *rpcclient.ConnConfig) {
+ w.disc = make(chan struct{})
+ w.done = make(chan struct{})
+ w.events = make(chan Event)
+ go w.run(cfg)
+}
+
+func (w *Wallet) run(cfg *rpcclient.ConnConfig) {
+ var cl *rpcclient.Client
+ onerr := func(err error) {
+ if w.disc == nil {
+ return
+ }
+ log.Printf("wallet error: %v", err)
+ w.events <- ErrorEvent{err}
+ }
+ rescanned := false
+ connected := make(chan struct{})
+ handlers := &rpcclient.NotificationHandlers{
+ OnClientConnected: func() {
+ connected <- struct{}{}
+ },
+ OnRescanFinished: func(hash *chainhash.Hash, height int32, blkTime time.Time) {
+ rescanned = true
+ },
+ OnRecvTx: func(trans *btcutil.Tx, details *btcjson.BlockDetails) {
+ for _, out := range trans.MsgTx().TxOut {
+ // Extract and print details from the script.
+ _, addresses, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, bitcoinNet)
+ if err != nil {
+ onerr(err)
+ return
+ }
+ if len(addresses) != 1 {
+ continue
+ }
+ addr := addresses[0]
+ if !bytes.Equal(addr.ScriptAddress(), w.addr.ScriptAddress()) {
+ continue
+ }
+ atomic.AddInt64((*int64)(&w.balance), out.Value)
+ var t time.Time
+ if details != nil {
+ t = time.Unix(details.Time, 0)
+ } else {
+ t = time.Now()
+ }
+ w.events <- TransactionEvent{
+ Trans: Transaction{
+ New: rescanned,
+ Hash: trans.Hash().String(),
+ amount: btcutil.Amount(out.Value),
+ time: t,
+ added: time.Now(),
+ },
+ }
+ }
+ },
+ }
+ var err error
+ cl, err = rpcclient.New(cfg, handlers)
+ if err != nil {
+ onerr(err)
+ }
+ go func() {
+ <-connected
+ block, err := cl.GetBlockHash(startHeight)
+ if err != nil {
+ onerr(err)
+ return
+ }
+ addrs := []btcutil.Address{w.addr}
+ if err := cl.NotifyReceived(addrs); err != nil {
+ log.Fatal(err)
+ }
+ if err := cl.Rescan(block, addrs, nil); err != nil {
+ onerr(err)
+ return
+ }
+ }()
+ for {
+ select {
+ case <-w.disc:
+ cl.Shutdown()
+ w.done <- struct{}{}
+ return
+ }
+ }
+}
+
+func (w *Wallet) disconnect() {
+ w.disc <- struct{}{}
+ <-w.done
+}
+
+func (t *Transaction) FormatAmount() string {
+ return fmt.Sprintf("%d", t.amount)
+ //return fmt.Sprintf("%d", t.amount)
+}
+
+func (t *Transaction) FormatTime() string {
+ return t.time.Local().Format("2006-01-02 15:04:05")
+}
+
+func (e TransactionEvent) isWalletEvent() {}
+func (e ErrorEvent) isWalletEvent() {}