diff --git a/expo/README.md b/expo/README.md index 470f45b9..38c94203 100644 --- a/expo/README.md +++ b/expo/README.md @@ -108,11 +108,11 @@ before continuing. You can run one of the following command: ```shell -make pack +make npm.pack ``` or ```shell -make publish +make npm.publish ``` diff --git a/expo/example/App.tsx b/expo/example/App.tsx index 989b7f80..7576d7d2 100644 --- a/expo/example/App.tsx +++ b/expo/example/App.tsx @@ -1,14 +1,30 @@ -import * as Gnonative from '@gnolang/gnonative'; +import { GnokeyProvider, useGnokeyContext } from '@gnolang/gnonative'; import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; +const config = { + remote: 'https://gno.berty.io', + chain_id: 'dev', +}; + export default function App() { - const gno = Gnonative.useGno(); + return ( + + + + ); +} + +const InnerApp = () => { + const gno = useGnokeyContext(); const [greeting, setGreeting] = useState(''); useEffect(() => { - const greeting = async () => { + (async () => { try { + const accounts = await gno.listKeyInfo(); + console.log(accounts); + setGreeting(await gno.hello('Gno')); for await (const res of await gno.helloStream('Gno')) { @@ -17,16 +33,16 @@ export default function App() { } catch (error) { console.log(error); } - }; - greeting(); + })(); }, []); return ( - Hey {greeting} + Gnonative App + {greeting} ); -} +}; const styles = StyleSheet.create({ container: { diff --git a/expo/src/hooks/use-gno.ts b/expo/src/hooks/use-gno.ts index e91efa40..b7b64a42 100644 --- a/expo/src/hooks/use-gno.ts +++ b/expo/src/hooks/use-gno.ts @@ -101,12 +101,18 @@ export interface GnoResponse { helloStream: (name: string) => Promise>; } +enum Status { + Stopped, + Starting, + Started, +} + let clientInstance: PromiseClient | undefined = undefined; -let bridgeInstance: boolean = false; +let bridgeStatus: Status = Status.Stopped; export const useGno = (): GnoResponse => { const getClient = async () => { - if (!bridgeInstance) { + if (bridgeStatus === Status.Stopped) { await initBridge(); } @@ -127,20 +133,22 @@ export const useGno = (): GnoResponse => { }; const closeBridge = async () => { - if (bridgeInstance) { + if (bridgeStatus !== Status.Stopped) { console.log('Closing bridge...'); await GoBridge.closeBridge(); console.log('Bridge closed.'); - bridgeInstance = false; + bridgeStatus = Status.Stopped; + clientInstance = undefined; } }; const initBridge = async () => { - if (!bridgeInstance) { + if (bridgeStatus === Status.Stopped) { console.log('Initializing bridge...'); + bridgeStatus = Status.Starting; await GoBridge.initBridge(); console.log('Bridge initialized.'); - bridgeInstance = true; + bridgeStatus = Status.Started; } }; diff --git a/expo/src/index.ts b/expo/src/index.ts index 063b7f0e..9eb57d39 100644 --- a/expo/src/index.ts +++ b/expo/src/index.ts @@ -17,4 +17,5 @@ export function addChangeListener(listener: (event: ChangeEventPayload) => void) export { ChangeEventPayload, GnonativeView, GnonativeViewProps }; export { useGno } from './hooks/use-gno'; +export * from './provider/gnokey-provider'; export * from '@buf/gnolang_gnonative.bufbuild_es/gnonativetypes_pb'; diff --git a/expo/src/provider/gnokey-provider.tsx b/expo/src/provider/gnokey-provider.tsx new file mode 100644 index 00000000..6b63dcd3 --- /dev/null +++ b/expo/src/provider/gnokey-provider.tsx @@ -0,0 +1,485 @@ +import { + AddressFromBech32Request, + AddressToBech32Request, + CallRequest, + CallResponse, + CreateAccountRequest, + DeleteAccountRequest, + DeleteAccountResponse, + GenerateRecoveryPhraseRequest, + GetActiveAccountRequest, + GetActiveAccountResponse, + GetChainIDRequest, + GetKeyInfoByAddressRequest, + GetKeyInfoByNameOrAddressRequest, + GetKeyInfoByNameRequest, + GetRemoteRequest, + HasKeyByAddressRequest, + HasKeyByNameOrAddressRequest, + HasKeyByNameRequest, + HelloRequest, + HelloStreamResponse, + ListKeyInfoRequest, + MsgCall, + MsgSend, + QEvalRequest, + QueryAccountRequest, + QueryAccountResponse, + QueryRequest, + QueryResponse, + RenderRequest, + SelectAccountRequest, + SelectAccountResponse, + SendRequest, + SendResponse, + SetChainIDRequest, + SetChainIDResponse, + SetPasswordRequest, + SetPasswordResponse, + SetRemoteRequest, + SetRemoteResponse, +} from '@buf/gnolang_gnonative.bufbuild_es/gnonativetypes_pb'; +import { GnoNativeService } from '@buf/gnolang_gnonative.connectrpc_es/rpc_connect'; +import { PromiseClient } from '@connectrpc/connect'; +import { createContext, useContext, useEffect, useState } from 'react'; + +import { GoBridge } from '../GoBridge'; +import * as Grpc from '../grpc/client'; +import { GnoAccount } from '../hooks/types'; + +export interface GnokeyContextProps { + initGnokey: (config: ConfigProps) => Promise; + + setRemote: (remote: string) => Promise; + getRemote: () => Promise; + setChainID: (chainId: string) => Promise; + getChainID: () => Promise; + createAccount: ( + nameOrBech32: string, + mnemonic: string, + password: string, + ) => Promise; + generateRecoveryPhrase: () => Promise; + listKeyInfo: () => Promise; + hasKeyByName: (name: string) => Promise; + hasKeyByAddress: (address: Uint8Array) => Promise; + hasKeyByNameOrAddress: (nameOrBech32: string) => Promise; + getKeyInfoByName: (name: string) => Promise; + getKeyInfoByAddress: (address: Uint8Array) => Promise; + getKeyInfoByNameOrAddress: (nameOrBech32: string) => Promise; + selectAccount: (nameOrBech32: string) => Promise; + setPassword: (password: string) => Promise; + getActiveAccount: () => Promise; + queryAccount: (address: Uint8Array) => Promise; + deleteAccount: ( + nameOrBech32: string, + password: string | undefined, + skipPassword: boolean, + ) => Promise; + query: (path: string, data: Uint8Array) => Promise; + render: (packagePath: string, args: string) => Promise; + qEval: (packagePath: string, expression: string) => Promise; + call: ( + packagePath: string, + fnc: string, + args: string[], + gasFee: string, + gasWanted: number, + send?: string, + memo?: string, + ) => Promise>; + send: ( + toAddress: Uint8Array, + send: string, + gasFee: string, + gasWanted: number, + memo?: string, + ) => Promise>; + addressToBech32: (address: Uint8Array) => Promise; + addressFromBech32: (bech32Address: string) => Promise; + // closeBridge: () => Promise; + // initBridge: () => Promise; + // debug + hello: (name: string) => Promise; + helloStream: (name: string) => Promise>; +} + +interface ConfigProps { + remote: string; + chain_id: string; +} + +interface GnokeyProviderProps { + config: ConfigProps; + children: React.ReactNode; +} + +enum BridgeStatus { + Stopped, + Starting, + Started, +} + +const GnokeyContext = createContext(null); + +const GnokeyProvider: React.FC = ({ children, config }) => { + const [initialized, setInitialized] = useState(false); + const [clientInstance, setClientInstance] = useState< + PromiseClient | undefined + >(undefined); + const [bridgeStatus, setBridgeStatus] = useState(BridgeStatus.Stopped); + + useEffect(() => { + (async () => { + await initGnokey(config); + setInitialized(true); + })(); + }, []); + + async function initGnokey(config): Promise { + console.log( + '🍄 Initializing Gnokey on remote: %s chain_id: %s', + config.remote, + config.chain_id, + ); + + if (bridgeStatus === BridgeStatus.Stopped) { + console.log('Bridge stopped. Initializing...'); + await initBridge(); + } + + if (clientInstance) { + console.error('GoBridge already initialized.'); + return true; + } + + const port = await GoBridge.getTcpPort(); + console.log('GoBridge GRPC client instance port: %s', port); + const client = await Grpc.createClient(port); + setClientInstance(client); + console.log('GoBridge GRPC client instance. Done.'); + + try { + await client.setRemote(new SetRemoteRequest({ remote: 'gno.land:26657' })); + await client.setChainID(new SetChainIDRequest({ chainId: 'portal-loop' })); + + console.log('✅ Gnokey bridge initialized.'); + } catch (error) { + console.error(error); + return false; + } + + return true; + } + + const initBridge = async () => { + if (bridgeStatus === BridgeStatus.Stopped) { + console.log('Initializing bridge...'); + setBridgeStatus(BridgeStatus.Starting); + await GoBridge.initBridge(); + console.log('Bridge initialized.'); + setBridgeStatus(BridgeStatus.Started); + } + }; + + const setRemote = async (remote: string) => { + const client = await getClient(); + const response = await client.setRemote(new SetRemoteRequest({ remote })); + return response; + }; + + const getRemote = async () => { + const client = await getClient(); + const response = await client.getRemote(new GetRemoteRequest()); + return response.remote; + }; + + const setChainID = async (chainId: string) => { + const client = await getClient(); + const response = await client.setChainID(new SetChainIDRequest({ chainId })); + return response; + }; + + const getChainID = async () => { + const client = await getClient(); + const response = await client.getChainID(new GetChainIDRequest()); + return response.chainId; + }; + + const createAccount = async (nameOrBech32: string, mnemonic: string, password: string) => { + const client = await getClient(); + const reponse = await client.createAccount( + new CreateAccountRequest({ + nameOrBech32, + mnemonic, + password, + }), + ); + return reponse.key; + }; + + const generateRecoveryPhrase = async () => { + const client = await getClient(); + const response = await client.generateRecoveryPhrase(new GenerateRecoveryPhraseRequest()); + return response.phrase; + }; + + const hasKeyByName = async (name: string) => { + const client = await getClient(); + const response = await client.hasKeyByName(new HasKeyByNameRequest({ name })); + return response.has; + }; + + const hasKeyByAddress = async (address: Uint8Array) => { + const client = await getClient(); + const response = await client.hasKeyByAddress(new HasKeyByAddressRequest({ address })); + return response.has; + }; + + const hasKeyByNameOrAddress = async (nameOrBech32: string) => { + const client = await getClient(); + const response = await client.hasKeyByNameOrAddress( + new HasKeyByNameOrAddressRequest({ nameOrBech32 }), + ); + return response.has; + }; + + const getKeyInfoByName = async (name: string) => { + const client = await getClient(); + const response = await client.getKeyInfoByName(new GetKeyInfoByNameRequest({ name })); + return response.key; + }; + + const getKeyInfoByAddress = async (address: Uint8Array) => { + const client = await getClient(); + const response = await client.getKeyInfoByAddress(new GetKeyInfoByAddressRequest({ address })); + return response.key; + }; + + const getKeyInfoByNameOrAddress = async (nameOrBech32: string) => { + const client = await getClient(); + const response = await client.getKeyInfoByNameOrAddress( + new GetKeyInfoByNameOrAddressRequest({ nameOrBech32 }), + ); + return response.key; + }; + + const listKeyInfo = async () => { + const client = await getClient(); + const response = await client.listKeyInfo(new ListKeyInfoRequest()); + return response.keys; + }; + + const selectAccount = async (nameOrBech32: string) => { + const client = await getClient(); + const response = await client.selectAccount( + new SelectAccountRequest({ + nameOrBech32, + }), + ); + return response; + }; + + const getClient = async () => { + if (!clientInstance) { + throw new Error('GoBridge client instance not initialized.'); + } + + return clientInstance; + }; + + const setPassword = async (password: string) => { + const client = await getClient(); + const response = await client.setPassword(new SetPasswordRequest({ password })); + return response; + }; + + const getActiveAccount = async () => { + const client = await getClient(); + const response = await client.getActiveAccount(new GetActiveAccountRequest()); + return response; + }; + + const queryAccount = async (address: Uint8Array) => { + const client = await getClient(); + const reponse = await client.queryAccount(new QueryAccountRequest({ address })); + return reponse; + }; + + const deleteAccount = async ( + nameOrBech32: string, + password: string | undefined, + skipPassword: boolean, + ) => { + const client = await getClient(); + const response = await client.deleteAccount( + new DeleteAccountRequest({ + nameOrBech32, + password, + skipPassword, + }), + ); + return response; + }; + + const query = async (path: string, data: Uint8Array) => { + const client = await getClient(); + const reponse = await client.query( + new QueryRequest({ + path, + data, + }), + ); + return reponse; + }; + + const render = async (packagePath: string, args: string) => { + const client = await getClient(); + const reponse = await client.render( + new RenderRequest({ + packagePath, + args, + }), + ); + return reponse.result; + }; + + const qEval = async (packagePath: string, expression: string) => { + const client = await getClient(); + const reponse = await client.qEval( + new QEvalRequest({ + packagePath, + expression, + }), + ); + return reponse.result; + }; + + const call = async ( + packagePath: string, + fnc: string, + args: string[], + gasFee: string, + gasWanted: number, + send?: string, + memo?: string, + ) => { + const client = await getClient(); + const reponse = client.call( + new CallRequest({ + gasFee, + gasWanted: BigInt(gasWanted), + memo, + msgs: [ + new MsgCall({ + packagePath, + fnc, + args, + send, + }), + ], + }), + ); + return reponse; + }; + + const send = async ( + toAddress: Uint8Array, + send: string, + gasFee: string, + gasWanted: number, + memo?: string, + ) => { + const client = await getClient(); + const reponse = client.send( + new SendRequest({ + gasFee, + gasWanted: BigInt(gasWanted), + memo, + msgs: [ + new MsgSend({ + toAddress, + send, + }), + ], + }), + ); + return reponse; + }; + + const addressToBech32 = async (address: Uint8Array) => { + const client = await getClient(); + const response = await client.addressToBech32(new AddressToBech32Request({ address })); + return response.bech32Address; + }; + + const addressFromBech32 = async (bech32Address: string) => { + const client = await getClient(); + const response = await client.addressFromBech32( + new AddressFromBech32Request({ bech32Address }), + ); + return response.address; + }; + + // debug + const hello = async (name: string) => { + const client = await getClient(); + const response = await client.hello(new HelloRequest({ name })); + return response.greeting; + }; + + // debug + const helloStream = async (name: string) => { + const client = await getClient(); + return client.helloStream(new HelloRequest({ name })); + }; + + const value = { + initGnokey, + setRemote, + getRemote, + setChainID, + getChainID, + createAccount, + generateRecoveryPhrase, + listKeyInfo, + hasKeyByName, + hasKeyByAddress, + hasKeyByNameOrAddress, + getKeyInfoByName, + getKeyInfoByAddress, + getKeyInfoByNameOrAddress, + selectAccount, + setPassword, + getActiveAccount, + queryAccount, + deleteAccount, + query, + render, + qEval, + call, + send, + addressToBech32, + addressFromBech32, + // debug + hello, + helloStream, + }; + + if (!initialized) { + return null; + } + + return {children}; +}; + +function useGnokeyContext() { + const context = useContext(GnokeyContext) as GnokeyContextProps; + + if (context === undefined) { + throw new Error('useGnokeyContext must be used within a GnokeyProvider'); + } + return context; +} + +export { useGnokeyContext, GnokeyProvider };