diff --git a/go.mod b/go.mod index ab1e1b9..20c6278 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,7 @@ require ( github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect github.com/creachadair/taskgroup v0.4.2 // indirect + github.com/creack/goselect v0.1.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect @@ -165,6 +166,7 @@ require ( github.com/ulikunitz/xz v0.5.11 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect + go.bug.st/serial v1.6.2 // indirect go.etcd.io/bbolt v1.3.7 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.19.0 // indirect diff --git a/go.sum b/go.sum index cb5fe99..bd62544 100644 --- a/go.sum +++ b/go.sum @@ -371,6 +371,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/taskgroup v0.4.2 h1:jsBLdAJE42asreGss2xZGZ8fJra7WtwnHWeJFxv2Li8= github.com/creachadair/taskgroup v0.4.2/go.mod h1:qiXUOSrbwAY3u0JPGTzObbE3yf9hcXHDKBZ2ZjpCbgM= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= @@ -1002,6 +1004,8 @@ github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= diff --git a/lib/README.md b/lib/README.md index 63780d0..543c976 100644 --- a/lib/README.md +++ b/lib/README.md @@ -9,6 +9,10 @@ After that we construct three messages to send `10plmnt` each to three addresses We then build and sign the transaction and eventually send this transaction via RPC. For debugging purposes we print the transaction that we send as JSON. +## Support for Trust Wallet over serial connection + +The config has a setting for a `serialPort`. If it is set then `BroadcastTxWithFileLock()` will sign transactions with the TrustWallet. Be aware that the public key must be available on the keyring in order to construct transactions signing data. + ``` package main diff --git a/lib/config.go b/lib/config.go index f62e9ca..9898fcb 100644 --- a/lib/config.go +++ b/lib/config.go @@ -17,6 +17,7 @@ type Config struct { rootDir string rpcEndpoint string txGas uint64 + serialPort string } // lib wide global singleton @@ -37,6 +38,7 @@ func DefaultConfig() *Config { rootDir: "~/.planetmint-go/", rpcEndpoint: "http://127.0.0.1:26657", txGas: 200000, + serialPort: "", } } @@ -116,3 +118,10 @@ func (config *Config) SetTxGas(txGas uint64) *Config { config.txGas = txGas return config } + +func (config *Config) SetSerialPort(port string) *Config { + changeLock.Lock() + defer changeLock.Unlock() + config.serialPort = port + return config +} diff --git a/lib/go.mod b/lib/go.mod index d83e060..a043792 100644 --- a/lib/go.mod +++ b/lib/go.mod @@ -7,6 +7,7 @@ require ( github.com/cosmos/cosmos-sdk v0.47.8 github.com/planetmint/planetmint-go v0.7.2 github.com/stretchr/testify v1.8.4 + go.bug.st/serial v1.6.2 sigs.k8s.io/yaml v1.3.0 ) @@ -58,6 +59,7 @@ require ( github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect github.com/creachadair/taskgroup v0.4.2 // indirect + github.com/creack/goselect v0.1.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect diff --git a/lib/go.sum b/lib/go.sum index 89626f4..ab4ca4c 100644 --- a/lib/go.sum +++ b/lib/go.sum @@ -372,6 +372,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/taskgroup v0.4.2 h1:jsBLdAJE42asreGss2xZGZ8fJra7WtwnHWeJFxv2Li8= github.com/creachadair/taskgroup v0.4.2/go.mod h1:qiXUOSrbwAY3u0JPGTzObbE3yf9hcXHDKBZ2ZjpCbgM= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= @@ -1013,6 +1015,8 @@ github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= diff --git a/lib/tests/e2e/suite.go b/lib/tests/e2e/suite.go index 1a13545..58f8dbf 100644 --- a/lib/tests/e2e/suite.go +++ b/lib/tests/e2e/suite.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/planetmint/planetmint-go/lib" + "github.com/planetmint/planetmint-go/lib/trustwallet" clitestutil "github.com/planetmint/planetmint-go/testutil/cli" "github.com/planetmint/planetmint-go/testutil/network" "github.com/planetmint/planetmint-go/testutil/sample" @@ -86,3 +87,54 @@ func (s *E2ETestSuite) TestBankSendBroadcastTxWithFileLock() { s.Require().NoError(err) assert.Equal(s.T(), "[]", txResponse.RawLog) } + +func (s *E2ETestSuite) TestLoadKeys() { + s.T().SkipNow() + _, err := setKeys() + if err == nil { + _, err = loadKeys() + s.Require().NoError(err) + } + s.Require().NoError(s.network.WaitForNextBlock()) +} + +func (s *E2ETestSuite) TestOccSigning() { + s.T().SkipNow() + val := s.network.Validators[0] + + keys, err := loadKeys() + s.Require().NoError(err) + + addr := sdk.MustAccAddressFromBech32(keys.PlanetmintAddress) + + coin := sdk.NewCoins(sdk.NewInt64Coin("stake", 10)) + msg := banktypes.NewMsgSend(addr, val.Address, coin) + + libConfig := lib.GetConfig() + libConfig.SetSerialPort("/dev/ttyACM0") + + out, err := lib.BroadcastTxWithFileLock(addr, msg) + s.Require().NoError(err) + + txResponse, err := lib.GetTxResponseFromOut(out) + s.Require().NoError(err) + s.Require().Equal("[]", txResponse.RawLog) + s.Require().Equal(uint32(0), txResponse.Code) +} + +// set sample mnemonic on trust wallet +func setKeys() (string, error) { + connector, err := trustwallet.NewTrustWalletConnector("/dev/ttyACM0") + if err != nil { + return "", err + } + return connector.RecoverFromMnemonic(sample.Mnemonic) +} + +func loadKeys() (*trustwallet.PlanetMintKeys, error) { + connector, err := trustwallet.NewTrustWalletConnector("/dev/ttyACM0") + if err != nil { + return nil, err + } + return connector.GetPlanetmintKeys() +} diff --git a/lib/trustwallet/occ.go b/lib/trustwallet/occ.go new file mode 100644 index 0000000..319d8dd --- /dev/null +++ b/lib/trustwallet/occ.go @@ -0,0 +1,147 @@ +package trustwallet + +import ( + "errors" + "fmt" + "io" + "time" + + "go.bug.st/serial" +) + +const ( + SlipEnd = 0xC0 + SlipEsc = 0xDB + SlipEscEnd = 0xDC + SlipEscEsc = 0xDD + nsPerUs = 1000 + nsPerMs = 8000 * nsPerUs +) + +// occDo performs the operations to send and receive data over serial port. +func occDo(data []byte, bufferDelayMs int, portName string, outBuffer []byte) (int, error) { + // Initialize unencoded and encoded payloads + payloadUnencoded := make([]byte, 0, len(data)) + payloadSlipEncoded := make([]byte, 0, len(data)*2) + + // Copy data to payloadUnencoded + payloadUnencoded = append(payloadUnencoded, data...) + + // Open serial port + mode := &serial.Mode{BaudRate: 115200} + s, err := serial.Open(portName, mode) + if err != nil { + return 0, fmt.Errorf("unable to open serial port: %w", err) + } + defer s.Close() + + // Encode payload using SLIP + encodeSLIP(payloadUnencoded, &payloadSlipEncoded) + + // Send encoded payload over serial + if _, err := s.Write(payloadSlipEncoded); err != nil { + return 0, fmt.Errorf("unable to write to serial port: %w", err) + } + + time.Sleep(time.Duration(bufferDelayMs) * time.Millisecond) + + // Read response from serial + readBuffer := make([]byte, 1) + encodedResponse := make([]byte, 0) + + slipMsgFramer := 0 + + for { + n, err := s.Read(readBuffer) + if err != nil && !errors.Is(err, io.EOF) { + time.Sleep(10 * time.Millisecond) + continue + } + if n == 0 { + break + } + + encodedResponse = append(encodedResponse, readBuffer[0]) + + if readBuffer[0] == SlipEnd { + if slipMsgFramer == 1 { + break + } + slipMsgFramer++ + } + time.Sleep(1 * time.Millisecond) + } + + // Decode SLIP response + decodedResponse, err := decodeSLIP(encodedResponse) + if err != nil { + return 0, fmt.Errorf("unable to decode SLIP: %w", err) + } + + // Copy decoded response to outBuffer + copyLength := min(len(decodedResponse), len(outBuffer)) + copy(outBuffer, decodedResponse[:copyLength]) + + return copyLength, nil +} + +// encodeSLIP encodes data using SLIP protocol. +func encodeSLIP(data []byte, encoded *[]byte) { + *encoded = append(*encoded, SlipEnd) + for _, b := range data { + switch b { + case SlipEnd: + *encoded = append(*encoded, SlipEsc, SlipEscEnd) + case SlipEsc: + *encoded = append(*encoded, SlipEsc, SlipEscEsc) + default: + *encoded = append(*encoded, b) + } + } + *encoded = append(*encoded, SlipEnd) +} + +// decodeSLIP decodes SLIP-encoded data. +func decodeSLIP(encoded []byte) ([]byte, error) { + // Check for empty input + if len(encoded) == 0 { + return nil, errors.New("encoded data is empty") + } + + // Remove first and last SLIP_END bytes + if encoded[0] == SlipEnd { + encoded = encoded[1:] + } + if encoded[len(encoded)-1] == SlipEnd { + encoded = encoded[:len(encoded)-1] + } + + decoded := make([]byte, 0, len(encoded)) + esc := false + + for _, b := range encoded { + switch { + case b == SlipEsc && !esc: + esc = true + case b == SlipEscEnd && esc: + decoded = append(decoded, SlipEnd) + esc = false + case b == SlipEscEsc && esc: + decoded = append(decoded, SlipEsc) + esc = false + default: + decoded = append(decoded, b) + esc = false + } + } + + return decoded, nil +} + +// min returns the smaller of two integers. +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/lib/trustwallet/osc_message_sender.go b/lib/trustwallet/osc_message_sender.go new file mode 100644 index 0000000..a4a6251 --- /dev/null +++ b/lib/trustwallet/osc_message_sender.go @@ -0,0 +1,88 @@ +package trustwallet + +import ( + "bytes" + "errors" + "fmt" + "strings" +) + +const ( + PrefixIhw = "/IHW" + BufferSize = 1024 + BufferDelayMs = 200 +) + +type OSCResponse struct { + Command string + Data []string +} + +type OSCMessageSender struct { + portName []byte + bufferSize int + bufferDelayMs int +} + +func NewOSCMessageSender(portName string) (*OSCMessageSender, error) { + return &OSCMessageSender{ + portName: []byte(portName), + bufferSize: BufferSize, + bufferDelayMs: BufferDelayMs, + }, nil +} + +func (s *OSCMessageSender) SendMessage(message []byte) (OSCResponse, error) { + outputBuffer := make([]byte, s.bufferSize) + + // Call occDo function + outputLength, err := occDo( + message, + s.bufferDelayMs, + string(s.portName), + outputBuffer, + ) + + if err != nil { + return OSCResponse{}, fmt.Errorf("failed to send message: %w", err) + } + + if outputLength == 0 { + return OSCResponse{}, errors.New("no response received") + } + + // Extract the information from the output buffer + return extractInformation(outputBuffer[:outputLength]) +} + +func extractInformation(responseBytes []byte) (OSCResponse, error) { + decodedString := string(bytes.Trim(responseBytes, "\x00")) + parts := strings.Split(decodedString, "\x00") + + var response OSCResponse + if len(parts) > 0 { + commandPart := parts[0] + dataParts := parts[1:] + + if strings.Contains(commandPart, ",") { + splitCmd := strings.SplitN(commandPart, ",", 2) + response.Command = strings.TrimSpace(splitCmd[0]) + dataParts = append([]string{splitCmd[1]}, dataParts...) + } else { + response.Command = strings.TrimSpace(commandPart) + } + + response.Data = make([]string, 0, len(dataParts)) + for _, part := range dataParts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + response.Data = append(response.Data, trimmed) + } + } + } + + if len(response.Data) == 0 { + response.Data = []string{"No valid data found."} + } + + return response, nil +} diff --git a/lib/trustwallet/trustwallet.go b/lib/trustwallet/trustwallet.go new file mode 100644 index 0000000..9d77ac6 --- /dev/null +++ b/lib/trustwallet/trustwallet.go @@ -0,0 +1,284 @@ +package trustwallet + +import ( + "bytes" + "encoding/binary" + "errors" + "strconv" + "sync" +) + +var ( + keys *PlanetMintKeys +) + +type Connector struct { + oscSender *OSCMessageSender + mu sync.Mutex +} + +func NewTrustWalletConnector(portName string) (*Connector, error) { + sender, err := NewOSCMessageSender(portName) + if err != nil { + return nil, err + } + return &Connector{ + oscSender: sender, + }, nil +} + +func (t *Connector) sendOSCMessage(address string, args ...interface{}) (OSCResponse, error) { + t.mu.Lock() + defer t.mu.Unlock() + + message, err := encodeOSCMessage(address, args...) + if err != nil { + return OSCResponse{}, err + } + + return t.oscSender.SendMessage(message) +} + +func (t *Connector) ValiseGet() (string, error) { + response, err := t.sendOSCMessage(PrefixIhw + "/getSeed") + if err != nil { + return "", err + } + if len(response.Data) > 0 { + return response.Data[0], nil + } + return "", errors.New("no data returned") +} + +func (t *Connector) CreateMnemonic() (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/mnemonicToSeed", int32(1)) + if err != nil { + return "", err + } + if len(response.Data) > 0 { + return response.Data[0], nil + } + return "", errors.New("no data returned") +} + +func (t *Connector) InjectPlanetminkeyToSE050(slot int) (bool, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/se050InjectSECPKeys", int32(slot)) + if err != nil { + return false, err + } + if len(response.Data) > 0 { + return response.Data[0] == "0", nil + } + return false, errors.New("no data returned") +} + +func (t *Connector) RecoverFromMnemonic(mnemonic string) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/mnemonicToSeed", int32(1), mnemonic) + if err != nil { + return "", err + } + if len(response.Data) > 0 { + return response.Data[0], nil + } + return "", errors.New("no data returned") +} + +func (t *Connector) GetPlanetmintKeys() (*PlanetMintKeys, error) { + if keys == nil { + response, err := t.sendOSCMessage(PrefixIhw + "/getPlntmntKeys") + if err != nil { + return nil, err + } + if len(response.Data) < 4 { + return nil, errors.New("trust wallet not initialized. Please initialize the wallet") + } + + keys = &PlanetMintKeys{ + PlanetmintAddress: response.Data[1], + ExtendedLiquidPubkey: response.Data[2], + ExtendedPlanetmintPubkey: response.Data[3], + RawPlanetmintPubkey: response.Data[4], + } + } + return keys, nil +} + +func (t *Connector) GetSeedSE050() (string, error) { + response, err := t.sendOSCMessage(PrefixIhw + "/se050GetSeed") + if err != nil { + return "", err + } + if len(response.Data) > 0 { + return response.Data[0], nil + } + return "", errors.New("no data returned") +} + +func (t *Connector) SignHashWithPlanetmint(dataToSign string) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/ecdsaSignPlmnt", dataToSign) + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no signature returned") +} + +func (t *Connector) SignHashWithRDDL(dataToSign string) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/ecdsaSignRddl", dataToSign) + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no signature returned") +} + +func (t *Connector) CreateOptegaKeypair(ctx int) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/optigaTrustXCreateSecret", int32(ctx), "") + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no public key returned") +} + +func (t *Connector) SignWithOptega(ctx int, dataToSign, pubkey string) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/optigaTrustXSignMessage", int32(ctx), dataToSign, pubkey, "") + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no signature returned") +} + +func (t *Connector) UnwrapPublicKey(publicKey string) (bool, string) { + length := len(publicKey) + if length == 136 || length == 130 { + return true, publicKey[len(publicKey)-128:] + } else if length == 128 { + return true, publicKey + } + return false, publicKey +} + +func (t *Connector) CalculateHash(dataToSign string) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/se050CalculateHash", dataToSign) + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no hash returned") +} + +func (t *Connector) CreateSE050KeypairNIST(ctx int) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/se050CreateKeyPair", int32(ctx), int32(1)) + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no public key returned") +} + +func (t *Connector) GetPublicKeyFromSE050(ctx int) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/se050GetPublicKey", int32(ctx)) + if err != nil { + return "", err + } + if len(response.Data) > 1 { + valid, pubKey := t.UnwrapPublicKey(response.Data[1]) + if !valid { + return "", errors.New("inject PlanetMintKey failed: No key found") + } + return pubKey, nil + } + return "", errors.New("no public key returned") +} + +func (t *Connector) SignWithSE050(dataToSign string, ctx int) (string, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/se050SignData", dataToSign, int32(ctx)) + if err != nil { + return "", err + } + if len(response.Data) > 1 { + return response.Data[1], nil + } + return "", errors.New("no signature returned") +} + +func (t *Connector) VerifySE050Signature(dataToSign, signature string, ctx int) (bool, error) { + response, err := t.sendOSCMessage(PrefixIhw+"/se050VerifySignature", dataToSign, signature, int32(ctx)) + if err != nil { + return false, err + } + if len(response.Data) > 1 { + return strconv.ParseBool(response.Data[1]) + } + return false, errors.New("no verification result returned") +} + +func encodeOSCMessage(address string, args ...interface{}) (returnBytes []byte, err error) { + var buffer bytes.Buffer + + // Write address + buffer.WriteString(address) + buffer.WriteByte(0) + alignBuffer(&buffer) + + // Write type tags + buffer.WriteByte(',') + for _, arg := range args { + switch arg.(type) { + case int32: + err = buffer.WriteByte('i') + case float32: + err = buffer.WriteByte('f') + case string: + err = buffer.WriteByte('s') + } + if err != nil { + return buffer.Bytes(), err + } + } + + buffer.WriteByte(0) + alignBuffer(&buffer) + + // Write arguments + for _, arg := range args { + switch v := arg.(type) { + case int32: + err = binary.Write(&buffer, binary.BigEndian, v) + case float32: + err = binary.Write(&buffer, binary.BigEndian, v) + case string: + _, err = buffer.WriteString(v) + if err != nil { + return buffer.Bytes(), err + } + err = buffer.WriteByte(0) + alignBuffer(&buffer) + } + if err != nil { + return buffer.Bytes(), err + } + } + + return buffer.Bytes(), nil +} + +func alignBuffer(buffer *bytes.Buffer) { + for buffer.Len()%4 != 0 { + buffer.WriteByte(0) + } +} diff --git a/lib/trustwallet/types.go b/lib/trustwallet/types.go new file mode 100644 index 0000000..24c1f7c --- /dev/null +++ b/lib/trustwallet/types.go @@ -0,0 +1,8 @@ +package trustwallet + +type PlanetMintKeys struct { + PlanetmintAddress string + ExtendedLiquidPubkey string + ExtendedPlanetmintPubkey string + RawPlanetmintPubkey string +} diff --git a/lib/tx.go b/lib/tx.go index 55d468d..32d2574 100644 --- a/lib/tx.go +++ b/lib/tx.go @@ -2,20 +2,27 @@ package lib import ( "bytes" + "encoding/hex" "encoding/json" "errors" + "fmt" "io" "os" "strconv" "sync" "syscall" + "github.com/cometbft/cometbft/crypto" comethttp "github.com/cometbft/cometbft/rpc/client/http" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/planetmint/planetmint-go/lib/trustwallet" ) var ( @@ -168,25 +175,7 @@ func broadcastTx(clientCtx client.Context, txf tx.Factory, msgs ...sdk.Msg) (out if err != nil { return } - output, ok := clientCtx.Output.(*bytes.Buffer) - if !ok { - err = ErrTypeAssertionFailed - return - } - defer output.Reset() - - result := make(map[string]interface{}) - err = json.Unmarshal(output.Bytes(), &result) - if err != nil { - return - } - - // Make a copy because we `defer output.Reset()` - out = &bytes.Buffer{} - // This is still copying references: *out = *output - // Make a real copy: https://stackoverflow.com/a/69758157 - out.Write(output.Bytes()) - return + return writeClientCtxOutputToBuffer(clientCtx) } // BroadcastTxWithFileLock broadcasts a transaction via gRPC and synchronises requests via a file lock. @@ -233,7 +222,11 @@ func BroadcastTxWithFileLock(fromAddress sdk.AccAddress, msgs ...sdk.Msg) (out * // Set new sequence number txf = txf.WithSequence(sequence) - out, err = broadcastTx(clientCtx, txf, msgs...) + if GetConfig().serialPort != "" { + out, err = broadcastTxWithTrustWalletSignature(clientCtx, txf, msgs...) + } else { + out, err = broadcastTx(clientCtx, txf, msgs...) + } if err != nil { return } @@ -260,3 +253,135 @@ func BroadcastTxWithFileLock(fromAddress sdk.AccAddress, msgs ...sdk.Msg) (out * return } + +func broadcastTxWithTrustWalletSignature(clientCtx client.Context, txf tx.Factory, msgs ...sdk.Msg) (out *bytes.Buffer, err error) { + txBuilder, err := txf.BuildUnsignedTx(msgs...) + if err != nil { + return + } + + if err = signWithTrustWallet(txf, clientCtx, txBuilder); err != nil { + return + } + + txBytes, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return + } + + res, err := clientCtx.BroadcastTx(txBytes) + if err != nil { + return + } + + if err = clientCtx.PrintProto(res); err != nil { + return + } + + return writeClientCtxOutputToBuffer(clientCtx) +} + +func writeClientCtxOutputToBuffer(clientCtx client.Context) (out *bytes.Buffer, err error) { + output, ok := clientCtx.Output.(*bytes.Buffer) + if !ok { + err = ErrTypeAssertionFailed + return + } + defer output.Reset() + + result := make(map[string]interface{}) + err = json.Unmarshal(output.Bytes(), &result) + if err != nil { + return + } + + // Make a copy because we `defer output.Reset()` + out = &bytes.Buffer{} + // This is still copying references: *out = *output + // Make a real copy: https://stackoverflow.com/a/69758157 + out.Write(output.Bytes()) + return +} + +func signWithTrustWallet(txf tx.Factory, clientCtx client.Context, txBuilder client.TxBuilder) error { + connector, err := trustwallet.NewTrustWalletConnector(GetConfig().serialPort) + if err != nil { + return err + } + + keys, err := connector.GetPlanetmintKeys() + if err != nil { + return err + } + + pubkeyBytes, err := hex.DecodeString(keys.RawPlanetmintPubkey) + if err != nil { + return err + } + pk := secp256k1.PubKey{Key: pubkeyBytes} + + signMode := txf.SignMode() + if signMode == signing.SignMode_SIGN_MODE_UNSPECIFIED { + // use the SignModeHandler's default mode if unspecified + signMode = clientCtx.TxConfig.SignModeHandler().DefaultMode() + } + + signerData := authsigning.SignerData{ + ChainID: txf.ChainID(), + AccountNumber: txf.AccountNumber(), + Sequence: txf.Sequence(), + PubKey: &pk, + Address: sdk.AccAddress(pk.Address()).String(), + } + + sigData := signing.SingleSignatureData{ + SignMode: signMode, + Signature: nil, + } + + sig := signing.SignatureV2{ + PubKey: &pk, + Data: &sigData, + Sequence: txf.Sequence(), + } + + if err := txBuilder.SetSignatures(sig); err != nil { + return err + } + + bytesToSign, err := clientCtx.TxConfig.SignModeHandler().GetSignBytes(signMode, signerData, txBuilder.GetTx()) + if err != nil { + return err + } + + hashBytesToSign := crypto.Sha256(bytesToSign) + hexHash := hex.EncodeToString(hashBytesToSign) + + hexSig, err := connector.SignHashWithPlanetmint(hexHash) + if err != nil { + return err + } + + signature, err := hex.DecodeString(hexSig) + if err != nil { + return err + } + + sigData = signing.SingleSignatureData{ + SignMode: signMode, + Signature: signature, + } + sig = signing.SignatureV2{ + PubKey: &pk, + Data: &sigData, + Sequence: txf.Sequence(), + } + + if err = txBuilder.SetSignatures(sig); err != nil { + return fmt.Errorf("unable to set signatures on payload: %w", err) + } + + // Run optional preprocessing if specified. By default, this is unset + // and will return nil. + return txf.PreprocessTx(clientCtx.FromName, txBuilder) +}