~eliasnaur/giobtc

76f3507ff7ef2a23c6a2822189f1b73cf6b59905 — Elias Naur 1 year, 1 month ago
initial commit

Signed-off-by: Elias Naur <mail@eliasnaur.com>
8 files changed, 721 insertions(+), 0 deletions(-)

A LICENSE
A btcd.sh
A go.mod
A go.sum
A golab-2019.slide
A golab-workshop-2019.slide
A main.go
A wallet.go
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()       {}