diff --git a/.gitignore b/.gitignore index 2582989..1758efa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.db datashards* mainnet* -testnet* \ No newline at end of file +testnet* +sandbox/ \ No newline at end of file diff --git a/functions.go b/functions.go index e587233..8173f6b 100644 --- a/functions.go +++ b/functions.go @@ -728,6 +728,7 @@ func closeWallet() { session.LastBalance = 0 engram.Disk = nil tx = Transfers{} + histCache.clear() if gnomon.Index != nil { logger.Printf("[Gnomon] Shutting down indexers...\n") @@ -993,6 +994,8 @@ func login() { shard := fmt.Sprintf("%x", sha1.Sum([]byte(address))) session.ID = shard session.LimitMessages = true + + go warmHistoryCache() } // Remove all overlays diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2ac39db --- /dev/null +++ b/go.sum @@ -0,0 +1,418 @@ +cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +fyne.io/fyne/v2 v2.6.2 h1:RPgwmXWn+EuP/TKwO7w5p73ILVC26qHD9j3CZUZNwgM= +fyne.io/fyne/v2 v2.6.2/go.mod h1:9IJ8uWgzfcMossFoUkLiOrUIEtaDvF4nML114WiCtXU= +fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= +fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb h1:2BazNmb/kwgqRdvE9L+NgW8sfoWGn3iy1Ox8R4+CSmc= +fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb/go.mod h1:u3LF1EkElytjOT8OHxft16trctGndF9qpsoH6YIDOUU= +github.com/Andrew-M-C/go.jsonvalue v1.4.1 h1:MGtqaKy5Z7Xi/hnWaeqWDMwno79uNRt0XoCGHa1pm74= +github.com/Andrew-M-C/go.jsonvalue v1.4.1/go.mod h1:EsYbZ97LlOhGUs+7qTwZI9KaJrPe6nK8sEZKEqr70Ww= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/VictoriaMetrics/metrics v1.23.1 h1:/j8DzeJBxSpL2qSIdqnRFLvQQhbJyJbbEi22yMm7oL0= +github.com/VictoriaMetrics/metrics v1.23.1/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc= +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= +github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= +github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= +github.com/cenkalti/hub v1.0.2 h1:Nqv9TNaA9boeO2wQFW8o87BY3zKthtnzXmWGmJqhAV8= +github.com/cenkalti/hub v1.0.2/go.mod h1:8LAFAZcCasb83vfxatMUnZHRoQcffho2ELpHb+kaTJU= +github.com/cenkalti/rpc2 v1.0.4 h1:MJWmm7mbt8r/ZkQS+qr/e2KMMrhMLPr/62CYZIHybdI= +github.com/cenkalti/rpc2 v1.0.4/go.mod h1:2yfU5b86vOr16+iY1jN3MvT6Kxc9Nf8j5iZWwUf7iaw= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/civilware/Gnomon v0.0.0-20240403103529-8b2fdb2b3106 h1:9NSEj0KUC/Xlaw7uk/CIswEevheDNojrG6JoMPXv3G8= +github.com/civilware/Gnomon v0.0.0-20240403103529-8b2fdb2b3106/go.mod h1:B0/D3D/FVqqugHZ0fO0da2AW+5MiO82uGLOZcmS0CFk= +github.com/civilware/epoch v0.0.0-20241002060739-1ed2fc6f74cb h1:ZM5hGZMiUvheyVjzJYgvY/A34NeDZ2WO8x7oMQA6Oyc= +github.com/civilware/epoch v0.0.0-20241002060739-1ed2fc6f74cb/go.mod h1:aATobu9h/6BK9C+1e/DRBi6+dUNrHFqmykhhrxQvbTQ= +github.com/civilware/tela v0.0.0-20250806221602-aa892d2ff8d4 h1:Coa5YQoaFcOd4fBEtUhbj3Kth3TX+9s5okjfulEis58= +github.com/civilware/tela v0.0.0-20250806221602-aa892d2ff8d4/go.mod h1:J+maI4zPRe7zpSpTyHDopUg3eQRqCKffvBIRnOhipB8= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creachadair/jrpc2 v0.35.4 h1:5ELLV7CMKLfALzkKNsQ//ngZLWDbEmAXgTgkL3JXAcU= +github.com/creachadair/jrpc2 v0.35.4/go.mod h1:a53Cer/NMD1y8P9UB2XbuOLRELKRLDf8u7bRi4v1qsE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deroproject/derohe v0.0.0-20250813215012-9b6a8b82c839 h1:RWh3dbb/Z29Non+otA9su7AfYpHPpadHzY6v5JNforg= +github.com/deroproject/derohe v0.0.0-20250813215012-9b6a8b82c839/go.mod h1:EWHh1VkXRnCHvyGML98kXhngDFYebmOhk/9kZ1ATJ1c= +github.com/deroproject/graviton v0.0.0-20220130070622-2c248a53b2e1 h1:nsiNx83HYmRmYpYO37pUzSTmB7p9PFtGBl4FyD+a0jg= +github.com/deroproject/graviton v0.0.0-20220130070622-2c248a53b2e1/go.mod h1:a4u6QJtGGIADg1JwujD77UtaAyhIxg14+I0C7xjyQcc= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= +github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= +github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= +github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= +github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw= +github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= +github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jackmordaunt/icns/v2 v2.2.6 h1:M7kg6pWRmB+SyCvM058cV2BlAz3MedOHy4e3j2i7FQg= +github.com/jackmordaunt/icns/v2 v2.2.6/go.mod h1:DqlVnR5iafSphrId7aSD06r3jg0KRC9V6lEBBp504ZQ= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0= +github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.14/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/reedsolomon v1.10.0/go.mod h1:qHMIzMkuZUWqIh8mS/GruPdo3u0qwX2jk/LH440ON7Y= +github.com/klauspost/reedsolomon v1.12.5 h1:4cJuyH926If33BeDgiZpI5OU0pE+wUHZvMSyNGqN73Y= +github.com/klauspost/reedsolomon v1.12.5/go.mod h1:LkXRjLYGM8K/iQfujYnaPeDmhZLqkrGUyG9p7zs5L68= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lesismal/llib v1.2.2 h1:ZoVgP9J58Ju3Yue5jtj8ybWl+BKqoVmdRaN1mNwG5Gc= +github.com/lesismal/llib v1.2.2/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= +github.com/lesismal/nbio v1.6.7 h1:EeiH0Vn0v5NG7masYNWugibPdNZZYYBPa0pGj4GOrbg= +github.com/lesismal/nbio v1.6.7/go.mod h1:mBn1rSIZ+cmOILhvP+/1Mb/JimgA+1LQudlHJUb/aNA= +github.com/lucor/goinfo v0.9.0 h1:EdsMzmY5TZujA4xb9xMLIdlp2+zvF7miNYkVXvqqgOQ= +github.com/lucor/goinfo v0.9.0/go.mod h1:L6m6tN5Rlova5Z83h1ZaKsMP1iiaoZ9vGTNzu5QKOD4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEdvY3iDK6jfWXvEaM5OCKkjxPKoJRdB3Gg= +github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= +github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA= +github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= +github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/cpu v0.0.9 h1:cGGLK8twbc1J1S/fHnZW7BylXYaFP+0fR2s+nzsFDiU= +github.com/templexxx/cpu v0.0.9/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= +github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/twpayne/go-geom v1.0.0 h1:ARrRnN4+rBX3LZFZQy9NFeXXlgQqVL4OOvAcnLdgy2g= +github.com/twpayne/go-geom v1.0.0/go.mod h1:RWsl+e3XSahOul/KH2BHCfF0QxSL4RMnMlFw/TNmET0= +github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= +github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= +github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= +github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= +github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= +github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= +github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xtaci/kcp-go/v5 v5.6.2 h1:pSXMa5MOsb+EIZKe4sDBqlTExu2A/2Z+DFhoX2qtt2A= +github.com/xtaci/kcp-go/v5 v5.6.2/go.mod h1:LsinWoru+lWWJHb+EM9HeuqYxV6bb9rNcK12v67jYzQ= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +github.com/ybbus/jsonrpc v2.1.2+incompatible h1:V4mkE9qhbDQ92/MLMIhlhMSbz8jNXdagC3xBR5NDwaQ= +github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= +golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= +golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= +mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/history_cache.go b/history_cache.go new file mode 100644 index 0000000..4c4b750 --- /dev/null +++ b/history_cache.go @@ -0,0 +1,80 @@ +package main + +import ( + "sync" + "time" + + "github.com/deroproject/derohe/cryptography/crypto" +) + +// historyCache holds the formatted history rows (newest first) for the wallet +// that is currently open, keyed per view ("Normal"/"Coinbase"/"Messages"). It +// is a paint accelerator only: every history visit still runs a full fetch and +// replaces the cached rows, so chain reorgs self-heal on the next refresh. +// RAM only; dropped on wallet close. Row slices are treated as immutable by +// all callers — never mutate a slice handed to put or returned by get. +type historyCache struct { + sync.Mutex + addr string // wallet address the cached views belong to + views map[string][]string // view name -> rows +} + +var histCache historyCache + +// get returns the cached rows for addr's view. A different address is always +// a miss, so one wallet can never paint another wallet's history. +func (c *historyCache) get(addr, view string) ([]string, bool) { + c.Lock() + defer c.Unlock() + if addr == "" || addr != c.addr { + return nil, false + } + rows, ok := c.views[view] + return rows, ok +} + +// put stores rows for addr's view. A new address evicts every view of the +// previous wallet. nil rows are a valid entry: an empty history still counts +// as a hit, so empty wallets paint instantly too. +func (c *historyCache) put(addr, view string, rows []string) { + if addr == "" { + return + } + c.Lock() + defer c.Unlock() + if addr != c.addr || c.views == nil { + c.addr = addr + c.views = make(map[string][]string) + } + c.views[view] = rows +} + +// clear drops all cached rows (privacy + memory on wallet close). +func (c *historyCache) clear() { + c.Lock() + defer c.Unlock() + c.addr = "" + c.views = nil +} + +// warmHistoryCache pre-builds the Normal-view rows shortly after login so the +// first History visit paints from cache. It runs one full Show_Transfers scan +// (which takes the wallet's exclusive lock), deferred a few seconds to stay +// clear of the login-time sync/save burst. Only the default view is warmed; +// Coinbase/Messages fill on their first visit. The "Normal" literal must match +// the label layoutHistory passes to load (see menu.OnChanged in layouts.go). +func warmHistoryCache() { + time.Sleep(3 * time.Second) + w := engram.Disk + if w == nil { + return + } + addr := w.GetAddress().String() + var zeroscid crypto.Hash + entries := w.Show_Transfers(zeroscid, false, true, true, 0, w.Get_Height(), "", "", 0, 0) + rows := streamHistoryRows(entries, historyRowNormal, 0, nil) + if engram.Disk != w { // wallet closed or switched while we scanned + return + } + histCache.put(addr, "Normal", rows) +} diff --git a/history_format.go b/history_format.go new file mode 100644 index 0000000..a1fa14a --- /dev/null +++ b/history_format.go @@ -0,0 +1,235 @@ +package main + +import ( + "image/color" + "strconv" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/widget" + + "github.com/deroproject/derohe/globals" + "github.com/deroproject/derohe/rpc" +) + +// History rows are encoded as ";;;"-delimited strings consumed by the list's +// UpdateItem callback (see newHistoryList). The functions below are pure +// (entry in, row out) so they can be unit-tested and benchmarked without a +// wallet, a daemon, or a running Fyne app. They are extracted verbatim from the +// three inline closures that used to live in layoutHistory(). + +// appendDate appends t as YYYY-MM-DD, byte-identical to +// t.AppendFormat(dst, "2006-01-02"). For the normal 4-digit-year range it uses +// direct digit math; outside it (year <1000 or >9999) it defers to AppendFormat, +// so the output is identical for any date. Gated by TestAppendDateMatchesFormat. +func appendDate(dst []byte, t time.Time) []byte { + y, m, d := t.Date() + if y < 1000 || y > 9999 { + return t.AppendFormat(dst, "2006-01-02") + } + return append(dst, + byte('0'+y/1000%10), byte('0'+y/100%10), byte('0'+y/10%10), byte('0'+y%10), + '-', byte('0'+int(m)/10), byte('0'+int(m)%10), + '-', byte('0'+d/10), byte('0'+d%10), + ) +} + +// historyStamp formats t as YYYY-MM-DD, byte-identical to t.Format("2006-01-02"). +func historyStamp(t time.Time) string { + var b [10]byte + return string(appendDate(b[:0], t)) +} + +// fastMoneyMax bounds the integer fast path of fastFormatMoney. Below it, exact +// integer arithmetic reproduces globals.FormatMoney byte-for-byte: FormatMoney's +// big.Float rounding error stays far under half a unit in the 5th decimal until +// ~9.2e18, so 1e17 leaves a huge margin. Above it we defer to FormatMoney, so the +// result is identical for every uint64. DERO's whole supply is ~2.1e12 atomic +// units, so the fast path covers all real amounts with orders of magnitude spare. +const fastMoneyMax = 100_000_000_000_000_000 // 1e17 atomic units + +// appendMoney appends globals.FormatMoney(amount) to dst without the per-call +// big.Float allocation, deferring to FormatMoney for extreme values (fastMoneyMax). +func appendMoney(dst []byte, amount uint64) []byte { + if amount > fastMoneyMax { + return append(dst, globals.FormatMoney(amount)...) + } + whole := amount / 100000 + frac := amount % 100000 + dst = strconv.AppendUint(dst, whole, 10) + return append(dst, + '.', + byte('0'+frac/10000%10), + byte('0'+frac/1000%10), + byte('0'+frac/100%10), + byte('0'+frac/10%10), + byte('0'+frac%10), + ) +} + +// fastFormatMoney is an allocation-light equivalent of globals.FormatMoney(amount) +// (atomic units -> ".<5 decimals>"). Equivalence to FormatMoney across the +// full uint64 range is enforced by the gate test TestFastFormatMoneyMatchesGlobals. +func fastFormatMoney(amount uint64) string { + var b [24]byte + return string(appendMoney(b[:0], amount)) +} + +// historyRowNormal formats a non-coinbase transfer. Returns (row, true) to show +// it, ("", false) to skip. It reads none of the fields ProcessPayload populates. +func historyRowNormal(e rpc.Entry) (string, bool) { + if e.Coinbase { + return "", false + } + // Build the whole ";;;"-delimited row in one stack buffer: a single allocation + // for the returned string instead of separate amount/height/stamp strings plus + // a final concat. Output is byte-identical (pinned by TestHistoryRowNormal). + var sb [128]byte + buf := sb[:0] + if e.Incoming { + buf = append(buf, "Received;;;"...) + buf = appendMoney(buf, e.Amount) + } else { + buf = append(buf, "Sent;;;("...) + buf = appendMoney(buf, e.Amount) + buf = append(buf, ')') + } + buf = append(buf, ";;;"...) + buf = strconv.AppendUint(buf, e.Height, 10) + buf = append(buf, ";;;"...) + buf = appendDate(buf, e.Time) + buf = append(buf, ";;;"...) + buf = append(buf, e.TXID...) + return string(buf), true +} + +// historyRowCoinbase formats a coinbase (network reward) transfer. +func historyRowCoinbase(e rpc.Entry) (string, bool) { + if !e.Coinbase { + return "", false + } + stamp := historyStamp(e.Time) + height := strconv.FormatUint(e.Height, 10) + amount := fastFormatMoney(e.Amount) + return "Network" + ";;;" + amount + ";;;" + height + ";;;" + stamp + ";;;" + e.TXID, true +} + +// clipHistoryField shortens a display field to 10 chars + ".." when it overflows, +// leaving shorter values untouched. Shared by the message row's username and comment. +func clipHistoryField(s string) string { + if len(s) > 10 { + return s[0:10] + ".." + } + return s +} + +// historyRowMessage formats an encrypted-message transfer (dst port 1337). It is +// the only view that reads Payload_RPC, so it is the only one that needs the +// decoded payload. The build loop currently decodes every entry up front +// (streamHistoryRows), so this function assumes Payload_RPC is already populated. +func historyRowMessage(e rpc.Entry) (string, bool) { + // Only the message view reads the decoded payload, so the decode lives here + // (guarded) instead of running unconditionally for every transfer in the + // build loop. Entries that arrive already-decoded (empty Payload) are reused. + if len(e.Payload) > 0 { + e.ProcessPayload() + } + if !e.Payload_RPC.HasValue(rpc.RPC_COMMENT, rpc.DataString) { + return "", false + } + stamp := historyStamp(e.Time) + direction := "Received" + if !e.Incoming { + direction = "Sent " + } + contact := "" + username := "" + if e.Payload_RPC.HasValue(rpc.RPC_NEEDS_REPLYBACK_ADDRESS, rpc.DataString) { + contact = e.Payload_RPC.Value(rpc.RPC_NEEDS_REPLYBACK_ADDRESS, rpc.DataString).(string) + username = clipHistoryField(contact) + } + comment := clipHistoryField(e.Payload_RPC.Value(rpc.RPC_COMMENT, rpc.DataString).(string)) + return direction + ";;;" + username + ";;;" + comment + ";;;" + stamp + ";;;" + e.TXID + ";;;" + contact, true +} + +// revealRowCap stops progressive snapshots once the list already holds far more +// than a screenful. Beyond it the caller's final Set delivers the remaining rows, +// so a long history avoids copying ever-larger snapshots it would never paint. +const revealRowCap = 2048 + +// streamHistoryRows builds the display rows from a transfer set, newest first, +// applying `row` as both filter and formatter. When batch > 0 it calls `reveal` +// with a snapshot of the rows built so far on a geometric cadence (up to +// revealRowCap rows), so the caller can paint partial results progressively. It +// returns the full final slice. No Fyne or UI-thread assumptions live here. +func streamHistoryRows(entries []rpc.Entry, row func(rpc.Entry) (string, bool), batch int, reveal func([]string)) []string { + rows := make([]string, 0, len(entries)) + next := batch + for i := len(entries) - 1; i >= 0; i-- { // newest first + r, ok := row(entries[i]) + if !ok { + continue + } + rows = append(rows, r) + + // Reveal partial results on a geometric cadence (batch, 2*batch, 4*batch, ...): + // the first screenful paints early; past revealRowCap we stop snapshotting so a + // long history pays neither the O(n^2) fixed-chunk copy nor huge late snapshots. + if batch > 0 && reveal != nil && len(rows) >= next && len(rows) <= revealRowCap { + snap := append([]string(nil), rows...) + reveal(snap) + next *= 2 + } + } + return rows +} + +// newHistoryList builds the bound list widget used by the history screen. The +// caller owns the backing slice (`data`); listData.Set writes through it so the +// screen's OnSelected can read data[id]. Extracted from layoutHistory() so the +// render/model path is constructable in a headless test. +func newHistoryList(data *[]string) (*widget.List, binding.StringList) { + listData := binding.BindStringList(data) + + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(ui.Width*0.3, 35)) + rectMid := canvas.NewRectangle(color.Transparent) + rectMid.SetMinSize(fyne.NewSize(ui.Width*0.35, 35)) + + listBox := widget.NewListWithData(listData, + func() fyne.CanvasObject { + return container.NewHBox( + container.NewStack( + rect, + widget.NewLabel(""), + ), + container.NewStack( + rectMid, + widget.NewLabel(""), + ), + container.NewStack( + rect, + widget.NewLabel(""), + ), + ) + }, + func(di binding.DataItem, co fyne.CanvasObject) { + dat := di.(binding.String) + str, err := dat.Get() + if err != nil { + return + } + + split := strings.Split(str, ";;;") + + co.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(split[0]) + co.(*fyne.Container).Objects[1].(*fyne.Container).Objects[1].(*widget.Label).SetText(split[1]) + co.(*fyne.Container).Objects[2].(*fyne.Container).Objects[1].(*widget.Label).SetText(split[3]) + }) + + return listBox, listData +} diff --git a/history_format_test.go b/history_format_test.go new file mode 100644 index 0000000..5f289bd --- /dev/null +++ b/history_format_test.go @@ -0,0 +1,376 @@ +package main + +import ( + "math/rand" + "slices" + "strconv" + "strings" + "sync" + "testing" + "time" + + "fyne.io/fyne/v2/test" + "github.com/deroproject/derohe/globals" + "github.com/deroproject/derohe/rpc" +) + +// Fixed instant so the "2006-01-02" stamp is deterministic. +var testTime = time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC) + +const testStamp = "2024-01-02" + +func TestHistoryRowNormal(t *testing.T) { + cases := []struct { + name string + e rpc.Entry + want string + wantOK bool + }{ + { + name: "received", + e: rpc.Entry{Coinbase: false, Incoming: true, Amount: 150000, Height: 12345, TXID: "tx_recv", Time: testTime}, + want: "Received;;;" + globals.FormatMoney(150000) + ";;;12345;;;" + testStamp + ";;;tx_recv", + wantOK: true, + }, + { + name: "sent_is_parenthesized", + e: rpc.Entry{Coinbase: false, Incoming: false, Amount: 250000, Height: 7, TXID: "tx_sent", Time: testTime}, + want: "Sent;;;(" + globals.FormatMoney(250000) + ");;;7;;;" + testStamp + ";;;tx_sent", + wantOK: true, + }, + { + name: "coinbase_skipped", + e: rpc.Entry{Coinbase: true, Incoming: true, Amount: 1, Height: 1, TXID: "cb", Time: testTime}, + want: "", + wantOK: false, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, ok := historyRowNormal(tc.e) + if ok != tc.wantOK || got != tc.want { + t.Fatalf("historyRowNormal() = %q,%v; want %q,%v", got, ok, tc.want, tc.wantOK) + } + }) + } +} + +func TestHistoryRowCoinbase(t *testing.T) { + cb := rpc.Entry{Coinbase: true, Amount: 600000, Height: 99, TXID: "cbtx", Time: testTime} + got, ok := historyRowCoinbase(cb) + want := "Network;;;" + globals.FormatMoney(600000) + ";;;99;;;" + testStamp + ";;;cbtx" + if !ok || got != want { + t.Fatalf("historyRowCoinbase() = %q,%v; want %q,true", got, ok, want) + } + if _, ok := historyRowCoinbase(rpc.Entry{Coinbase: false, Time: testTime}); ok { + t.Fatalf("non-coinbase entry should be skipped") + } +} + +// msgEntry builds a message entry with Payload_RPC populated directly (decoded), +// which is what historyRowMessage reads. Empty Payload, so it is robust to the +// optimize-loop later moving the ProcessPayload decode into historyRowMessage +// (the len(Payload)>0 guard will skip it and use this preset value). +func msgEntry(incoming bool, comment, replyback, txid string) rpc.Entry { + args := rpc.Arguments{ + {Name: rpc.RPC_COMMENT, DataType: rpc.DataString, Value: comment}, + } + if replyback != "" { + args = append(args, rpc.Argument{Name: rpc.RPC_NEEDS_REPLYBACK_ADDRESS, DataType: rpc.DataString, Value: replyback}) + } + return rpc.Entry{Incoming: incoming, TXID: txid, Time: testTime, Payload_RPC: args} +} + +func TestHistoryRowMessage(t *testing.T) { + // Received, short comment + short replyback (no truncation). + got, ok := historyRowMessage(msgEntry(true, "hi", "deroAddr", "m1")) + want := "Received;;;deroAddr;;;hi;;;" + testStamp + ";;;m1;;;deroAddr" + if !ok || got != want { + t.Fatalf("message(received) = %q,%v; want %q,true", got, ok, want) + } + + // Sent ("Sent " with 4 trailing spaces); long comment + long replyback + // both truncate username/comment to first 10 chars + "..", but contact (field 6) + // keeps the full address. + long := "0123456789ABCDEF" + trunc := long[0:10] + ".." + got2, ok2 := historyRowMessage(msgEntry(false, long, long, "m2")) + want2 := "Sent ;;;" + trunc + ";;;" + trunc + ";;;" + testStamp + ";;;m2;;;" + long + if !ok2 || got2 != want2 { + t.Fatalf("message(sent,long) = %q,%v; want %q,true", got2, ok2, want2) + } + + // No comment -> skipped. + if _, ok := historyRowMessage(rpc.Entry{Time: testTime}); ok { + t.Fatalf("message without comment should be skipped") + } +} + +// TestHistoryRowMessageDecodesPayload exercises the production path: entries from +// Get_Payments_DestinationPort arrive with a raw CBOR Payload and nil Payload_RPC, +// and historyRowMessage must decode it in-place (the optimize-loop hoist). The +// other message tests preset Payload_RPC with empty Payload, so they never hit +// this branch. +func TestHistoryRowMessageDecodesPayload(t *testing.T) { + payload, err := rpc.Arguments{ + {Name: rpc.RPC_DESTINATION_PORT, DataType: rpc.DataUint64, Value: uint64(1337)}, + {Name: rpc.RPC_SOURCE_PORT, DataType: rpc.DataUint64, Value: uint64(0)}, + {Name: rpc.RPC_NEEDS_REPLYBACK_ADDRESS, DataType: rpc.DataString, Value: "deroSender"}, + {Name: rpc.RPC_COMMENT, DataType: rpc.DataString, Value: "gm"}, + }.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + + e := rpc.Entry{Incoming: true, TXID: "mp", Time: testTime, Payload: payload} + got, ok := historyRowMessage(e) + want := "Received;;;deroSender;;;gm;;;" + testStamp + ";;;mp;;;deroSender" + if !ok || got != want { + t.Fatalf("historyRowMessage(raw payload) = %q,%v; want %q,true", got, ok, want) + } +} + +func TestStreamHistoryRows(t *testing.T) { + // Chain order (oldest first). historyRowNormal drops the coinbase. + entries := []rpc.Entry{ + {Coinbase: false, Incoming: true, Amount: 100000, Height: 1, TXID: "a", Time: testTime}, + {Coinbase: true, Incoming: true, Amount: 200000, Height: 2, TXID: "cb", Time: testTime}, + {Coinbase: false, Incoming: false, Amount: 300000, Height: 3, TXID: "b", Time: testTime}, + } + + base := streamHistoryRows(entries, historyRowNormal, 0, nil) + if len(base) != 2 { + t.Fatalf("want 2 rows (coinbase filtered), got %d: %v", len(base), base) + } + // Newest first: height 3 then height 1. + if !strings.Contains(base[0], ";;;3;;;") || !strings.Contains(base[1], ";;;1;;;") { + t.Fatalf("expected newest-first (height 3 then 1), got %v", base) + } + + // The final slice must be invariant to batch size — this is what licenses + // the optimize-loop changing the reveal cadence. + for _, batch := range []int{1, 2, 50, 1000} { + out := streamHistoryRows(entries, historyRowNormal, batch, func([]string) {}) + if len(out) != len(base) { + t.Fatalf("batch %d changed length: %d vs %d", batch, len(out), len(base)) + } + for i := range out { + if out[i] != base[i] { + t.Fatalf("batch %d changed row %d: %q vs %q", batch, i, out[i], base[i]) + } + } + } +} + +func TestHistoryListSmoke(t *testing.T) { + test.NewTempApp(t) + + var data []string + listBox, listData := newHistoryList(&data) + + rows := []string{ + "Received;;;1.00000;;;5;;;2024-01-02;;;x", + "Sent;;;(2.00000);;;6;;;2024-01-02;;;y", + } + if err := listData.Set(rows); err != nil { + t.Fatalf("listData.Set: %v", err) + } + if listBox.Length() != len(rows) { + t.Fatalf("listBox.Length() = %d; want %d", listBox.Length(), len(rows)) + } +} + +// makeEntries builds n synthetic transfers, most carrying a real CBOR payload so +// that ProcessPayload does production-representative decode work (otherwise the +// benchmark would be blind to the dominant build-path cost). +func makeEntries(n int) []rpc.Entry { + payload, err := rpc.Arguments{ + {Name: rpc.RPC_DESTINATION_PORT, DataType: rpc.DataUint64, Value: uint64(0)}, + {Name: rpc.RPC_SOURCE_PORT, DataType: rpc.DataUint64, Value: uint64(0)}, + {Name: rpc.RPC_COMMENT, DataType: rpc.DataString, Value: "payment for services rendered"}, + }.MarshalBinary() + if err != nil { + panic(err) + } + out := make([]rpc.Entry, n) + for i := 0; i < n; i++ { + out[i] = rpc.Entry{ + Height: uint64(i + 1), + Incoming: i%2 == 0, + Amount: uint64((i + 1) * 137), + TXID: "tx" + strconv.Itoa(i), + Time: testTime, + Payload: payload, + } + } + return out +} + +func BenchmarkStreamHistoryRows(b *testing.B) { + entries := makeEntries(20000) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = streamHistoryRows(entries, historyRowNormal, 50, func([]string) {}) + } +} + +func BenchmarkHistoryListPopulate(b *testing.B) { + test.NewApp() + rows := streamHistoryRows(makeEntries(20000), historyRowNormal, 0, nil) + var data []string + _, listData := newHistoryList(&data) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = listData.Set(rows) + } +} + +// TestFastFormatMoneyMatchesGlobals is the correctness gate for the fastFormatMoney +// optimization: it must produce byte-identical output to globals.FormatMoney for +// every uint64. globals.FormatMoney remains the ground truth; this proves the fast +// integer path (and the big.Float fallback above fastMoneyMax) never diverges. +func TestFastFormatMoneyMatchesGlobals(t *testing.T) { + check := func(a uint64) { + t.Helper() + if got, want := fastFormatMoney(a), globals.FormatMoney(a); got != want { + t.Fatalf("fastFormatMoney(%d) = %q; globals.FormatMoney = %q", a, got, want) + } + } + + // Exhaustive low range: every fractional pattern, carry, and leading-zero case. + for a := uint64(0); a <= 1_000_000; a++ { + check(a) + } + + // Many magnitudes of the whole part with edge fractions, up to past total supply + // and to the fast-path boundary. + fracs := []uint64{0, 1, 9, 10, 99, 100, 9999, 10000, 50000, 99998, 99999} + for whole := uint64(1); whole <= 2_000_000_000_000; whole = whole*7 + 3 { + for _, f := range fracs { + check(whole*100000 + f) + } + } + + // The fast/fallback boundary and the big.Float-only region above it. + for _, base := range []uint64{ + fastMoneyMax - 8, fastMoneyMax, fastMoneyMax + 8, + 9_000_000_000_000_000_000, 9_223_372_036_854_775_807, + 18_000_000_000_000_000_000, ^uint64(0), + } { + for d := uint64(0); d < 9 && base+d >= base; d++ { + check(base + d) + } + } + + // Deterministic random across the full uint64 range. + r := rand.New(rand.NewSource(20260630)) + for i := 0; i < 1_000_000; i++ { + check(r.Uint64()) + } +} + +// TestAppendDateMatchesFormat is the correctness gate for appendDate: it must +// match t.Format("2006-01-02") byte-for-byte for any date (the fast 4-digit-year +// path and the AppendFormat fallback for edge years alike). +func TestAppendDateMatchesFormat(t *testing.T) { + check := func(tm time.Time) { + t.Helper() + if got, want := string(appendDate(nil, tm)), tm.Format("2006-01-02"); got != want { + t.Fatalf("appendDate(%v) = %q; Format = %q", tm, got, want) + } + } + + // Structured sweep incl. leap years, month/day boundaries, and edge year ranges + // that exercise the AppendFormat fallback (<1000, >9999). + for _, y := range []int{1, 99, 999, 1000, 1969, 1970, 2000, 2024, 2025, 9999, 10000, 12024} { + for mo := 1; mo <= 12; mo++ { + for _, d := range []int{1, 9, 10, 28, 29, 30, 31} { + check(time.Date(y, time.Month(mo), d, 13, 14, 15, 0, time.UTC)) + } + } + } + + // Deterministic random instants across ~300 years. + r := rand.New(rand.NewSource(42)) + base := time.Date(1971, 1, 1, 0, 0, 0, 0, time.UTC).Unix() + for i := 0; i < 200_000; i++ { + check(time.Unix(base+r.Int63n(300*365*24*3600), 0).UTC()) + } +} + +// TestHistoryCache pins the cache contract layoutHistory relies on: per-view +// storage, address isolation (one wallet can never paint another's rows), +// whole-wallet eviction on address switch, empty history as a valid hit, and +// clear() dropping everything on wallet close. +func TestHistoryCache(t *testing.T) { + var c historyCache + + if _, ok := c.get("walletA", "Normal"); ok { + t.Fatal("empty cache must miss") + } + + rowsA := []string{"r1", "r2"} + c.put("walletA", "Normal", rowsA) + if got, ok := c.get("walletA", "Normal"); !ok || !slices.Equal(got, rowsA) { + t.Fatalf("get after put = %v,%v; want %v,true", got, ok, rowsA) + } + + if _, ok := c.get("walletA", "Coinbase"); ok { + t.Fatal("views must be cached independently") + } + c.put("walletA", "Coinbase", []string{"c1"}) + if _, ok := c.get("walletA", "Normal"); !ok { + t.Fatal("putting a sibling view must not evict") + } + + if got, ok := c.get("walletB", "Normal"); ok || got != nil { + t.Fatal("another wallet's address must miss") + } + + c.put("walletB", "Normal", []string{"x"}) + if _, ok := c.get("walletA", "Normal"); ok { + t.Fatal("address switch must evict the previous wallet") + } + if _, ok := c.get("walletA", "Coinbase"); ok { + t.Fatal("address switch must evict ALL of the previous wallet's views") + } + + c.put("walletB", "Messages", nil) + if got, ok := c.get("walletB", "Messages"); !ok || len(got) != 0 { + t.Fatal("empty rows must cache as a valid hit") + } + + c.put("", "Normal", []string{"x"}) + if _, ok := c.get("", "Normal"); ok { + t.Fatal("empty address must never store or hit") + } + + c.clear() + if _, ok := c.get("walletB", "Normal"); ok { + t.Fatal("clear must evict everything") + } +} + +// TestHistoryCacheConcurrent exercises put/get/clear from many goroutines; it +// exists to fail under `go test -race` if the internal locking ever regresses. +func TestHistoryCacheConcurrent(t *testing.T) { + var c historyCache + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + addr := "wallet" + strconv.Itoa(n%2) + for j := 0; j < 500; j++ { + c.put(addr, "Normal", []string{"r"}) + c.get(addr, "Normal") + if j%100 == 0 { + c.clear() + } + } + }(i) + } + wg.Wait() +} diff --git a/layouts.go b/layouts.go index 24f6b38..c6bb263 100644 --- a/layouts.go +++ b/layouts.go @@ -27,6 +27,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "sort" "strconv" "strings" @@ -551,7 +552,9 @@ func layoutDashboard() fyne.CanvasObject { linkHistory := widget.NewHyperlinkWithStyle("View History", nil, fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) linkHistory.OnTapped = func() { session.LastDomain = session.Window.Content() - session.Window.SetContent(layoutTransition()) + // No layoutTransition here: both SetContent calls run in this event + // handler, so the transition frame never paints — it was only a GIF + // re-decode on the tap path. session.Window.SetContent(layoutHistory()) removeOverlays() } @@ -3201,6 +3204,7 @@ func layoutRestore() fyne.CanvasObject { session.Path = "" session.Name = "" tx = Transfers{} + histCache.clear() btnCreate.Hide() form.Hide() @@ -9406,73 +9410,36 @@ func layoutAlert(t int) fyne.CanvasObject { func layoutHistory() fyne.CanvasObject { var data []string - var entries []rpc.Entry var zeroscid crypto.Hash - var listData binding.StringList - var listBox *widget.List - var txid string - view := "" + // TXID -> entry for the rows currently shown, published on the UI thread + // by load()'s final fyne.Do. Lets a row tap open the detail view without + // re-scanning the wallet under its exclusive lock. + var byTXID map[string]rpc.Entry + + // loadGen invalidates in-flight loads when the user switches views, so a + // slow older fetch (stalled behind a wallet save) can never repaint the + // screen after a newer view took over. Only touched on the UI thread + // (load itself and fyne.Do callbacks). + var loadGen int header := canvas.NewText(" Transaction History", colors.Green) header.TextSize = 22 header.TextStyle = fyne.TextStyle{Bold: true} - details_header := canvas.NewText(" Transaction Detail", colors.Green) - details_header.TextSize = 22 - details_header.TextStyle = fyne.TextStyle{Bold: true} - frame := &iframe{} rectWidth := canvas.NewRectangle(color.Transparent) rectWidth.SetMinSize(fyne.NewSize(ui.MaxWidth, 10)) - rectWidth90 := canvas.NewRectangle(color.Transparent) - rectWidth90.SetMinSize(fyne.NewSize(ui.Width, 10)) heading := canvas.NewText("H I S T O R Y", colors.Gray) heading.TextSize = 16 heading.Alignment = fyne.TextAlignCenter heading.TextStyle = fyne.TextStyle{Bold: true} - rect := canvas.NewRectangle(color.Transparent) - rect.SetMinSize(fyne.NewSize(ui.Width*0.3, 35)) - - rectMid := canvas.NewRectangle(color.Transparent) - rectMid.SetMinSize(fyne.NewSize(ui.Width*0.35, 35)) - results := canvas.NewText("", colors.Green) results.TextSize = 13 - listData = binding.BindStringList(&data) - listBox = widget.NewListWithData(listData, - func() fyne.CanvasObject { - return container.NewHBox( - container.NewStack( - rect, - widget.NewLabel(""), - ), - container.NewStack( - rectMid, - widget.NewLabel(""), - ), - container.NewStack( - rect, - widget.NewLabel(""), - ), - ) - }, - func(di binding.DataItem, co fyne.CanvasObject) { - dat := di.(binding.String) - str, err := dat.Get() - if err != nil { - return - } - - split := strings.Split(str, ";;;") - - co.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(split[0]) - co.(*fyne.Container).Objects[1].(*fyne.Container).Objects[1].(*widget.Label).SetText(split[1]) - co.(*fyne.Container).Objects[2].(*fyne.Container).Objects[1].(*widget.Label).SetText(split[3]) - }) + listBox, listData := newHistoryList(&data) menu := widget.NewSelect([]string{"Normal", "Coinbase", "Messages"}, nil) menu.PlaceHolder = "(Select Transaction Type)" @@ -9508,228 +9475,171 @@ func layoutHistory() fyne.CanvasObject { linkBack := widget.NewHyperlinkWithStyle("Back to Dashboard", nil, fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) linkBack.OnTapped = func() { session.LastDomain = session.Window.Content() - session.Window.SetContent(layoutTransition()) + // No layoutTransition: it never paints (see linkHistory in layoutDashboard). session.Window.SetContent(layoutDashboard()) removeOverlays() } - label := canvas.NewText(view, colors.Account) - label.TextSize = 15 - label.TextStyle = fyne.TextStyle{Bold: true} - - menu.OnChanged = func(s string) { - switch s { - case "Normal": - listBox.UnselectAll() - results.Text = " Scanning..." - results.Refresh() - count := 0 - data = nil - listData.Set(nil) - entries = engram.Disk.Show_Transfers(zeroscid, false, true, true, 0, engram.Disk.Get_Height(), "", "", 0, 0) - - if entries != nil { - go func() { - for e := range entries { - var height string - var direction string - var stamp string - - entries[e].ProcessPayload() - - if !entries[e].Coinbase { - timefmt := entries[e].Time - //stamp = string(timefmt.Format(time.RFC822)) - stamp = timefmt.Format("2006-01-02") - height = strconv.FormatUint(entries[e].Height, 10) - amount := "" - txid = entries[e].TXID - - if !entries[e].Incoming { - direction = "Sent" - amount = "(" + globals.FormatMoney(entries[e].Amount) + ")" - } else { - direction = "Received" - amount = globals.FormatMoney(entries[e].Amount) - } - - count += 1 - data = append(data, direction+";;;"+amount+";;;"+height+";;;"+stamp+";;;"+txid) - } - } - - results.Text = fmt.Sprintf(" Results: %d", count) - - listData.Set(data) - - listBox.OnSelected = func(id widget.ListItemID) { - //var zeroscid crypto.Hash - split := strings.Split(data[id], ";;;") - var zeroscid crypto.Hash - _, result := engram.Disk.Get_Payments_TXID(zeroscid, split[4]) - - if result.TXID == "" { - label.Text = "---" - } else { - label.Text = result.TXID - } - label.Refresh() + // load runs the (potentially heavy) transfer scan OFF the UI goroutine and + // reveals rows newest-first in batches, so the first screenful paints almost + // immediately instead of blocking on the full history. Revisits skip even + // that: rows cached by the last build (histCache) paint synchronously and + // the fresh scan swaps in underneath only if anything changed — the scan + // can stall for seconds behind the wallet's save lock, so the user must + // never wait on it twice. Per Fyne v2.6, the non-binding widget calls + // (Text/Refresh/scroll) must run inside fyne.Do; listData.Set is kept on + // the UI thread too so it never races OnSelected reading data[id]. + load := func(label string, fetch func(w *walletapi.Wallet_Disk) []rpc.Entry, build func(rpc.Entry) (string, bool), onSel func(widget.ListItemID)) { + listBox.UnselectAll() + listBox.OnSelected = func(id widget.ListItemID) { + // The background refresh can swap the rows between the tap and + // this callback; never index past the current data. + if id < 0 || id >= len(data) { + listBox.UnselectAll() + return + } + onSel(id) + } - overlay := session.Window.Canvas().Overlays() - overlay.Add( - container.NewStack( - &iframe{}, - canvas.NewRectangle(colors.DarkMatter), - ), - ) - overlay.Add(layoutHistoryDetail(split[4])) - listBox.UnselectAll() - } + // Snapshot the wallet for the whole load: the goroutine below may + // outlive a sign-out (closeWallet sets engram.Disk to nil while the + // fetch waits on the wallet's save lock), so it must never touch the + // global again — a nil deref in a bare goroutine would kill the app. + w := engram.Disk + if w == nil { + return + } + addr := w.GetAddress().String() + byTXID = nil + loadGen++ + gen := loadGen - fyne.Do(func() { - results.Refresh() - listBox.Refresh() - listBox.ScrollToBottom() - }) - }() - } else { - results.Text = fmt.Sprintf(" Results: %d", count) - results.Refresh() - } - case "Coinbase": - listBox.UnselectAll() + cached, hit := histCache.get(addr, label) + if hit { + listData.Set(cached) + results.Text = fmt.Sprintf(" Results: %d (updating)", len(cached)) + results.Refresh() + listBox.Refresh() + listBox.ScrollToTop() + } else { results.Text = " Scanning..." results.Refresh() - count := 0 data = nil listData.Set(nil) - entries = engram.Disk.Show_Transfers(zeroscid, true, true, true, 0, engram.Disk.Get_Height(), "", "", 0, 0) - - if entries != nil { - go func() { - for e := range entries { - var height string - var direction string - var stamp string - - entries[e].ProcessPayload() - - if entries[e].Coinbase { - direction = "Network" - timefmt := entries[e].Time - stamp = timefmt.Format("2006-01-02") - height = strconv.FormatUint(entries[e].Height, 10) - amount := globals.FormatMoney(entries[e].Amount) - txid = entries[e].TXID - - count += 1 - data = append(data, direction+";;;"+amount+";;;"+height+";;;"+stamp+";;;"+txid) - } - } - - results.Text = fmt.Sprintf(" Results: %d", count) - - listData.Set(data) + } - listBox.OnSelected = func(id widget.ListItemID) { - listBox.UnselectAll() - } + go func() { + entries := fetch(w) + logger.Printf("[History] %s: entries=%d height=%d\n", label, len(entries), w.Get_Height()) + var final []string + if hit { + // Rows are already on screen: build silently, swap once below. + final = streamHistoryRows(entries, build, 0, nil) + } else { + final = streamHistoryRows(entries, build, 50, func(snap []string) { fyne.Do(func() { - results.Refresh() - listBox.Refresh() - listBox.ScrollToBottom() + if gen != loadGen { + return + } + listData.Set(snap) }) - }() - } else { - results.Text = fmt.Sprintf(" Results: %d", count) - - fyne.Do(func() { - results.Refresh() }) } - case "Messages": - listBox.UnselectAll() - results.Text = " Scanning..." - results.Refresh() - count := 0 - data = nil - listData.Set(nil) - entries = engram.Disk.Get_Payments_DestinationPort(zeroscid, uint64(1337), 0) - - if entries != nil { - go func() { - for e := range entries { - var stamp string - var direction string - var comment string - - entries[e].ProcessPayload() - - timefmt := entries[e].Time - //stamp = string(timefmt.Format(time.RFC822)) - stamp = timefmt.Format("2006-01-02") + if engram.Disk != w { // wallet closed or switched mid-fetch: + return // do not re-populate the cache closeWallet just cleared + } + // Cache even a superseded fetch: it is still the freshest full + // build of this view and the cache is keyed per view. + histCache.put(addr, label, final) + + m := make(map[string]rpc.Entry, len(entries)) + for _, e := range entries { + // Keep the FIRST entry per TXID: multi-payload transactions + // repeat a TXID, and the old tap-time Get_Payments_TXID scan + // returned the first match in slice order. + if _, ok := m[e.TXID]; !ok { + m[e.TXID] = e + } + } - temp := entries[e].Incoming - if !temp { - direction = "Sent " - } else { - direction = "Received" - } - if entries[e].Payload_RPC.HasValue(rpc.RPC_COMMENT, rpc.DataString) { - contact := "" - username := "" - if entries[e].Payload_RPC.HasValue(rpc.RPC_NEEDS_REPLYBACK_ADDRESS, rpc.DataString) { - contact = entries[e].Payload_RPC.Value(rpc.RPC_NEEDS_REPLYBACK_ADDRESS, rpc.DataString).(string) - if len(contact) > 10 { - username = contact[0:10] + ".." - } else { - username = contact - } - } + fyne.Do(func() { + if gen != loadGen { + return + } + byTXID = m + // After a cached paint, an identical rebuild is the common case; + // skipping the swap avoids flicker and keeps the scroll position. + if !hit || !slices.Equal(final, cached) { + listData.Set(final) + listBox.Refresh() + } + results.Text = fmt.Sprintf(" Results: %d", len(final)) + results.Refresh() + if !hit { + listBox.ScrollToTop() + } + }) + }() + } - comment = entries[e].Payload_RPC.Value(rpc.RPC_COMMENT, rpc.DataString).(string) - if len(comment) > 10 { - comment = comment[0:10] + ".." - } + menu.OnChanged = func(s string) { + switch s { + case "Normal": + load("Normal", + func(w *walletapi.Wallet_Disk) []rpc.Entry { + return w.Show_Transfers(zeroscid, false, true, true, 0, w.Get_Height(), "", "", 0, 0) + }, + historyRowNormal, + func(id widget.ListItemID) { + split := strings.Split(data[id], ";;;") - txid = entries[e].TXID - count += 1 - data = append(data, direction+";;;"+username+";;;"+comment+";;;"+stamp+";;;"+txid+";;;"+contact) - } + overlay := session.Window.Canvas().Overlays() + overlay.Add( + container.NewStack( + &iframe{}, + canvas.NewRectangle(colors.DarkMatter), + ), + ) + var entry *rpc.Entry + if e, ok := byTXID[split[4]]; ok { + entry = &e } - - results.Text = fmt.Sprintf(" Results: %d", count) - - listData.Set(data) - - listBox.OnSelected = func(id widget.ListItemID) { - split := strings.Split(data[id], ";;;") - overlay := session.Window.Canvas().Overlays() - overlay.Add( - container.NewStack( - &iframe{}, - canvas.NewRectangle(colors.DarkMatter), - ), - ) - overlay.Add(layoutHistoryDetail(split[4])) - listBox.UnselectAll() - listBox.Refresh() + overlay.Add(layoutHistoryDetail(split[4], entry)) + listBox.UnselectAll() + }) + case "Coinbase": + load("Coinbase", + func(w *walletapi.Wallet_Disk) []rpc.Entry { + return w.Show_Transfers(zeroscid, true, true, true, 0, w.Get_Height(), "", "", 0, 0) + }, + historyRowCoinbase, + func(id widget.ListItemID) { + listBox.UnselectAll() + }) + case "Messages": + load("Messages", + func(w *walletapi.Wallet_Disk) []rpc.Entry { + return w.Get_Payments_DestinationPort(zeroscid, uint64(1337), 0) + }, + historyRowMessage, + func(id widget.ListItemID) { + split := strings.Split(data[id], ";;;") + overlay := session.Window.Canvas().Overlays() + overlay.Add( + container.NewStack( + &iframe{}, + canvas.NewRectangle(colors.DarkMatter), + ), + ) + var entry *rpc.Entry + if e, ok := byTXID[split[4]]; ok { + entry = &e } - - fyne.Do(func() { - results.Refresh() - listBox.Refresh() - listBox.ScrollToBottom() - }) - }() - } else { - results.Text = fmt.Sprintf(" Results: %d", count) - - fyne.Do(func() { - results.Refresh() + overlay.Add(layoutHistoryDetail(split[4], entry)) + listBox.UnselectAll() + listBox.Refresh() }) - } default: } @@ -9804,10 +9714,17 @@ func layoutHistory() fyne.CanvasObject { ), ) + menu.SetSelected("Normal") + return NewVScroll(layout) } -func layoutHistoryDetail(txid string) fyne.CanvasObject { +// layoutHistoryDetail renders the detail overlay for one transfer. The caller +// passes the entry when it already holds it (the history list keeps a +// TXID-indexed copy of the rows it shows); the nil fallback re-scans the +// wallet by TXID and is only reachable while the list's first fetch is still +// in flight. +func layoutHistoryDetail(txid string, entry *rpc.Entry) fyne.CanvasObject { wSpacer := widget.NewLabel(" ") rectWidth := canvas.NewRectangle(color.Transparent) @@ -9947,8 +9864,18 @@ func layoutHistoryDetail(txid string) fyne.CanvasObject { layout.NewSpacer(), ) - var zeroscid crypto.Hash - _, details := engram.Disk.Get_Payments_TXID(zeroscid, txid) + var details rpc.Entry + if entry != nil { + details = *entry + // Stored entries can carry an undecoded payload; decode the local + // copy before display (mirrors historyRowMessage). + if len(details.Payload) > 0 { + details.ProcessPayload() + } + } else { + var zeroscid crypto.Hash + _, details = engram.Disk.Get_Payments_TXID(zeroscid, txid) + } stamp := string(details.Time.Format(time.RFC822)) height := strconv.FormatUint(details.Height, 10) diff --git a/main.go b/main.go index 9841e24..0214ffb 100644 --- a/main.go +++ b/main.go @@ -164,19 +164,17 @@ func main() { // Intercept mobile back button event session.Window.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) { if ev.Name == mobile.KeyBack { + // No layoutTransition on the back path: both SetContent calls run + // inside this key handler, so the transition frame never paints — + // it only re-decoded the loading GIF and left it animating unseen. if session.LastDomain != nil { - session.Window.SetContent(layoutTransition()) session.Window.SetContent(session.LastDomain) } else { if engram.Disk != nil { - session.LastDomain = layoutDashboard() session.LastDomain = session.Window.Content() - session.Window.SetContent(layoutTransition()) session.Window.SetContent(layoutDashboard()) } else { - session.LastDomain = layoutMain() session.LastDomain = session.Window.Content() - session.Window.SetContent(layoutTransition()) session.Window.SetContent(layoutMain()) } }