diff --git a/expo/android/src/main/java/land/gno/gnonative/GnonativeModule.kt b/expo/android/src/main/java/land/gno/gnonative/GnonativeModule.kt index 196324cf..b8aeb028 100644 --- a/expo/android/src/main/java/land/gno/gnonative/GnonativeModule.kt +++ b/expo/android/src/main/java/land/gno/gnonative/GnonativeModule.kt @@ -88,6 +88,19 @@ class GnonativeModule : Module() { } } + AsyncFunction("startGnokeyMobileService") { promise: Promise -> + try { + bridgeGnoNative?.let { + bridgeGnoNative!!.startGnokeyMobileService() + promise.resolve(true) + } ?: run { + throw GoBridgeNotStartedError() + } + } catch (err: CodedException) { + promise.reject(err) + } + } + AsyncFunction("invokeGrpcMethod") { method: String, jsonMessage: String, promise: Promise -> try { bridgeGnoNative?.let { diff --git a/expo/ios/GnonativeModule.swift b/expo/ios/GnonativeModule.swift index 3d44db64..b0c492e1 100644 --- a/expo/ios/GnonativeModule.swift +++ b/expo/ios/GnonativeModule.swift @@ -92,6 +92,18 @@ public class GnonativeModule: Module { } } + AsyncFunction("startGnokeyMobileService") { (promise: Promise) in + do { + guard let service = self.bridge else { + throw GnoError(.notStarted) + } + try service.startGnokeyMobileService() + promise.resolve(true) + } catch let error { + promise.reject(error) + } + } + AsyncFunction("closeBridge") { (promise: Promise) in do { guard let service = self.bridge else { diff --git a/expo/src/GoBridge.ts b/expo/src/GoBridge.ts index 2de66304..2dc18498 100644 --- a/expo/src/GoBridge.ts +++ b/expo/src/GoBridge.ts @@ -4,6 +4,7 @@ export interface GoBridgeInterface { initBridge(): Promise; closeBridge(): Promise; getTcpPort(): Promise; + startGnokeyMobileService(): Promise; invokeGrpcMethod(method: string, jsonMessage: string): Promise; createStreamClient(method: string, jsonMessage: string): Promise; streamClientReceive(id: string): Promise; @@ -23,6 +24,10 @@ class GoBridge implements GoBridgeInterface { return GnonativeModule.getTcpPort(); } + startGnokeyMobileService(): Promise { + return GnonativeModule.startGnokeyMobileService(); + } + invokeGrpcMethod(method: string, jsonMessage: string): Promise { return GnonativeModule.invokeGrpcMethod(method, jsonMessage); } diff --git a/expo/src/api/GnoNativeApi.ts b/expo/src/api/GnoNativeApi.ts index 2874e1d4..4f90a605 100644 --- a/expo/src/api/GnoNativeApi.ts +++ b/expo/src/api/GnoNativeApi.ts @@ -80,6 +80,10 @@ export class GnoNativeApi implements GnoKeyApi, GoBridgeInterface { new SetChainIDRequest({ chainId: this.config.chain_id }), ); console.log('✅ GnoNative bridge initialized.'); + if (this.config.start_gnokey_mobile_service) { + await this.startGnokeyMobileService(); + console.log('✅ Gnokey Mobile service started.'); + } return true; } catch (error) { console.error(error); @@ -380,6 +384,9 @@ export class GnoNativeApi implements GnoKeyApi, GoBridgeInterface { getTcpPort(): Promise { return GoBridge.getTcpPort(); } + startGnokeyMobileService(): Promise { + return GoBridge.startGnokeyMobileService(); + } invokeGrpcMethod(method: string, jsonMessage: string): Promise { return GoBridge.invokeGrpcMethod(method, jsonMessage); } diff --git a/expo/src/api/types.ts b/expo/src/api/types.ts index 4b49fdb1..afac6b55 100644 --- a/expo/src/api/types.ts +++ b/expo/src/api/types.ts @@ -22,6 +22,8 @@ export enum BridgeStatus { export interface Config { remote: string; chain_id: string; + // If true, initBridge also starts a Gnokey Mobile service. (Only needed for the Gnokey Mobile app.) + start_gnokey_mobile_service: boolean; } export interface GnoKeyApi { diff --git a/framework/service/bridge.go b/framework/service/bridge.go index 10a45770..ce2ec976 100644 --- a/framework/service/bridge.go +++ b/framework/service/bridge.go @@ -13,6 +13,7 @@ import ( "github.com/peterbourgon/unixtransport" "go.uber.org/multierr" + gnokey_mobile_service "github.com/gnolang/gnokey-mobile/service" api_gen "github.com/gnolang/gnonative/api/gen/go" "github.com/gnolang/gnonative/api/gen/go/_goconnect" "github.com/gnolang/gnonative/service" @@ -23,6 +24,7 @@ type BridgeConfig struct { TmpDir string UseTcpListener bool DisableUdsListener bool + UseGnokeyMobile bool } func NewBridgeConfig() *BridgeConfig { @@ -38,6 +40,8 @@ type Bridge struct { serviceServer service.GnoNativeService + gnokeyMobileService gnokey_mobile_service.GnokeyMobileService + ServiceClient } @@ -77,6 +81,10 @@ func NewBridge(config *BridgeConfig) (*Bridge, error) { svcOpts = append(svcOpts, service.WithDisableUdsListener()) } + if config.UseGnokeyMobile { + svcOpts = append(svcOpts, service.WithUseGnokeyMobile()) + } + serviceServer, err := service.NewGnoNativeService(svcOpts...) if err != nil { return nil, errors.Wrap(err, "unable to create bridge service") @@ -152,6 +160,28 @@ func (b *Bridge) GetTcpAddr() string { return b.serviceServer.GetTcpAddr() } +// Start the Gnokey Mobile service and save it in gnokeyMobileService. This will be closed in Close(). +// If the gnonative serviceServer is not started, do nothing. +// If gnokeyMobileService is already started, do nothing. +func (b *Bridge) StartGnokeyMobileService() error { + if b.serviceServer == nil { + return nil + } + if b.gnokeyMobileService != nil { + // Already started + return nil + } + + // Use the default options + gnokeyMobileService, err := gnokey_mobile_service.NewGnokeyMobileService(b.serviceServer) + if err != nil { + return err + } + + b.gnokeyMobileService = gnokeyMobileService + return nil +} + func (b *Bridge) Close() error { var errs error @@ -177,6 +207,8 @@ func (b *Bridge) Close() error { errs = multierr.Append(errs, err) } + // TODO: Close b.gnokeyMobileService + cancel() } diff --git a/go.mod b/go.mod index 23bec96a..a48e16ea 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( connectrpc.com/grpchealth v1.3.0 connectrpc.com/grpcreflect v1.2.0 github.com/gnolang/gno v0.1.1 + github.com/gnolang/gnokey-mobile v0.0.0-20240814140149-eb333b936c7c github.com/oklog/run v1.1.0 github.com/peterbourgon/ff/v3 v3.4.0 github.com/peterbourgon/unixtransport v0.0.3 diff --git a/go.sum b/go.sum index a975d273..7c001173 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gnolang/gnokey-mobile v0.0.0-20240814140149-eb333b936c7c h1:rL7dVjWOpdQxmbsh69HrgAklolhydTZmPvgo6BpgdhE= +github.com/gnolang/gnokey-mobile v0.0.0-20240814140149-eb333b936c7c/go.mod h1:2NrHp15t6QXGNDruOpw6/xN6z50kquVvZs6uxvUNhsQ= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/service/api.go b/service/api.go index 07b4007c..d57061b8 100644 --- a/service/api.go +++ b/service/api.go @@ -34,6 +34,16 @@ func (s *gnoNativeService) SetRemote(ctx context.Context, req *connect.Request[a } func (s *gnoNativeService) GetRemote(ctx context.Context, req *connect.Request[api_gen.GetRemoteRequest]) (*connect.Response[api_gen.GetRemoteResponse], error) { + if s.useGnokeyMobile { + // Always get the remote from the Gnokey Mobile service + res, err := s.gnokeyMobileClient.GetRemote(context.Background(), req) + if err != nil { + return nil, err + } + + return connect.NewResponse(res.Msg), nil + } + return connect.NewResponse(&api_gen.GetRemoteResponse{Remote: s.ClientGetRemote()}), nil } @@ -77,6 +87,16 @@ func ConvertKeyInfo(key crypto_keys.Info) (*api_gen.KeyInfo, error) { func (s *gnoNativeService) ListKeyInfo(ctx context.Context, req *connect.Request[api_gen.ListKeyInfoRequest]) (*connect.Response[api_gen.ListKeyInfoResponse], error) { s.logger.Debug("ListKeyInfo called") + if s.useGnokeyMobile { + // Always get the list of keys from the Gnokey Mobile service + res, err := s.gnokeyMobileClient.ListKeyInfo(context.Background(), req) + if err != nil { + return nil, err + } + + return connect.NewResponse(res.Msg), nil + } + keys, err := s.ClientListKeyInfo() if err != nil { return nil, err @@ -272,8 +292,12 @@ func (s *gnoNativeService) GetActiveAccount(ctx context.Context, req *connect.Re func (s *gnoNativeService) QueryAccount(ctx context.Context, req *connect.Request[api_gen.QueryAccountRequest]) (*connect.Response[api_gen.QueryAccountResponse], error) { s.logger.Debug("QueryAccount", zap.ByteString("address", req.Msg.Address)) + c, err := s.getClient() + if err != nil { + return nil, getGrpcError(err) + } // gnoclient wants the bech32 address. - account, _, err := s.client.QueryAccount(crypto.AddressFromBytes(req.Msg.Address)) + account, _, err := c.QueryAccount(crypto.AddressFromBytes(req.Msg.Address)) if err != nil { return nil, getGrpcError(err) } @@ -324,7 +348,11 @@ func (s *gnoNativeService) Query(ctx context.Context, req *connect.Request[api_g Data: req.Msg.Data, } - bres, err := s.client.Query(cfg) + c, err := s.getClient() + if err != nil { + return nil, getGrpcError(err) + } + bres, err := c.Query(cfg) if err != nil { return nil, getGrpcError(err) } @@ -335,7 +363,11 @@ func (s *gnoNativeService) Query(ctx context.Context, req *connect.Request[api_g func (s *gnoNativeService) Render(ctx context.Context, req *connect.Request[api_gen.RenderRequest]) (*connect.Response[api_gen.RenderResponse], error) { s.logger.Debug("Render", zap.String("packagePath", req.Msg.PackagePath), zap.String("args", req.Msg.Args)) - result, _, err := s.client.Render(req.Msg.PackagePath, req.Msg.Args) + c, err := s.getClient() + if err != nil { + return nil, getGrpcError(err) + } + result, _, err := c.Render(req.Msg.PackagePath, req.Msg.Args) if err != nil { return nil, getGrpcError(err) } @@ -346,7 +378,11 @@ func (s *gnoNativeService) Render(ctx context.Context, req *connect.Request[api_ func (s *gnoNativeService) QEval(ctx context.Context, req *connect.Request[api_gen.QEvalRequest]) (*connect.Response[api_gen.QEvalResponse], error) { s.logger.Debug("QEval", zap.String("packagePath", req.Msg.PackagePath), zap.String("expression", req.Msg.Expression)) - result, _, err := s.client.QEval(req.Msg.PackagePath, req.Msg.Expression) + c, err := s.getClient() + if err != nil { + return nil, getGrpcError(err) + } + result, _, err := c.QEval(req.Msg.PackagePath, req.Msg.Expression) if err != nil { return nil, getGrpcError(err) } @@ -361,6 +397,53 @@ func (s *gnoNativeService) Call(ctx context.Context, req *connect.Request[api_ge cfg, msgs := convertCallRequest(req.Msg) + if s.useGnokeyMobile { + c, err := s.getClient() + if err != nil { + return getGrpcError(err) + } + tx, err := c.MakeCallTx(*cfg, msgs...) + if err != nil { + return err + } + txJSON, err := amino.MarshalJSON(tx) + if err != nil { + return err + } + + // Use Gnokey Mobile to sign. + // Note that req.Msg.CallerAddress must be set to the desired signer. The app can get the + // address using ListKeyInfo. + signedTxJSON, err := s.gnokeyMobileClient.SignTx( + context.Background(), + connect.NewRequest(&api_gen.SignTxRequest{ + TxJson: string(txJSON), + }), + ) + if err != nil { + return err + } + signedTx := &std.Tx{} + if err := amino.UnmarshalJSON([]byte(signedTxJSON.Msg.SignedTxJson), signedTx); err != nil { + return err + } + + // Now broadcast + bres, err := c.BroadcastTxCommit(signedTx) + if err != nil { + return getGrpcError(err) + } + + if err := stream.Send(&api_gen.CallResponse{ + Result: bres.DeliverTx.Data, + }); err != nil { + s.logger.Error("Call stream.Send returned error", zap.Error(err)) + return err + } + + return nil + } + s.lock.RLock() if s.activeAccount == nil { s.lock.RUnlock() diff --git a/service/config.go b/service/config.go index 64b93c06..e54b9096 100644 --- a/service/config.go +++ b/service/config.go @@ -24,6 +24,7 @@ type Config struct { UdsPath string UseTcpListener bool DisableUdsListener bool + UseGnokeyMobile bool } type GnoNativeOption func(cfg *Config) error @@ -327,6 +328,16 @@ var WithDisableUdsListener = func() GnoNativeOption { } } +// --- Gnokey Mobile options --- + +// WithUseGnokeyMobile sets the gRPC service to use Gnokey Mobile for key-based operations. +var WithUseGnokeyMobile = func() GnoNativeOption { + return func(cfg *Config) error { + cfg.UseGnokeyMobile = true + return nil + } +} + // --- Fallback options --- var defaults = []FallBackOption{ diff --git a/service/service.go b/service/service.go index 3b283698..64f01a89 100644 --- a/service/service.go +++ b/service/service.go @@ -17,6 +17,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" crypto_keys "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/std" + gnokey_mobile_goconnect "github.com/gnolang/gnokey-mobile/api/gen/go/_goconnect" api_gen "github.com/gnolang/gnonative/api/gen/go" "github.com/gnolang/gnonative/api/gen/go/_goconnect" "github.com/pkg/errors" @@ -56,6 +57,10 @@ type gnoNativeService struct { // here because the remote is a private member of the HTTP struct. remote string + // Gnokey Mobile support + useGnokeyMobile bool + gnokeyMobileClient gnokey_mobile_goconnect.GnokeyMobileServiceClient + // Map of key name to userAccount. userAccounts map[string]*userAccount // The active account in userAccounts, or nil if none @@ -111,6 +116,28 @@ func initService(cfg *Config) (*gnoNativeService, error) { return nil, err } + if cfg.UseGnokeyMobile { + // Create an inter-app connection to the Gnokey Mobile service + gnokeyMobileServerAddr := "http://localhost:26659" + svc.useGnokeyMobile = true + svc.gnokeyMobileClient = gnokey_mobile_goconnect.NewGnokeyMobileServiceClient( + http.DefaultClient, + gnokeyMobileServerAddr, + ) + + // For Gnokey Mobile, we don't need a local svc.client.Keybase . + svc.client = &gnoclient.Client{} + + // This will set svc.remote and set up svc.client.RPCClient . + _, err := svc.getClient() + if err != nil { + return nil, err + } + svc.logger.Info("initService: Gnokey Mobile gRPC client connected to", zap.String("path", gnokeyMobileServerAddr)) + + return svc, nil + } + kb, _ := keys.NewKeyBaseFromDir(cfg.RootDir) signer := &gnoclient.SignerFromKeybase{ Keybase: kb, @@ -131,6 +158,37 @@ func initService(cfg *Config) (*gnoNativeService, error) { return svc, nil } +// Get the gnoclient.Client . If not useGnokeyMobile then just return s.client . +// If useGnokeyMobile then query gnokeyMobileClient for the gno.land remote node +// address (which may have changed) and reconfigure s.client.RPCClient and +// s.remote if necessary. +func (s *gnoNativeService) getClient() (*gnoclient.Client, error) { + if s.useGnokeyMobile { + // Get the current Remote from Gnokey Mobile + res, err := s.gnokeyMobileClient.GetRemote( + context.Background(), + connect.NewRequest(&api_gen.GetRemoteRequest{}), + ) + if err != nil { + return nil, err + } + + if s.remote != res.Msg.Remote { + s.logger.Debug("getClient: Setting remote to " + res.Msg.Remote) + + // Gnokey Mobile has changed the remote (or this is the first call) + // Imitate gnoNativeService.SetRemote + s.client.RPCClient, err = rpcclient.NewHTTPClient(res.Msg.Remote) + if err != nil { + return nil, err + } + s.remote = res.Msg.Remote + } + } + + return s.client, nil +} + // Get s.client.Signer as a SignerFromKeybase. func (s *gnoNativeService) getSigner() *gnoclient.SignerFromKeybase { signer, ok := s.client.Signer.(*gnoclient.SignerFromKeybase)