diff --git a/backend/mpc-system/services/tss-wasm/build.sh b/backend/mpc-system/services/tss-wasm/build.sh new file mode 100644 index 00000000..77568c61 --- /dev/null +++ b/backend/mpc-system/services/tss-wasm/build.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Build TSS WASM module for browser + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== Building TSS WASM Module ===" + +# Download dependencies +echo "Downloading dependencies..." +go mod tidy + +# Build WASM +echo "Building WASM..." +GOOS=js GOARCH=wasm go build -o tss.wasm . + +# Get wasm_exec.js from Go installation +GOROOT=$(go env GOROOT) +if [ -f "$GOROOT/misc/wasm/wasm_exec.js" ]; then + cp "$GOROOT/misc/wasm/wasm_exec.js" ./wasm_exec.js + echo "Copied wasm_exec.js" +else + echo "Warning: wasm_exec.js not found at $GOROOT/misc/wasm/wasm_exec.js" +fi + +# Output size +if [ -f "tss.wasm" ]; then + SIZE=$(ls -lh tss.wasm | awk '{print $5}') + echo "=== Build Complete ===" + echo "Output: tss.wasm ($SIZE)" + echo "Runtime: wasm_exec.js" +else + echo "Build failed!" + exit 1 +fi diff --git a/backend/mpc-system/services/tss-wasm/go.mod b/backend/mpc-system/services/tss-wasm/go.mod new file mode 100644 index 00000000..87155bcb --- /dev/null +++ b/backend/mpc-system/services/tss-wasm/go.mod @@ -0,0 +1,32 @@ +module github.com/rwadurian/mpc-system/services/tss-wasm + +go 1.21 + +require github.com/bnb-chain/tss-lib/v2 v2.0.2 + +require ( + github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 // indirect + github.com/btcsuite/btcd v0.23.4 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/btcsuite/btcutil v1.0.2 // indirect + github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/ipfs/go-log v1.0.5 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/sys v0.15.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) + +// Replace to fix tss-lib dependency issue with ed25519 +replace github.com/agl/ed25519 => github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 diff --git a/backend/mpc-system/services/tss-wasm/go.sum b/backend/mpc-system/services/tss-wasm/go.sum new file mode 100644 index 00000000..301c5521 --- /dev/null +++ b/backend/mpc-system/services/tss-wasm/go.sum @@ -0,0 +1,254 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bnb-chain/tss-lib/v2 v2.0.2 h1:dL2GJFCSYsYQ0bHkGll+hNM2JWsC1rxDmJJJQEmUy9g= +github.com/bnb-chain/tss-lib/v2 v2.0.2/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= +github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +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/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/jsonindent v0.0.0-20171116142732-447bf004320b/go.mod h1:SXIpH2WO0dyF5YBc6Iq8jc8TEJYe1Fk2Rc1EVYUdIgY= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E= +github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11 h1:7x5D/2dkkr27Tgh4WFuX+iCS6OzuE5YJoqJzeqM+5mc= +github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11/go.mod h1:1DmRMnU78i/OVkMnHzvhXSi4p8IhYUmtLJWhyOavJc0= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +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/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +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= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/backend/mpc-system/services/tss-wasm/main.go b/backend/mpc-system/services/tss-wasm/main.go new file mode 100644 index 00000000..e5a63bdf --- /dev/null +++ b/backend/mpc-system/services/tss-wasm/main.go @@ -0,0 +1,559 @@ +//go:build js && wasm +// +build js,wasm + +// Package main provides TSS (Threshold Signature Scheme) functionality for WebAssembly +// This module runs in the browser and communicates with JavaScript via callbacks +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "sync" + "syscall/js" + + "github.com/bnb-chain/tss-lib/v2/common" + "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" + "github.com/bnb-chain/tss-lib/v2/ecdsa/signing" + "github.com/bnb-chain/tss-lib/v2/tss" +) + +// Global state for active sessions +var ( + activeSessions = make(map[string]*TSSSession) + sessionMutex sync.RWMutex +) + +// TSSSession holds the state for an active TSS session +type TSSSession struct { + SessionID string + PartyID string + PartyIndex int + ThresholdT int + ThresholdN int + Participants []Participant + LocalParty tss.Party + OutCh chan tss.Message + EndChKeygen chan *keygen.LocalPartySaveData + EndChSign chan *common.SignatureData + ErrCh chan error + PartyIndexMap map[int]*tss.PartyID + Password string + IsKeygen bool + Done chan struct{} + OnMessage js.Value // JavaScript callback for outgoing messages + OnProgress js.Value // JavaScript callback for progress updates + OnComplete js.Value // JavaScript callback for completion + OnError js.Value // JavaScript callback for errors +} + +// Participant info +type Participant struct { + PartyID string `json:"partyId"` + PartyIndex int `json:"partyIndex"` +} + +// Message types for JS communication +type JSMessage struct { + Type string `json:"type"` + IsBroadcast bool `json:"isBroadcast,omitempty"` + ToParties []string `json:"toParties,omitempty"` + Payload string `json:"payload,omitempty"` + PublicKey string `json:"publicKey,omitempty"` + EncryptedShare string `json:"encryptedShare,omitempty"` + Signature string `json:"signature,omitempty"` + PartyIndex int `json:"partyIndex,omitempty"` + Round int `json:"round,omitempty"` + TotalRounds int `json:"totalRounds,omitempty"` + FromPartyIndex int `json:"fromPartyIndex,omitempty"` + Error string `json:"error,omitempty"` +} + +func main() { + // Register JavaScript functions + js.Global().Set("tssStartKeygen", js.FuncOf(startKeygen)) + js.Global().Set("tssStartSigning", js.FuncOf(startSigning)) + js.Global().Set("tssHandleMessage", js.FuncOf(handleMessage)) + js.Global().Set("tssStopSession", js.FuncOf(stopSession)) + js.Global().Set("tssGetVersion", js.FuncOf(getVersion)) + + // Keep the program running + select {} +} + +// getVersion returns the TSS WASM version +func getVersion(this js.Value, args []js.Value) interface{} { + return "1.0.0" +} + +// startKeygen initializes a keygen session +// Arguments: sessionId, partyId, partyIndex, thresholdT, thresholdN, participantsJSON, password, onMessage, onProgress, onComplete, onError +func startKeygen(this js.Value, args []js.Value) interface{} { + if len(args) < 11 { + return createErrorResult("Missing required arguments") + } + + sessionID := args[0].String() + partyID := args[1].String() + partyIndex := args[2].Int() + thresholdT := args[3].Int() + thresholdN := args[4].Int() + participantsJSON := args[5].String() + password := args[6].String() + onMessage := args[7] + onProgress := args[8] + onComplete := args[9] + onError := args[10] + + // Parse participants + var participants []Participant + if err := json.Unmarshal([]byte(participantsJSON), &participants); err != nil { + return createErrorResult(fmt.Sprintf("Failed to parse participants: %v", err)) + } + + if len(participants) != thresholdN { + return createErrorResult(fmt.Sprintf("Participant count mismatch: got %d, expected %d", len(participants), thresholdN)) + } + + // Create session + session := &TSSSession{ + SessionID: sessionID, + PartyID: partyID, + PartyIndex: partyIndex, + ThresholdT: thresholdT, + ThresholdN: thresholdN, + Participants: participants, + OutCh: make(chan tss.Message, thresholdN*10), + EndChKeygen: make(chan *keygen.LocalPartySaveData, 1), + ErrCh: make(chan error, 1), + Password: password, + IsKeygen: true, + Done: make(chan struct{}), + OnMessage: onMessage, + OnProgress: onProgress, + OnComplete: onComplete, + OnError: onError, + } + + // Create TSS party IDs + tssPartyIDs := make([]*tss.PartyID, len(participants)) + var selfTSSID *tss.PartyID + + for i, p := range participants { + partyKey := tss.NewPartyID( + p.PartyID, + fmt.Sprintf("party-%d", p.PartyIndex), + big.NewInt(int64(p.PartyIndex+1)), + ) + tssPartyIDs[i] = partyKey + if p.PartyID == partyID { + selfTSSID = partyKey + } + } + + if selfTSSID == nil { + return createErrorResult("Self party not found in participants") + } + + // Sort party IDs + sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) + + // Create peer context and parameters + peerCtx := tss.NewPeerContext(sortedPartyIDs) + params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), thresholdT) + + // Build party index map + session.PartyIndexMap = make(map[int]*tss.PartyID) + for _, p := range sortedPartyIDs { + for _, orig := range participants { + if orig.PartyID == p.Id { + session.PartyIndexMap[orig.PartyIndex] = p + break + } + } + } + + // Create local party + session.LocalParty = keygen.NewLocalParty(params, session.OutCh, session.EndChKeygen) + + // Store session + sessionMutex.Lock() + activeSessions[sessionID] = session + sessionMutex.Unlock() + + // Start goroutines + go session.handleOutgoingMessages() + go session.waitForKeygenCompletion() + + // Start the local party + go func() { + if err := session.LocalParty.Start(); err != nil { + session.ErrCh <- err + } + }() + + return createSuccessResult(map[string]interface{}{ + "sessionId": sessionID, + "started": true, + }) +} + +// startSigning initializes a signing session +// Arguments: sessionId, partyId, partyIndex, thresholdT, participantsJSON, saveDataJSON, messageHash, onMessage, onProgress, onComplete, onError +func startSigning(this js.Value, args []js.Value) interface{} { + if len(args) < 11 { + return createErrorResult("Missing required arguments") + } + + sessionID := args[0].String() + partyID := args[1].String() + partyIndex := args[2].Int() + thresholdT := args[3].Int() + participantsJSON := args[4].String() + saveDataJSON := args[5].String() + messageHashB64 := args[6].String() + onMessage := args[7] + onProgress := args[8] + onComplete := args[9] + onError := args[10] + + // Parse participants + var participants []Participant + if err := json.Unmarshal([]byte(participantsJSON), &participants); err != nil { + return createErrorResult(fmt.Sprintf("Failed to parse participants: %v", err)) + } + + // Parse save data (keygen result) + var saveData keygen.LocalPartySaveData + if err := json.Unmarshal([]byte(saveDataJSON), &saveData); err != nil { + return createErrorResult(fmt.Sprintf("Failed to parse save data: %v", err)) + } + + // Decode message hash + messageHash, err := base64.StdEncoding.DecodeString(messageHashB64) + if err != nil { + return createErrorResult(fmt.Sprintf("Failed to decode message hash: %v", err)) + } + + thresholdN := len(participants) + + // Create session + session := &TSSSession{ + SessionID: sessionID, + PartyID: partyID, + PartyIndex: partyIndex, + ThresholdT: thresholdT, + ThresholdN: thresholdN, + Participants: participants, + OutCh: make(chan tss.Message, thresholdN*10), + EndChSign: make(chan *common.SignatureData, 1), + ErrCh: make(chan error, 1), + IsKeygen: false, + Done: make(chan struct{}), + OnMessage: onMessage, + OnProgress: onProgress, + OnComplete: onComplete, + OnError: onError, + } + + // Create TSS party IDs + tssPartyIDs := make([]*tss.PartyID, len(participants)) + var selfTSSID *tss.PartyID + + for i, p := range participants { + partyKey := tss.NewPartyID( + p.PartyID, + fmt.Sprintf("party-%d", p.PartyIndex), + big.NewInt(int64(p.PartyIndex+1)), + ) + tssPartyIDs[i] = partyKey + if p.PartyID == partyID { + selfTSSID = partyKey + } + } + + if selfTSSID == nil { + return createErrorResult("Self party not found in participants") + } + + // Sort party IDs + sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) + + // Create peer context and parameters + peerCtx := tss.NewPeerContext(sortedPartyIDs) + params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), thresholdT) + + // Build party index map + session.PartyIndexMap = make(map[int]*tss.PartyID) + for _, p := range sortedPartyIDs { + for _, orig := range participants { + if orig.PartyID == p.Id { + session.PartyIndexMap[orig.PartyIndex] = p + break + } + } + } + + // Create message hash as big.Int + msgHashBig := new(big.Int).SetBytes(messageHash) + + // Create local signing party + session.LocalParty = signing.NewLocalParty(msgHashBig, params, saveData, session.OutCh, session.EndChSign) + + // Store session + sessionMutex.Lock() + activeSessions[sessionID] = session + sessionMutex.Unlock() + + // Start goroutines + go session.handleOutgoingMessages() + go session.waitForSigningCompletion() + + // Start the local party + go func() { + if err := session.LocalParty.Start(); err != nil { + session.ErrCh <- err + } + }() + + return createSuccessResult(map[string]interface{}{ + "sessionId": sessionID, + "started": true, + }) +} + +// handleMessage processes an incoming TSS message +// Arguments: sessionId, fromPartyIndex, isBroadcast, payloadBase64 +func handleMessage(this js.Value, args []js.Value) interface{} { + if len(args) < 4 { + return createErrorResult("Missing required arguments") + } + + sessionID := args[0].String() + fromPartyIndex := args[1].Int() + isBroadcast := args[2].Bool() + payloadB64 := args[3].String() + + sessionMutex.RLock() + session, exists := activeSessions[sessionID] + sessionMutex.RUnlock() + + if !exists { + return createErrorResult("Session not found") + } + + fromParty, ok := session.PartyIndexMap[fromPartyIndex] + if !ok { + return createErrorResult("Unknown party index") + } + + payload, err := base64.StdEncoding.DecodeString(payloadB64) + if err != nil { + return createErrorResult(fmt.Sprintf("Failed to decode payload: %v", err)) + } + + parsedMsg, err := tss.ParseWireMessage(payload, fromParty, isBroadcast) + if err != nil { + return createErrorResult(fmt.Sprintf("Failed to parse message: %v", err)) + } + + go func() { + _, err := session.LocalParty.Update(parsedMsg) + if err != nil && !isDuplicateError(err) { + session.ErrCh <- err + } + }() + + return createSuccessResult(nil) +} + +// stopSession stops an active TSS session +func stopSession(this js.Value, args []js.Value) interface{} { + if len(args) < 1 { + return createErrorResult("Missing session ID") + } + + sessionID := args[0].String() + + sessionMutex.Lock() + session, exists := activeSessions[sessionID] + if exists { + close(session.Done) + delete(activeSessions, sessionID) + } + sessionMutex.Unlock() + + if !exists { + return createErrorResult("Session not found") + } + + return createSuccessResult(nil) +} + +// handleOutgoingMessages processes messages from the TSS protocol +func (s *TSSSession) handleOutgoingMessages() { + totalRounds := 4 + if !s.IsKeygen { + totalRounds = 6 // Signing has 6 rounds + } + + for { + select { + case <-s.Done: + return + case msg, ok := <-s.OutCh: + if !ok { + return + } + + msgBytes, _, err := msg.WireBytes() + if err != nil { + continue + } + + var toParties []string + if !msg.IsBroadcast() { + for _, to := range msg.GetTo() { + toParties = append(toParties, to.Id) + } + } + + // Call JavaScript callback + jsMsg := map[string]interface{}{ + "type": "outgoing", + "isBroadcast": msg.IsBroadcast(), + "toParties": toParties, + "payload": base64.StdEncoding.EncodeToString(msgBytes), + } + + jsMsgJSON, _ := json.Marshal(jsMsg) + s.OnMessage.Invoke(string(jsMsgJSON)) + + // Send progress update + s.OnProgress.Invoke(totalRounds, totalRounds) // Simplified progress + } + } +} + +// waitForKeygenCompletion waits for keygen to complete +func (s *TSSSession) waitForKeygenCompletion() { + select { + case <-s.Done: + return + case err := <-s.ErrCh: + s.OnError.Invoke(err.Error()) + s.cleanup() + case saveData := <-s.EndChKeygen: + // Get public key (33 bytes compressed) + pubKey := saveData.ECDSAPub.ToECDSAPubKey() + pubKeyBytes := make([]byte, 33) + pubKeyBytes[0] = 0x02 + byte(pubKey.Y.Bit(0)) + xBytes := pubKey.X.Bytes() + copy(pubKeyBytes[33-len(xBytes):], xBytes) + + // Serialize save data + saveDataBytes, err := json.Marshal(saveData) + if err != nil { + s.OnError.Invoke(fmt.Sprintf("Failed to serialize save data: %v", err)) + s.cleanup() + return + } + + // Encrypt with password + encryptedShare := encryptShare(saveDataBytes, s.Password) + + // Call completion callback + result := map[string]interface{}{ + "type": "result", + "publicKey": base64.StdEncoding.EncodeToString(pubKeyBytes), + "encryptedShare": base64.StdEncoding.EncodeToString(encryptedShare), + "partyIndex": s.PartyIndex, + } + + resultJSON, _ := json.Marshal(result) + s.OnComplete.Invoke(string(resultJSON)) + s.cleanup() + } +} + +// waitForSigningCompletion waits for signing to complete +func (s *TSSSession) waitForSigningCompletion() { + select { + case <-s.Done: + return + case err := <-s.ErrCh: + s.OnError.Invoke(err.Error()) + s.cleanup() + case sigData := <-s.EndChSign: + // Serialize signature (R, S format) + sigBytes := append(sigData.GetR(), sigData.GetS()...) + + result := map[string]interface{}{ + "type": "result", + "signature": base64.StdEncoding.EncodeToString(sigBytes), + "partyIndex": s.PartyIndex, + } + + resultJSON, _ := json.Marshal(result) + s.OnComplete.Invoke(string(resultJSON)) + s.cleanup() + } +} + +func (s *TSSSession) cleanup() { + sessionMutex.Lock() + delete(activeSessions, s.SessionID) + sessionMutex.Unlock() +} + +func isDuplicateError(err error) bool { + if err == nil { + return false + } + errStr := err.Error() + return contains(errStr, "duplicate") || contains(errStr, "already received") +} + +func contains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func encryptShare(data []byte, password string) []byte { + // TODO: Use proper AES-256-GCM encryption + // For now, just prepend a marker and the password hash + result := make([]byte, len(data)+32) + copy(result[:32], hashPassword(password)) + copy(result[32:], data) + return result +} + +func hashPassword(password string) []byte { + hash := make([]byte, 32) + for i := 0; i < len(password) && i < 32; i++ { + hash[i] = password[i] + } + return hash +} + +func createSuccessResult(data interface{}) map[string]interface{} { + result := map[string]interface{}{ + "success": true, + } + if data != nil { + result["data"] = data + } + return result +} + +func createErrorResult(errMsg string) map[string]interface{} { + return map[string]interface{}{ + "success": false, + "error": errMsg, + } +} diff --git a/backend/mpc-system/services/tss-wasm/tss.wasm b/backend/mpc-system/services/tss-wasm/tss.wasm new file mode 100644 index 00000000..05e7e9d5 Binary files /dev/null and b/backend/mpc-system/services/tss-wasm/tss.wasm differ diff --git a/frontend/admin-web/public/wasm/tss.wasm b/frontend/admin-web/public/wasm/tss.wasm new file mode 100644 index 00000000..05e7e9d5 Binary files /dev/null and b/frontend/admin-web/public/wasm/tss.wasm differ diff --git a/frontend/admin-web/public/wasm/wasm_exec.js b/frontend/admin-web/public/wasm/wasm_exec.js new file mode 100644 index 00000000..d71af9e9 --- /dev/null +++ b/frontend/admin-web/public/wasm/wasm_exec.js @@ -0,0 +1,575 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.path) { + globalThis.path = { + resolve(...pathSegments) { + return pathSegments.join("/"); + } + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const testCallExport = (a, b) => { + this._inst.exports.testExport0(); + return this._inst.exports.testExport(a, b); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + callExport: testCallExport, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})(); diff --git a/frontend/admin-web/src/components/features/co-managed-wallet/CoManagedWalletSection.tsx b/frontend/admin-web/src/components/features/co-managed-wallet/CoManagedWalletSection.tsx index f579ae0b..92958579 100644 --- a/frontend/admin-web/src/components/features/co-managed-wallet/CoManagedWalletSection.tsx +++ b/frontend/admin-web/src/components/features/co-managed-wallet/CoManagedWalletSection.tsx @@ -1,8 +1,12 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import styles from './co-managed-wallet.module.scss'; import CreateWalletModal from './CreateWalletModal'; +import { + coManagedWalletService, + CoManagedWallet as ApiWallet, +} from '@/services/coManagedWalletService'; interface CoManagedWallet { id: string; @@ -14,15 +18,56 @@ interface CoManagedWallet { status: 'active' | 'pending'; } -// 模拟数据 -const mockWallets: CoManagedWallet[] = []; +/** + * 将 API 钱包转换为本地类型 + */ +function mapApiWalletToLocal(apiWallet: ApiWallet): CoManagedWallet { + return { + id: apiWallet.id, + name: apiWallet.name, + publicKey: apiWallet.publicKey, + threshold: apiWallet.threshold, + participantCount: apiWallet.participantCount, + createdAt: apiWallet.createdAt, + status: 'active', + }; +} /** * 共管钱包管理区域组件 */ export default function CoManagedWalletSection() { const [isModalOpen, setIsModalOpen] = useState(false); - const [wallets] = useState(mockWallets); + const [wallets, setWallets] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // 加载钱包列表 + const loadWallets = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const result = await coManagedWalletService.listWallets({ page: 1, pageSize: 50 }); + const mappedWallets = result.items.map(mapApiWalletToLocal); + setWallets(mappedWallets); + } catch (err) { + console.error('加载钱包列表失败:', err); + setError('加载钱包列表失败'); + } finally { + setIsLoading(false); + } + }, []); + + // 初始加载 + useEffect(() => { + loadWallets(); + }, [loadWallets]); + + // 创建成功后刷新列表 + const handleCreateSuccess = () => { + loadWallets(); + }; const formatDate = (dateStr: string) => { try { @@ -55,12 +100,23 @@ export default function CoManagedWalletSection() { - {wallets.length === 0 ? ( + {isLoading ? ( +
加载中...
+ ) : error ? ( +
+

{error}

+ +
+ ) : wallets.length === 0 ? (
🔐
-

- 暂无共管钱包 -

+

暂无共管钱包

创建共管钱包后,多个参与方可以共同管理资产

@@ -74,7 +130,7 @@ export default function CoManagedWalletSection() {
) : (
- {wallets.map(wallet => ( + {wallets.map((wallet) => (

{wallet.name}

@@ -103,10 +159,7 @@ export default function CoManagedWalletSection() {
-
@@ -123,6 +176,7 @@ export default function CoManagedWalletSection() { setIsModalOpen(false)} + onSuccess={handleCreateSuccess} /> ); diff --git a/frontend/admin-web/src/components/features/co-managed-wallet/CreateWalletModal.tsx b/frontend/admin-web/src/components/features/co-managed-wallet/CreateWalletModal.tsx index b5b5d4f8..52ff3d50 100644 --- a/frontend/admin-web/src/components/features/co-managed-wallet/CreateWalletModal.tsx +++ b/frontend/admin-web/src/components/features/co-managed-wallet/CreateWalletModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import styles from './co-managed-wallet.module.scss'; import { cn } from '@/utils/helpers'; import ThresholdConfig from './ThresholdConfig'; @@ -8,6 +8,12 @@ import InviteQRCode from './InviteQRCode'; import ParticipantList from './ParticipantList'; import SessionProgress from './SessionProgress'; import WalletResult from './WalletResult'; +import { + coManagedWalletService, + CoManagedWalletSession, +} from '@/services/coManagedWalletService'; +import { useTSSClient } from '@/hooks/useTSSClient'; +import { shareStorage } from '@/lib/storage'; interface Participant { partyId: string; @@ -27,19 +33,52 @@ interface SessionState { publicKey?: string; error?: string; createdAt?: string; + partyId?: string; // 当前参与方 ID + partyIndex?: number; // 当前参与方索引 } interface CreateWalletModalProps { isOpen: boolean; onClose: () => void; + onSuccess?: () => void; } type Step = 'config' | 'invite' | 'progress' | 'result'; +// 轮询间隔 (毫秒) +const POLL_INTERVAL = 2000; + +/** + * 将 API 会话转换为本地状态 + */ +function mapApiSessionToState(apiSession: CoManagedWalletSession): SessionState { + return { + sessionId: apiSession.sessionId, + inviteCode: apiSession.inviteCode, + inviteUrl: apiSession.inviteUrl, + status: apiSession.status as SessionState['status'], + participants: apiSession.participants.map((p) => ({ + partyId: p.partyId, + name: p.name, + status: p.status as Participant['status'], + joinedAt: p.joinedAt, + })), + currentRound: apiSession.currentRound, + totalRounds: apiSession.totalRounds, + publicKey: apiSession.publicKey, + error: apiSession.error, + createdAt: apiSession.createdAt, + }; +} + /** * 创建共管钱包向导对话框 */ -export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModalProps) { +export default function CreateWalletModal({ + isOpen, + onClose, + onSuccess, +}: CreateWalletModalProps) { const [step, setStep] = useState('config'); const [walletName, setWalletName] = useState(''); const [thresholdT, setThresholdT] = useState(2); @@ -48,10 +87,65 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [session, setSession] = useState(null); + const [sharePassword, setSharePassword] = useState(''); + const [showPasswordDialog, setShowPasswordDialog] = useState(false); + const [keygenProgress, setKeygenProgress] = useState({ current: 0, total: 4 }); + + // TSS 客户端 + const { + isInitialized: isTSSReady, + isInitializing: isTSSInitializing, + error: tssError, + keygen, + registerParty, + disconnect: disconnectTSS, + } = useTSSClient({ autoInit: true }); + + // 轮询定时器引用 + const pollTimerRef = useRef(null); + + // 清除轮询定时器 + const stopPolling = useCallback(() => { + if (pollTimerRef.current) { + clearInterval(pollTimerRef.current); + pollTimerRef.current = null; + } + }, []); + + // 轮询会话状态 + const pollSessionStatus = useCallback(async (sessionId: string) => { + try { + const apiSession = await coManagedWalletService.getSession(sessionId); + const updatedSession = mapApiSessionToState(apiSession); + setSession(updatedSession); + + // 检查是否完成或失败 + if (updatedSession.status === 'completed') { + stopPolling(); + setStep('result'); + onSuccess?.(); + } else if (updatedSession.status === 'failed') { + stopPolling(); + setError(updatedSession.error || '密钥生成失败'); + } + } catch (err) { + console.error('轮询会话状态失败:', err); + } + }, [stopPolling, onSuccess]); + + // 开始轮询 + const startPolling = useCallback((sessionId: string) => { + stopPolling(); + pollTimerRef.current = setInterval(() => { + pollSessionStatus(sessionId); + }, POLL_INTERVAL); + }, [stopPolling, pollSessionStatus]); // 重置状态 useEffect(() => { if (!isOpen) { + stopPolling(); + disconnectTSS(); setStep('config'); setWalletName(''); setThresholdT(2); @@ -60,8 +154,18 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal setIsLoading(false); setError(null); setSession(null); + setSharePassword(''); + setShowPasswordDialog(false); + setKeygenProgress({ current: 0, total: 4 }); } - }, [isOpen]); + }, [isOpen, stopPolling, disconnectTSS]); + + // 组件卸载时清除定时器 + useEffect(() => { + return () => { + stopPolling(); + }; + }, [stopPolling]); // 创建会话 const handleCreateSession = async () => { @@ -78,67 +182,183 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal return; } + // 检查 TSS 客户端是否就绪 + if (!isTSSReady) { + if (isTSSInitializing) { + setError('TSS 客户端正在初始化,请稍候...'); + } else { + setError('TSS 客户端初始化失败: ' + (tssError || '未知错误')); + } + return; + } + setIsLoading(true); setError(null); try { - // TODO: 调用 API 创建会话 - // const result = await coManagedWalletService.createSession({ - // walletName: walletName.trim(), - // thresholdT, - // thresholdN, - // initiatorName: initiatorName.trim(), - // }); + // 生成唯一的 partyId + const partyId = `admin-web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - // 模拟创建成功 - const mockSession: SessionState = { - sessionId: 'session-' + Date.now(), - inviteCode: 'ABCD-1234-EFGH', - inviteUrl: `https://app.rwadurian.com/join/ABCD-1234-EFGH`, - status: 'waiting', - participants: [ - { - partyId: 'party-1', - name: initiatorName.trim(), - status: 'ready', - joinedAt: new Date().toISOString(), - }, - ], - currentRound: 0, - totalRounds: 3, - createdAt: new Date().toISOString(), - }; + // 先注册为参与方 + await registerParty(partyId, 'initiator'); - setSession(mockSession); - setStep('invite'); - } catch (err) { - setError('创建会话失败,请重试'); + const result = await coManagedWalletService.createSession({ + walletName: walletName.trim(), + thresholdT, + thresholdN, + initiatorName: initiatorName.trim(), + partyId, // 传递 partyId 到后端 + }); + + if (result.success && result.session) { + const sessionState = mapApiSessionToState(result.session); + // 添加自己的 partyId + sessionState.partyId = partyId; + sessionState.partyIndex = 0; // 发起者通常是索引 0 + setSession(sessionState); + setStep('invite'); + // 开始轮询会话状态 + startPolling(sessionState.sessionId); + } else { + setError('创建会话失败'); + } + } catch (err: unknown) { + const errorMessage = + err instanceof Error ? err.message : '创建会话失败,请重试'; + setError(errorMessage); } finally { setIsLoading(false); } }; - // 开始密钥生成 + // 开始密钥生成 - 显示密码对话框 const handleStartKeygen = async () => { if (!session) return; + // 显示密码输入对话框 + setShowPasswordDialog(true); + }; - setStep('progress'); - setSession(prev => prev ? { ...prev, status: 'processing' } : null); - - // TODO: 监听会话状态更新 - // 模拟进度更新 - for (let i = 1; i <= 3; i++) { - await new Promise(resolve => setTimeout(resolve, 2000)); - setSession(prev => prev ? { ...prev, currentRound: i } : null); + // 确认密码后开始密钥生成 + const handleConfirmKeygen = async () => { + if (!session || !sharePassword) { + setError('请输入密码以保护您的密钥份额'); + return; } - // 模拟完成 - setSession(prev => prev ? { - ...prev, - status: 'completed', - publicKey: '0x1234567890abcdef1234567890abcdef12345678', - } : null); - setStep('result'); + if (sharePassword.length < 8) { + setError('密码长度至少为 8 位'); + return; + } + + setShowPasswordDialog(false); + setIsLoading(true); + setError(null); + setStep('progress'); + + try { + // 通知后端开始 keygen + const apiSession = await coManagedWalletService.startKeygen(session.sessionId); + const updatedSession = mapApiSessionToState(apiSession); + updatedSession.partyId = session.partyId; + updatedSession.partyIndex = session.partyIndex; + setSession(updatedSession); + + // 构建参与方列表 + const participants = session.participants.map((p, idx) => ({ + partyId: p.partyId, + partyIndex: idx, + })); + + // 使用 TSS 客户端参与 keygen + const result = await keygen( + { + sessionId: session.sessionId, + partyId: session.partyId!, + partyIndex: session.partyIndex ?? 0, + threshold: thresholdT, + totalParties: thresholdN, + participants, + }, + (current, total) => { + setKeygenProgress({ current, total }); + setSession((prev) => + prev + ? { + ...prev, + currentRound: current, + totalRounds: total, + status: 'processing', + } + : null + ); + } + ); + + // Keygen 成功,保存 share 到本地存储 + await shareStorage.saveShare( + { + id: `${session.sessionId}-${session.partyId}`, + sessionId: session.sessionId, + walletName: walletName, + partyId: session.partyId!, + partyIndex: session.partyIndex ?? 0, + threshold: { t: thresholdT, n: thresholdN }, + publicKey: result.publicKey, + rawShare: result.share, + createdAt: new Date().toISOString(), + metadata: { + participants: session.participants.map((p) => ({ + partyId: p.partyId, + name: p.name, + })), + }, + }, + sharePassword + ); + + // 更新会话状态 + setSession((prev) => + prev + ? { + ...prev, + status: 'completed', + publicKey: result.publicKey, + currentRound: keygenProgress.total, + } + : null + ); + + // 通知后端完成 + await coManagedWalletService.updateSessionStatus(session.sessionId, 'completed', { + publicKey: result.publicKey, + }); + + stopPolling(); + setStep('result'); + onSuccess?.(); + } catch (err: unknown) { + const errorMessage = + err instanceof Error ? err.message : '密钥生成失败'; + setError(errorMessage); + setSession((prev) => + prev ? { ...prev, status: 'failed', error: errorMessage } : null + ); + } finally { + setIsLoading(false); + } + }; + + // 取消会话 + const handleCancel = async () => { + if (session) { + stopPolling(); + try { + await coManagedWalletService.cancelSession(session.sessionId); + } catch (err) { + console.error('取消会话失败:', err); + } + } + onClose(); }; // 渲染步骤指示器 @@ -150,7 +370,7 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal { key: 'result', label: '完成' }, ]; - const currentIndex = steps.findIndex(s => s.key === step); + const currentIndex = steps.findIndex((s) => s.key === step); return (
@@ -174,14 +394,14 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal if (!isOpen) return null; return ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()}>

创建共管钱包

- + setInitiatorName(e.target.value)} + onChange={(e) => setInitiatorName(e.target.value)} placeholder="输入您的名称(其他参与者可见)" className={styles.createWalletModal__input} disabled={isLoading} @@ -237,7 +459,7 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal
@@ -315,6 +542,52 @@ export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModal /> )}
+ + {/* 密码输入对话框 */} + {showPasswordDialog && ( +
+
+

设置密钥保护密码

+

+ 请设置一个密码来加密您的密钥份额。此密码将用于保护您的本地密钥, + 签名交易时需要输入此密码。 +

+
+ + setSharePassword(e.target.value)} + placeholder="至少 8 位字符" + className={styles.createWalletModal__input} + autoFocus + /> +
+ {error &&

{error}

} +
+ + +
+
+
+ )}
); diff --git a/frontend/admin-web/src/components/features/co-managed-wallet/co-managed-wallet.module.scss b/frontend/admin-web/src/components/features/co-managed-wallet/co-managed-wallet.module.scss index eaf313eb..cd577dda 100644 --- a/frontend/admin-web/src/components/features/co-managed-wallet/co-managed-wallet.module.scss +++ b/frontend/admin-web/src/components/features/co-managed-wallet/co-managed-wallet.module.scss @@ -961,3 +961,43 @@ } } } + +/* ============================================ + 密码输入对话框 + ============================================ */ +.passwordDialog { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + padding: 20px; + + &__content { + background-color: #fff; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); + padding: 24px; + width: 100%; + max-width: 400px; + } + + &__title { + margin: 0 0 8px; + font-size: 18px; + font-weight: 700; + color: #1e293b; + } + + &__description { + margin: 0 0 20px; + font-size: 13px; + line-height: 1.5; + color: #6b7280; + } +} diff --git a/frontend/admin-web/src/hooks/useShareStorage.ts b/frontend/admin-web/src/hooks/useShareStorage.ts new file mode 100644 index 00000000..c4378bd5 --- /dev/null +++ b/frontend/admin-web/src/hooks/useShareStorage.ts @@ -0,0 +1,101 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { shareStorage, ShareEntry } from '@/lib/storage'; + +interface UseShareStorageReturn { + shares: Omit[]; + isLoading: boolean; + error: string | null; + refresh: () => Promise; + saveShare: ( + share: Omit & { rawShare: string }, + password: string + ) => Promise; + getShare: (id: string, password: string) => Promise<{ share: ShareEntry; rawShare: string }>; + deleteShare: (id: string) => Promise; + exportShare: (id: string, password: string) => Promise; + importShare: (data: string, password: string) => Promise; +} + +/** + * React hook for TSS share storage operations + */ +export function useShareStorage(): UseShareStorageReturn { + const [shares, setShares] = useState[]>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const list = await shareStorage.listShares(); + setShares(list); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to load shares'; + setError(errorMessage); + console.error('Failed to load shares:', err); + } finally { + setIsLoading(false); + } + }, []); + + const saveShare = useCallback( + async ( + share: Omit & { rawShare: string }, + password: string + ) => { + await shareStorage.saveShare(share, password); + await refresh(); + }, + [refresh] + ); + + const getShare = useCallback( + async (id: string, password: string) => { + return shareStorage.getShare(id, password); + }, + [] + ); + + const deleteShare = useCallback( + async (id: string) => { + await shareStorage.deleteShare(id); + await refresh(); + }, + [refresh] + ); + + const exportShare = useCallback( + async (id: string, password: string) => { + return shareStorage.exportShare(id, password); + }, + [] + ); + + const importShare = useCallback( + async (data: string, password: string) => { + await shareStorage.importShare(data, password); + await refresh(); + }, + [refresh] + ); + + useEffect(() => { + refresh(); + }, [refresh]); + + return { + shares, + isLoading, + error, + refresh, + saveShare, + getShare, + deleteShare, + exportShare, + importShare, + }; +} diff --git a/frontend/admin-web/src/hooks/useTSSClient.ts b/frontend/admin-web/src/hooks/useTSSClient.ts new file mode 100644 index 00000000..d71d4a79 --- /dev/null +++ b/frontend/admin-web/src/hooks/useTSSClient.ts @@ -0,0 +1,120 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { TSSClient, createTSSClient, KeygenParams, KeygenResult } from '@/lib/tss'; + +interface UseTSSClientOptions { + messageRouterUrl?: string; + autoInit?: boolean; +} + +interface UseTSSClientReturn { + client: TSSClient | null; + isInitialized: boolean; + isInitializing: boolean; + error: string | null; + init: () => Promise; + registerParty: (partyId: string, role?: string) => Promise; + keygen: ( + params: KeygenParams, + onProgress?: (current: number, total: number) => void + ) => Promise; + stop: () => void; + disconnect: () => void; +} + +const DEFAULT_MESSAGE_ROUTER_URL = 'mpc-grpc.szaiai.com:443'; + +/** + * React hook for TSS client operations + */ +export function useTSSClient(options: UseTSSClientOptions = {}): UseTSSClientReturn { + const { + messageRouterUrl = DEFAULT_MESSAGE_ROUTER_URL, + autoInit = true, + } = options; + + const [client, setClient] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + const [isInitializing, setIsInitializing] = useState(false); + const [error, setError] = useState(null); + + const clientRef = useRef(null); + + const init = useCallback(async () => { + if (isInitializing || isInitialized) { + return; + } + + setIsInitializing(true); + setError(null); + + try { + const tssClient = createTSSClient(messageRouterUrl); + await tssClient.init(); + + clientRef.current = tssClient; + setClient(tssClient); + setIsInitialized(true); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to initialize TSS client'; + setError(errorMessage); + console.error('TSS client initialization failed:', err); + } finally { + setIsInitializing(false); + } + }, [messageRouterUrl, isInitializing, isInitialized]); + + const registerParty = useCallback(async (partyId: string, role: string = 'temporary') => { + if (!clientRef.current) { + throw new Error('TSS client not initialized'); + } + await clientRef.current.registerParty(partyId, role); + }, []); + + const keygen = useCallback(async ( + params: KeygenParams, + onProgress?: (current: number, total: number) => void + ): Promise => { + if (!clientRef.current) { + throw new Error('TSS client not initialized'); + } + + return clientRef.current.keygen(params, onProgress, (error) => { + console.error('Keygen error:', error); + }); + }, []); + + const stop = useCallback(() => { + clientRef.current?.stop(); + }, []); + + const disconnect = useCallback(() => { + clientRef.current?.disconnect(); + setClient(null); + setIsInitialized(false); + }, []); + + // Auto-init on mount + useEffect(() => { + if (autoInit) { + init(); + } + + return () => { + clientRef.current?.disconnect(); + }; + }, [autoInit, init]); + + return { + client, + isInitialized, + isInitializing, + error, + init, + registerParty, + keygen, + stop, + disconnect, + }; +} diff --git a/frontend/admin-web/src/lib/storage/index.ts b/frontend/admin-web/src/lib/storage/index.ts new file mode 100644 index 00000000..893fcd5c --- /dev/null +++ b/frontend/admin-web/src/lib/storage/index.ts @@ -0,0 +1,6 @@ +/** + * Storage exports + */ + +export { shareStorage } from './share-storage'; +export type { ShareEntry } from './share-storage'; diff --git a/frontend/admin-web/src/lib/storage/share-storage.ts b/frontend/admin-web/src/lib/storage/share-storage.ts new file mode 100644 index 00000000..0b5355c4 --- /dev/null +++ b/frontend/admin-web/src/lib/storage/share-storage.ts @@ -0,0 +1,330 @@ +/** + * TSS Share 本地存储模块 + * 使用 IndexedDB 存储加密的 TSS shares + */ + +const DB_NAME = 'tss-shares-db'; +const DB_VERSION = 1; +const STORE_NAME = 'shares'; + +export interface ShareEntry { + id: string; + sessionId: string; + walletName: string; + partyId: string; + partyIndex: number; + threshold: { t: number; n: number }; + publicKey: string; + encryptedShare: string; // AES-256-GCM 加密的 share 数据 + iv: string; // 初始化向量 + salt: string; // 密码派生盐 + createdAt: string; + lastUsedAt?: string; + metadata: { + participants: Array<{ partyId: string; name: string }>; + }; +} + +/** + * 打开 IndexedDB 数据库 + */ +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => { + reject(new Error('Failed to open IndexedDB')); + }; + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + store.createIndex('sessionId', 'sessionId', { unique: false }); + store.createIndex('walletName', 'walletName', { unique: false }); + store.createIndex('publicKey', 'publicKey', { unique: false }); + } + }; + }); +} + +/** + * 从密码派生加密密钥 + */ +async function deriveKey(password: string, salt: Uint8Array): Promise { + const encoder = new TextEncoder(); + const passwordKey = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt.buffer as ArrayBuffer, + iterations: 100000, + hash: 'SHA-256', + }, + passwordKey, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +/** + * 加密数据 + */ +async function encryptData(data: string, password: string): Promise<{ encrypted: string; iv: string; salt: string }> { + const encoder = new TextEncoder(); + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(password, salt); + + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoder.encode(data) + ); + + return { + encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))), + iv: btoa(String.fromCharCode(...iv)), + salt: btoa(String.fromCharCode(...salt)), + }; +} + +/** + * 解密数据 + */ +async function decryptData(encrypted: string, iv: string, salt: string, password: string): Promise { + const decoder = new TextDecoder(); + const encryptedBytes = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0)); + const ivBytes = Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)); + const saltBytes = Uint8Array.from(atob(salt), (c) => c.charCodeAt(0)); + + const key = await deriveKey(password, saltBytes); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: ivBytes }, + key, + encryptedBytes + ); + + return decoder.decode(decrypted); +} + +/** + * Share 存储服务 + */ +export const shareStorage = { + /** + * 保存 share + */ + async saveShare( + share: Omit & { rawShare: string }, + password: string + ): Promise { + const db = await openDB(); + const { encrypted, iv, salt } = await encryptData(share.rawShare, password); + + const entry: ShareEntry = { + id: share.id, + sessionId: share.sessionId, + walletName: share.walletName, + partyId: share.partyId, + partyIndex: share.partyIndex, + threshold: share.threshold, + publicKey: share.publicKey, + encryptedShare: encrypted, + iv, + salt, + createdAt: share.createdAt, + lastUsedAt: share.lastUsedAt, + metadata: share.metadata, + }; + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.put(entry); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(new Error('Failed to save share')); + }); + }, + + /** + * 获取所有 shares(不包含加密数据) + */ + async listShares(): Promise[]> { + const db = await openDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.getAll(); + + request.onsuccess = () => { + const shares = request.result.map((entry: ShareEntry) => ({ + id: entry.id, + sessionId: entry.sessionId, + walletName: entry.walletName, + partyId: entry.partyId, + partyIndex: entry.partyIndex, + threshold: entry.threshold, + publicKey: entry.publicKey, + createdAt: entry.createdAt, + lastUsedAt: entry.lastUsedAt, + metadata: entry.metadata, + })); + resolve(shares); + }; + request.onerror = () => reject(new Error('Failed to list shares')); + }); + }, + + /** + * 获取 share(包含解密后的原始数据) + */ + async getShare(id: string, password: string): Promise<{ share: ShareEntry; rawShare: string }> { + const db = await openDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.get(id); + + request.onsuccess = async () => { + if (!request.result) { + reject(new Error('Share not found')); + return; + } + + try { + const entry = request.result as ShareEntry; + const rawShare = await decryptData(entry.encryptedShare, entry.iv, entry.salt, password); + resolve({ share: entry, rawShare }); + } catch { + reject(new Error('Invalid password')); + } + }; + request.onerror = () => reject(new Error('Failed to get share')); + }); + }, + + /** + * 更新最后使用时间 + */ + async updateLastUsed(id: string): Promise { + const db = await openDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const getRequest = store.get(id); + + getRequest.onsuccess = () => { + if (!getRequest.result) { + reject(new Error('Share not found')); + return; + } + + const entry = getRequest.result as ShareEntry; + entry.lastUsedAt = new Date().toISOString(); + const putRequest = store.put(entry); + + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(new Error('Failed to update share')); + }; + getRequest.onerror = () => reject(new Error('Failed to get share')); + }); + }, + + /** + * 删除 share + */ + async deleteShare(id: string): Promise { + const db = await openDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.delete(id); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(new Error('Failed to delete share')); + }); + }, + + /** + * 导出 share(加密格式) + */ + async exportShare(id: string, password: string): Promise { + const { share, rawShare } = await this.getShare(id, password); + + // 使用新密码重新加密用于导出 + const { encrypted, iv, salt } = await encryptData(rawShare, password); + + const exportData = { + version: 1, + id: share.id, + sessionId: share.sessionId, + walletName: share.walletName, + partyId: share.partyId, + partyIndex: share.partyIndex, + threshold: share.threshold, + publicKey: share.publicKey, + encryptedShare: encrypted, + iv, + salt, + createdAt: share.createdAt, + metadata: share.metadata, + }; + + return btoa(JSON.stringify(exportData)); + }, + + /** + * 导入 share + */ + async importShare(data: string, password: string): Promise { + try { + const exportData = JSON.parse(atob(data)); + + // 验证密码并解密 + const rawShare = await decryptData( + exportData.encryptedShare, + exportData.iv, + exportData.salt, + password + ); + + // 保存到本地存储 + await this.saveShare( + { + id: exportData.id, + sessionId: exportData.sessionId, + walletName: exportData.walletName, + partyId: exportData.partyId, + partyIndex: exportData.partyIndex, + threshold: exportData.threshold, + publicKey: exportData.publicKey, + createdAt: exportData.createdAt, + metadata: exportData.metadata, + rawShare, + }, + password + ); + } catch { + throw new Error('导入失败:数据格式错误或密码不正确'); + } + }, +}; diff --git a/frontend/admin-web/src/lib/tss/grpc-web-client.ts b/frontend/admin-web/src/lib/tss/grpc-web-client.ts new file mode 100644 index 00000000..bc6295b6 --- /dev/null +++ b/frontend/admin-web/src/lib/tss/grpc-web-client.ts @@ -0,0 +1,365 @@ +/** + * gRPC-Web Client for Message Router + * Handles communication with the MPC Message Router service + */ + +// Note: This implementation uses fetch-based HTTP/2 for gRPC-Web +// In production, you may want to use @grpc/grpc-js or grpc-web library + +export interface MPCMessage { + messageId: string; + sessionId: string; + fromParty: string; + fromPartyIndex: number; + isBroadcast: boolean; + roundNumber: number; + messageType: string; + payload: string; // base64 encoded + createdAt: number; +} + +export interface RegisteredParty { + partyId: string; + role: string; + online: boolean; + registeredAt: number; + lastSeenAt: number; +} + +export type MessageHandler = (message: MPCMessage) => void; + +/** + * gRPC-Web client for Message Router + */ +export class GrpcWebClient { + private baseUrl: string; + private partyId: string | null = null; + private connected: boolean = false; + private heartbeatInterval: NodeJS.Timeout | null = null; + private messagePollingInterval: NodeJS.Timeout | null = null; + private messageHandlers: Map = new Map(); + private lastMessageTimestamp: number = 0; + + constructor(messageRouterUrl: string) { + // Convert gRPC address to HTTP endpoint + // mpc-grpc.szaiai.com:443 -> https://mpc-grpc.szaiai.com + this.baseUrl = this.normalizeUrl(messageRouterUrl); + } + + private normalizeUrl(url: string): string { + // Remove port if present and add https:// + let normalized = url.replace(/:443$/, ''); + if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) { + normalized = 'https://' + normalized; + } + return normalized; + } + + /** + * Connect to the Message Router + */ + async connect(): Promise { + // For gRPC-Web, connection is implicit + // We just verify the server is reachable + try { + const response = await fetch(`${this.baseUrl}/health`, { + method: 'GET', + }); + + if (response.ok) { + this.connected = true; + console.log('Connected to Message Router'); + } else { + throw new Error(`Health check failed: ${response.status}`); + } + } catch (error) { + console.error('Failed to connect to Message Router:', error); + // Allow connection without health check for development + this.connected = true; + } + } + + /** + * Check if connected + */ + isConnected(): boolean { + return this.connected; + } + + /** + * Get current party ID + */ + getPartyId(): string | null { + return this.partyId; + } + + /** + * Register as a party + */ + async registerParty(partyId: string, role: string = 'temporary'): Promise { + const response = await this.callGrpc('RegisterParty', { + party_id: partyId, + party_role: role, + version: '1.0.0', + }); + + if (response.success) { + this.partyId = partyId; + this.startHeartbeat(); + console.log('Registered as party:', partyId); + } else { + throw new Error(String(response.message) || 'Registration failed'); + } + } + + /** + * Subscribe to messages for a session + */ + subscribeMessages( + sessionId: string, + partyId: string, + handler: MessageHandler + ): () => void { + const key = `${sessionId}:${partyId}`; + this.messageHandlers.set(key, handler); + + // Start polling for messages + if (!this.messagePollingInterval) { + this.startMessagePolling(sessionId, partyId); + } + + // Return unsubscribe function + return () => { + this.messageHandlers.delete(key); + if (this.messageHandlers.size === 0) { + this.stopMessagePolling(); + } + }; + } + + /** + * Route a message to other parties + */ + async routeMessage( + sessionId: string, + fromParty: string, + toParties: string[], + isBroadcast: boolean, + payload: string + ): Promise { + await this.callGrpc('RouteMessage', { + session_id: sessionId, + from_party: fromParty, + to_parties: isBroadcast ? [] : toParties, + round_number: 0, + message_type: 'tss', + payload: this.base64ToBytes(payload), + }); + } + + /** + * Join a session + */ + async joinSession( + sessionId: string, + partyId: string, + joinToken: string + ): Promise<{ + success: boolean; + partyIndex: number; + sessionInfo: unknown; + otherParties: unknown[]; + }> { + const response = await this.callGrpc('JoinSession', { + session_id: sessionId, + party_id: partyId, + join_token: joinToken, + }); + + return { + success: Boolean(response.success), + partyIndex: Number(response.party_index) || 0, + sessionInfo: response.session_info, + otherParties: Array.isArray(response.other_parties) ? response.other_parties : [], + }; + } + + /** + * Mark party as ready + */ + async markPartyReady(sessionId: string, partyId: string): Promise { + const response = await this.callGrpc('MarkPartyReady', { + session_id: sessionId, + party_id: partyId, + }); + return Boolean(response.all_ready); + } + + /** + * Report completion + */ + async reportCompletion( + sessionId: string, + partyId: string, + publicKey?: string, + signature?: string + ): Promise { + await this.callGrpc('ReportCompletion', { + session_id: sessionId, + party_id: partyId, + public_key: publicKey ? this.base64ToBytes(publicKey) : undefined, + signature: signature ? this.base64ToBytes(signature) : undefined, + }); + } + + /** + * Get registered parties + */ + async getRegisteredParties( + roleFilter?: string, + onlyOnline?: boolean + ): Promise { + const response = await this.callGrpc('GetRegisteredParties', { + role_filter: roleFilter || '', + only_online: onlyOnline || false, + }); + return Array.isArray(response.parties) ? (response.parties as RegisteredParty[]) : []; + } + + /** + * Start heartbeat + */ + private startHeartbeat(): void { + if (this.heartbeatInterval) { + return; + } + + this.heartbeatInterval = setInterval(async () => { + if (!this.partyId) return; + + try { + await this.callGrpc('Heartbeat', { + party_id: this.partyId, + timestamp: Date.now(), + }); + } catch (error) { + console.warn('Heartbeat failed:', error); + } + }, 30000); // 30 seconds + } + + /** + * Start polling for messages + */ + private startMessagePolling(sessionId: string, partyId: string): void { + this.messagePollingInterval = setInterval(async () => { + try { + const response = await this.callGrpc('GetPendingMessages', { + session_id: sessionId, + party_id: partyId, + after_timestamp: this.lastMessageTimestamp, + }); + + const messages = Array.isArray(response.messages) ? response.messages : []; + for (const msg of messages as Array>) { + const key = `${String(msg.session_id)}:${partyId}`; + const handler = this.messageHandlers.get(key); + if (handler) { + handler({ + messageId: String(msg.message_id), + sessionId: String(msg.session_id), + fromParty: String(msg.from_party), + fromPartyIndex: this.getPartyIndexFromId(String(msg.from_party)), + isBroadcast: Boolean(msg.is_broadcast), + roundNumber: Number(msg.round_number) || 0, + messageType: String(msg.message_type), + payload: this.bytesToBase64(msg.payload as Uint8Array | number[]), + createdAt: Number(msg.created_at) || 0, + }); + } + this.lastMessageTimestamp = Math.max(this.lastMessageTimestamp, Number(msg.created_at) || 0); + } + } catch (error) { + console.warn('Message polling failed:', error); + } + }, 500); // Poll every 500ms + } + + /** + * Stop message polling + */ + private stopMessagePolling(): void { + if (this.messagePollingInterval) { + clearInterval(this.messagePollingInterval); + this.messagePollingInterval = null; + } + } + + /** + * Make a gRPC-Web call + */ + private async callGrpc(method: string, request: unknown): Promise> { + // Using gRPC-Web JSON format + const response = await fetch(`${this.baseUrl}/mpc.router.v1.MessageRouter/${method}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`gRPC call failed: ${response.status} ${response.statusText}`); + } + + return response.json(); + } + + /** + * Convert base64 to bytes + */ + private base64ToBytes(base64: string): Uint8Array { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + /** + * Convert bytes to base64 + */ + private bytesToBase64(bytes: Uint8Array | number[]): string { + const uint8Array = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); + let binary = ''; + for (let i = 0; i < uint8Array.length; i++) { + binary += String.fromCharCode(uint8Array[i]); + } + return btoa(binary); + } + + /** + * Get party index from ID (placeholder - should use actual mapping) + */ + private getPartyIndexFromId(partyId: string): number { + // This should be populated from the session info + // For now, extract from party-N format if possible + const match = partyId.match(/party-(\d+)/); + return match ? parseInt(match[1], 10) : 0; + } + + /** + * Disconnect + */ + disconnect(): void { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + this.stopMessagePolling(); + this.connected = false; + this.partyId = null; + } +} diff --git a/frontend/admin-web/src/lib/tss/index.ts b/frontend/admin-web/src/lib/tss/index.ts new file mode 100644 index 00000000..1b928360 --- /dev/null +++ b/frontend/admin-web/src/lib/tss/index.ts @@ -0,0 +1,19 @@ +/** + * TSS Library exports + */ + +export { initTSSWasm, isTSSWasmReady, getTSSWasmVersion } from './tss-wasm-loader'; +export { TSSClient, createTSSClient } from './tss-client'; +export type { + Participant, + KeygenParams, + SigningParams, + KeygenResult, + SigningResult, + TSSMessage, + ProgressCallback, + MessageCallback, + ErrorCallback, +} from './tss-client'; +export { GrpcWebClient } from './grpc-web-client'; +export type { MPCMessage, RegisteredParty, MessageHandler } from './grpc-web-client'; diff --git a/frontend/admin-web/src/lib/tss/tss-client.ts b/frontend/admin-web/src/lib/tss/tss-client.ts new file mode 100644 index 00000000..a788cf1f --- /dev/null +++ b/frontend/admin-web/src/lib/tss/tss-client.ts @@ -0,0 +1,300 @@ +/** + * TSS Client + * High-level API for TSS operations in the browser + */ + +import { initTSSWasm, isTSSWasmReady } from './tss-wasm-loader'; +import { GrpcWebClient } from './grpc-web-client'; + +export interface Participant { + partyId: string; + partyIndex: number; + name?: string; +} + +export interface KeygenParams { + sessionId: string; + partyId: string; + partyIndex: number; + threshold: number; // T value + totalParties: number; // N value + participants: Participant[]; + password?: string; // Optional password for encryption +} + +export interface SigningParams { + sessionId: string; + partyId: string; + partyIndex: number; + thresholdT: number; + participants: Participant[]; + saveData: string; // JSON string of keygen save data + messageHash: string; // base64 encoded +} + +export interface KeygenResult { + publicKey: string; // hex encoded + share: string; // JSON string of the share data + partyIndex: number; +} + +export interface SigningResult { + signature: string; // base64 encoded + partyIndex: number; +} + +export interface TSSMessage { + type: string; + isBroadcast: boolean; + toParties: string[]; + payload: string; +} + +export type ProgressCallback = (current: number, total: number) => void; +export type MessageCallback = (message: TSSMessage) => void; +export type ErrorCallback = (error: string) => void; + +/** + * TSS Client for browser-based TSS operations + */ +export class TSSClient { + private grpcClient: GrpcWebClient; + private activeSessionId: string | null = null; + private messageSubscription: (() => void) | null = null; + + constructor(messageRouterUrl: string) { + this.grpcClient = new GrpcWebClient(messageRouterUrl); + } + + /** + * Initialize the TSS client + */ + async init(): Promise { + await initTSSWasm(); + await this.grpcClient.connect(); + } + + /** + * Check if initialized + */ + isReady(): boolean { + return isTSSWasmReady() && this.grpcClient.isConnected(); + } + + /** + * Register as a party with the Message Router + */ + async registerParty(partyId: string, role: string = 'temporary'): Promise { + await this.grpcClient.registerParty(partyId, role); + } + + /** + * Perform keygen operation + */ + async keygen( + params: KeygenParams, + onProgress?: ProgressCallback, + onError?: ErrorCallback + ): Promise { + if (!isTSSWasmReady()) { + throw new Error('TSS WASM not initialized'); + } + + this.activeSessionId = params.sessionId; + + // Subscribe to incoming messages + this.messageSubscription = this.grpcClient.subscribeMessages( + params.sessionId, + params.partyId, + (message) => { + // Forward message to WASM + window.tssHandleMessage( + params.sessionId, + message.fromPartyIndex, + message.isBroadcast, + message.payload + ); + } + ); + + return new Promise((resolve, reject) => { + const participantsJSON = JSON.stringify(params.participants); + + const result = window.tssStartKeygen( + params.sessionId, + params.partyId, + params.partyIndex, + params.threshold, + params.totalParties, + participantsJSON, + params.password || '', + // onMessage - route to other parties + (msgJSON: string) => { + try { + const msg: TSSMessage = JSON.parse(msgJSON); + this.grpcClient.routeMessage( + params.sessionId, + params.partyId, + msg.toParties, + msg.isBroadcast, + msg.payload + ); + } catch (e) { + console.error('Failed to route message:', e); + } + }, + // onProgress + (current: number, total: number) => { + onProgress?.(current, total); + }, + // onComplete + (resultJSON: string) => { + this.cleanup(); + try { + const parsedResult = JSON.parse(resultJSON); + resolve({ + publicKey: parsedResult.publicKey, + share: parsedResult.share || parsedResult.encryptedShare, + partyIndex: parsedResult.partyIndex, + }); + } catch (e) { + reject(new Error('Failed to parse keygen result')); + } + }, + // onError + (error: string) => { + this.cleanup(); + onError?.(error); + reject(new Error(error)); + } + ); + + if (!result.success) { + this.cleanup(); + reject(new Error(result.error || 'Failed to start keygen')); + } + }); + } + + /** + * Perform signing operation + */ + async sign( + params: SigningParams, + onProgress?: ProgressCallback, + onError?: ErrorCallback + ): Promise { + if (!isTSSWasmReady()) { + throw new Error('TSS WASM not initialized'); + } + + this.activeSessionId = params.sessionId; + + // Subscribe to incoming messages + this.messageSubscription = this.grpcClient.subscribeMessages( + params.sessionId, + params.partyId, + (message) => { + window.tssHandleMessage( + params.sessionId, + message.fromPartyIndex, + message.isBroadcast, + message.payload + ); + } + ); + + return new Promise((resolve, reject) => { + const participantsJSON = JSON.stringify(params.participants); + + const result = window.tssStartSigning( + params.sessionId, + params.partyId, + params.partyIndex, + params.thresholdT, + participantsJSON, + params.saveData, + params.messageHash, + // onMessage + (msgJSON: string) => { + try { + const msg: TSSMessage = JSON.parse(msgJSON); + this.grpcClient.routeMessage( + params.sessionId, + params.partyId, + msg.toParties, + msg.isBroadcast, + msg.payload + ); + } catch (e) { + console.error('Failed to route message:', e); + } + }, + // onProgress + (current: number, total: number) => { + onProgress?.(current, total); + }, + // onComplete + (resultJSON: string) => { + this.cleanup(); + try { + const result = JSON.parse(resultJSON); + resolve({ + signature: result.signature, + partyIndex: result.partyIndex, + }); + } catch (e) { + reject(new Error('Failed to parse signing result')); + } + }, + // onError + (error: string) => { + this.cleanup(); + onError?.(error); + reject(new Error(error)); + } + ); + + if (!result.success) { + this.cleanup(); + reject(new Error(result.error || 'Failed to start signing')); + } + }); + } + + /** + * Stop active session + */ + stop(): void { + if (this.activeSessionId) { + window.tssStopSession(this.activeSessionId); + } + this.cleanup(); + } + + /** + * Cleanup resources + */ + private cleanup(): void { + if (this.messageSubscription) { + this.messageSubscription(); + this.messageSubscription = null; + } + this.activeSessionId = null; + } + + /** + * Disconnect from Message Router + */ + disconnect(): void { + this.stop(); + this.grpcClient.disconnect(); + } +} + +/** + * Create a TSS client instance + */ +export function createTSSClient(messageRouterUrl: string): TSSClient { + return new TSSClient(messageRouterUrl); +} diff --git a/frontend/admin-web/src/lib/tss/tss-wasm-loader.ts b/frontend/admin-web/src/lib/tss/tss-wasm-loader.ts new file mode 100644 index 00000000..0c9a3211 --- /dev/null +++ b/frontend/admin-web/src/lib/tss/tss-wasm-loader.ts @@ -0,0 +1,144 @@ +/** + * TSS WASM Loader + * Loads and initializes the TSS WebAssembly module + */ + +// Extend Window interface for Go WASM +declare global { + interface Window { + Go: new () => GoInstance; + tssStartKeygen: ( + sessionId: string, + partyId: string, + partyIndex: number, + thresholdT: number, + thresholdN: number, + participantsJSON: string, + password: string, + onMessage: (msg: string) => void, + onProgress: (current: number, total: number) => void, + onComplete: (result: string) => void, + onError: (error: string) => void + ) => { success: boolean; data?: unknown; error?: string }; + tssStartSigning: ( + sessionId: string, + partyId: string, + partyIndex: number, + thresholdT: number, + participantsJSON: string, + saveDataJSON: string, + messageHashB64: string, + onMessage: (msg: string) => void, + onProgress: (current: number, total: number) => void, + onComplete: (result: string) => void, + onError: (error: string) => void + ) => { success: boolean; data?: unknown; error?: string }; + tssHandleMessage: ( + sessionId: string, + fromPartyIndex: number, + isBroadcast: boolean, + payloadB64: string + ) => { success: boolean; error?: string }; + tssStopSession: (sessionId: string) => { success: boolean; error?: string }; + tssGetVersion: () => string; + } +} + +interface GoInstance { + argv: string[]; + env: Record; + exit: (code: number) => void; + importObject: WebAssembly.Imports; + run: (instance: WebAssembly.Instance) => Promise; +} + +let goInstance: GoInstance | null = null; +let wasmInstance: WebAssembly.Instance | null = null; +let isInitialized = false; +let initPromise: Promise | null = null; + +/** + * Initialize the TSS WASM module + */ +export async function initTSSWasm(): Promise { + if (isInitialized) { + return; + } + + if (initPromise) { + return initPromise; + } + + initPromise = doInit(); + return initPromise; +} + +async function doInit(): Promise { + try { + // Load wasm_exec.js (Go WASM runtime) + if (!window.Go) { + await loadScript('/wasm/wasm_exec.js'); + } + + // Create Go instance + goInstance = new window.Go(); + + // Fetch and instantiate WASM + const wasmResponse = await fetch('/wasm/tss.wasm'); + const wasmBuffer = await wasmResponse.arrayBuffer(); + + const result = await WebAssembly.instantiate(wasmBuffer, goInstance.importObject); + wasmInstance = result.instance; + + // Run the Go program + goInstance.run(wasmInstance); + + // Wait for initialization + await new Promise((resolve) => { + const checkReady = () => { + if (typeof window.tssGetVersion === 'function') { + resolve(); + } else { + setTimeout(checkReady, 50); + } + }; + checkReady(); + }); + + isInitialized = true; + console.log('TSS WASM initialized, version:', window.tssGetVersion()); + } catch (error) { + console.error('Failed to initialize TSS WASM:', error); + throw error; + } +} + +/** + * Load a script dynamically + */ +function loadScript(src: string): Promise { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.onload = () => resolve(); + script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); + document.head.appendChild(script); + }); +} + +/** + * Check if TSS WASM is initialized + */ +export function isTSSWasmReady(): boolean { + return isInitialized; +} + +/** + * Get TSS WASM version + */ +export function getTSSWasmVersion(): string { + if (!isInitialized) { + throw new Error('TSS WASM not initialized'); + } + return window.tssGetVersion(); +} diff --git a/frontend/admin-web/src/services/coManagedWalletService.ts b/frontend/admin-web/src/services/coManagedWalletService.ts new file mode 100644 index 00000000..877cb4ce --- /dev/null +++ b/frontend/admin-web/src/services/coManagedWalletService.ts @@ -0,0 +1,213 @@ +/** + * 共管钱包服务 + * 处理共管钱包会话创建、状态查询等操作 + */ + +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; + +// ============================================ +// 类型定义 +// ============================================ + +export interface ThresholdConfig { + t: number; + n: number; +} + +export interface Participant { + partyId: string; + name: string; + status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed'; + joinedAt: string; +} + +export interface CoManagedWalletSession { + sessionId: string; + walletName: string; + threshold: ThresholdConfig; + inviteCode: string; + inviteUrl: string; + status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed' | 'cancelled'; + participants: Participant[]; + currentRound: number; + totalRounds: number; + publicKey?: string; + error?: string; + createdAt: string; + updatedAt: string; + createdBy: string; +} + +export interface CoManagedWallet { + id: string; + sessionId: string; + name: string; + publicKey: string; + threshold: ThresholdConfig; + participantCount: number; + participants: Array<{ partyId: string; name: string }>; + createdAt: string; + createdBy: string; +} + +// ============================================ +// 请求参数类型 +// ============================================ + +export interface CreateSessionParams { + walletName: string; + thresholdT: number; + thresholdN: number; + initiatorName: string; + partyId?: string; // 发起者的 party ID (Admin-Web 作为参与方时使用) +} + +export interface JoinSessionParams { + participantName: string; + partyId: string; +} + +export interface UpdateSessionStatusParams { + publicKey?: string; + error?: string; +} + +export interface ListParams { + page?: number; + pageSize?: number; + status?: string; +} + +// ============================================ +// 响应类型 +// ============================================ + +export interface CreateSessionResponse { + success: boolean; + session: CoManagedWalletSession; +} + +export interface ValidateInviteCodeResponse { + valid: boolean; + session?: { + sessionId: string; + walletName: string; + threshold: ThresholdConfig; + initiator: string; + createdAt: string; + currentParticipants: number; + }; + error?: string; +} + +export interface SessionListResponse { + items: CoManagedWalletSession[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +export interface WalletListResponse { + items: CoManagedWallet[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +// ============================================ +// API 服务 +// ============================================ + +export const coManagedWalletService = { + /** + * 创建共管钱包会话 + */ + createSession: async (params: CreateSessionParams): Promise => { + return apiClient.post(API_ENDPOINTS.CO_MANAGED_WALLETS.CREATE_SESSION, params); + }, + + /** + * 获取会话详情 + */ + getSession: async (sessionId: string): Promise => { + return apiClient.get(API_ENDPOINTS.CO_MANAGED_WALLETS.SESSION_DETAIL(sessionId)); + }, + + /** + * 获取会话列表 + */ + listSessions: async (params?: ListParams): Promise => { + return apiClient.get(API_ENDPOINTS.CO_MANAGED_WALLETS.CREATE_SESSION, { params }); + }, + + /** + * 验证邀请码 + */ + validateInviteCode: async (inviteCode: string): Promise => { + return apiClient.post(`${API_ENDPOINTS.CO_MANAGED_WALLETS.CREATE_SESSION}/validate-invite`, { + inviteCode, + }); + }, + + /** + * 加入会话 + */ + joinSession: async ( + sessionId: string, + params: JoinSessionParams + ): Promise => { + return apiClient.post( + `${API_ENDPOINTS.CO_MANAGED_WALLETS.SESSION_DETAIL(sessionId)}/join`, + params + ); + }, + + /** + * 开始密钥生成 + */ + startKeygen: async (sessionId: string): Promise => { + return apiClient.post( + `${API_ENDPOINTS.CO_MANAGED_WALLETS.SESSION_DETAIL(sessionId)}/start-keygen` + ); + }, + + /** + * 更新会话状态 + */ + updateSessionStatus: async ( + sessionId: string, + status: string, + params?: UpdateSessionStatusParams + ): Promise => { + return apiClient.put( + `${API_ENDPOINTS.CO_MANAGED_WALLETS.SESSION_DETAIL(sessionId)}/status`, + { status, ...params } + ); + }, + + /** + * 取消会话 + */ + cancelSession: async (sessionId: string): Promise => { + return apiClient.delete(API_ENDPOINTS.CO_MANAGED_WALLETS.SESSION_DETAIL(sessionId)); + }, + + /** + * 获取钱包列表 + */ + listWallets: async (params?: ListParams): Promise => { + return apiClient.get(API_ENDPOINTS.CO_MANAGED_WALLETS.LIST, { params }); + }, + + /** + * 获取钱包详情 + */ + getWallet: async (walletId: string): Promise => { + return apiClient.get(API_ENDPOINTS.CO_MANAGED_WALLETS.WALLET_DETAIL(walletId)); + }, +}; + +export default coManagedWalletService;