From 0bea8dbff7d1e344439ab9261730a5185f7457d0 Mon Sep 17 00:00:00 2001 From: D4ryl00 Date: Wed, 2 Apr 2025 10:57:26 +0200 Subject: [PATCH 1/4] feat: estimate gasWanted Signed-off-by: D4ryl00 --- .github/workflows/pull-request.yml | 8 ++- mobile/.env | 2 + mobile/app/(app)/tosign/index.tsx | 32 ++++++++++- mobile/app/_layout.tsx | 43 ++++++++------ mobile/package-lock.json | 34 ++--------- mobile/package.json | 2 +- mobile/providers/index.tsx | 3 +- mobile/providers/indexer-provider.tsx | 80 ++++++++++++++++++++++++++ mobile/redux/features/linkingSlice.ts | 53 ++++++++++++++++- mobile/redux/features/vaultAddSlice.ts | 3 +- 10 files changed, 201 insertions(+), 59 deletions(-) create mode 100644 mobile/providers/indexer-provider.tsx diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 327ee4f..d045da3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -13,18 +13,20 @@ jobs: uses: actions/checkout@v4 - name: Setup asdf - uses: asdf-vm/actions/setup@v3 + uses: ynab/asdf-action@v1.2 + with: + version: 0.16.7 - name: Setup Node version working-directory: mobile run: | asdf plugin add nodejs asdf install nodejs - echo "node_version=$(asdf current nodejs | xargs | cut -d ' ' -f 2)" >> $GITHUB_ENV + echo "node_version=$(asdf current nodejs | xargs | cut -d ' ' -f 6)" >> $GITHUB_ENV - name: Set nodejs as global exec run: | - asdf global nodejs ${{ env.node_version }} + asdf set -u nodejs ${{ env.node_version }} - name: Install dependencies working-directory: mobile diff --git a/mobile/.env b/mobile/.env index 737972b..ac738f3 100644 --- a/mobile/.env +++ b/mobile/.env @@ -6,9 +6,11 @@ EXPO_PUBLIC_GNO_CHAIN_ID=portal-loop # EXPO_PUBLIC_GNO_REMOTE=https://api.gno.berty.io:443 # EXPO_PUBLIC_FAUCET_REMOTE=https://faucetpass.gno.berty.io +EXPO_PUBLIC_TXINDEXER_REMOTE=https://txindexer.gno.berty.io # EXPO_PUBLIC_GNO_CHAIN_ID=dev # local # EXPO_PUBLIC_GNO_REMOTE=http://localhost:26657 # EXPO_PUBLIC_FAUCET_REMOTE=http://localhost:5050 +# EXPO_PUBLIC_TXINDEXER_REMOTE=http://localhost:26657 diff --git a/mobile/app/(app)/tosign/index.tsx b/mobile/app/(app)/tosign/index.tsx index 1270214..07faf30 100644 --- a/mobile/app/(app)/tosign/index.tsx +++ b/mobile/app/(app)/tosign/index.tsx @@ -1,6 +1,6 @@ import { Layout, Ruller, TextInput } from "@/components"; import { - selectClientName, selectBech32Address, selectTxInput, signTx, useAppDispatch, + estimateGasWanted, selectClientName, selectBech32Address, selectTxInput, selectUpdateTx, signTx, useAppDispatch, useAppSelector, reasonSelector, selectCallback, selectKeyInfo, clearLinking, selectChainId, selectRemote, selectSession, selectSessionWanted, newSessionKey, SessionKeyInfo } from "@/redux"; @@ -25,10 +25,13 @@ export default function Page() { const reason = useAppSelector(reasonSelector); const bech32Address = useAppSelector(selectBech32Address); const txInput = useAppSelector(selectTxInput); + const updateTx = useAppSelector(selectUpdateTx) ?? false; const callback = useAppSelector(selectCallback); const keyInfo = useAppSelector(selectKeyInfo); const chainId = useAppSelector(selectChainId); const remote = useAppSelector(selectRemote); + const [signedTx, setSignedTx] = useState(undefined); + const [gasWanted, setGasWanted] = useState(BigInt(0)); // const session = useAppSelector(selectSession); // const sessionWanted = useAppSelector(selectSessionWanted); @@ -59,6 +62,29 @@ export default function Page() { })(); }, [bech32Address]); + useEffect(() => { + (async () => { + try { + console.log("onChangeAccountHandler", keyInfo); + + if (!txInput || !keyInfo) { + throw new Error("No transaction input or keyInfo found."); + } + + const { gasWanted } = await dispatch(estimateGasWanted({ keyInfo, updateTx: updateTx })).unwrap(); + + // need to pause to let the Keybase DB close before using it again + await new Promise((f) => setTimeout(f, 1000)); + + const signedTx = await dispatch(signTx({ keyInfo })).unwrap(); + setSignedTx(signedTx.signedTxJson); + setGasWanted(gasWanted); + } catch (error: unknown | Error) { + console.error(error); + } + })(); + }, [txInput, keyInfo]); + const signTxAndReturnToRequester = async () => { console.log('signing the tx', keyInfo); @@ -137,8 +163,8 @@ export default function Page() { - - {gasFee} ugnot + + {gasWanted?.toString()} {/* {sessionWanted && diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 4b896a5..5fae6bd 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -2,7 +2,7 @@ import { Stack } from "expo-router"; import { ThemeProvider as ThemeProvider2 } from "@react-navigation/native"; import { Guard } from "@/components/auth/guard"; import { GnoNativeProvider } from "@gnolang/gnonative"; -import { LinkingProvider, ReduxProvider } from "@/providers"; +import { IndexerProvider, LinkingProvider, ReduxProvider } from "@/providers"; import { DefaultTheme } from "@/assets/styles"; import { ThemeProvider } from "@/modules/ui-components"; @@ -13,26 +13,33 @@ const gnoDefaultConfig = { chain_id: process.env.EXPO_PUBLIC_GNO_CHAIN_ID!, }; +const indexerConfig = { + // @ts-ignore + remote: process.env.EXPO_PUBLIC_TXINDEXER_REMOTE!, +}; + export default function AppLayout() { return ( - - - - - - - - - - - + + + + + + + + + + + + + ); } diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 3807daa..4938b73 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -12,7 +12,7 @@ "@connectrpc/connect": "^1.2.1", "@connectrpc/connect-web": "^1.2.1", "@expo/vector-icons": "^14.0.4", - "@gnolang/gnonative": "^4.0.0", + "@gnolang/gnonative": "^4.1.0", "@react-native-async-storage/async-storage": "1.23.1", "@reduxjs/toolkit": "^2.1.0", "date-fns": "^3.6.0", @@ -2339,30 +2339,6 @@ "node": ">=6.9.0" } }, - "node_modules/@buf/gnolang_gnonative.bufbuild_es": { - "version": "1.8.0-20241030100906-5ceb62413f58.3", - "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.bufbuild_es/-/gnolang_gnonative.bufbuild_es-1.8.0-20241030100906-5ceb62413f58.3.tgz", - "peerDependencies": { - "@bufbuild/protobuf": "^1.8.0" - } - }, - "node_modules/@buf/gnolang_gnonative.connectrpc_es": { - "version": "1.4.0-20241030100906-5ceb62413f58.3", - "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.connectrpc_es/-/gnolang_gnonative.connectrpc_es-1.4.0-20241030100906-5ceb62413f58.3.tgz", - "dependencies": { - "@buf/gnolang_gnonative.bufbuild_es": "1.7.2-20241030100906-5ceb62413f58.2" - }, - "peerDependencies": { - "@connectrpc/connect": "^1.4.0" - } - }, - "node_modules/@buf/gnolang_gnonative.connectrpc_es/node_modules/@buf/gnolang_gnonative.bufbuild_es": { - "version": "1.7.2-20241030100906-5ceb62413f58.2", - "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.bufbuild_es/-/gnolang_gnonative.bufbuild_es-1.7.2-20241030100906-5ceb62413f58.2.tgz", - "peerDependencies": { - "@bufbuild/protobuf": "^1.7.2" - } - }, "node_modules/@bufbuild/protobuf": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", @@ -4025,12 +4001,10 @@ } }, "node_modules/@gnolang/gnonative": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@gnolang/gnonative/-/gnonative-4.0.0.tgz", - "integrity": "sha512-bjx6ahGDeWZshQsysFVIBj1Kte7+tVQUjqH3xhadshgzkhbD0wdX0pFbNSnczlL0POMAJT+6wkGKG5uZhNTOOw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@gnolang/gnonative/-/gnonative-4.1.0.tgz", + "integrity": "sha512-d7EmhPTK4eaYndYgevMk7RVqbA4HR3iYheJcZ3yyX78+RCMb+EsgxkjPF+31B0wZMLbw0ATjibVlGjc6Ugk+mw==", "dependencies": { - "@buf/gnolang_gnonative.bufbuild_es": "^1.8.0-20241030100906-5ceb62413f58.3", - "@buf/gnolang_gnonative.connectrpc_es": "^1.4.0-20241030100906-5ceb62413f58.3", "@bufbuild/protobuf": "^1.8.0", "@connectrpc/connect": "^1.4.0", "@connectrpc/connect-web": "^1.4.0", diff --git a/mobile/package.json b/mobile/package.json index fcb9504..8d26531 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -17,7 +17,7 @@ "@connectrpc/connect": "^1.2.1", "@connectrpc/connect-web": "^1.2.1", "@expo/vector-icons": "^14.0.4", - "@gnolang/gnonative": "^4.0.0", + "@gnolang/gnonative": "^4.1.0", "@react-native-async-storage/async-storage": "1.23.1", "@reduxjs/toolkit": "^2.1.0", "date-fns": "^3.6.0", diff --git a/mobile/providers/index.tsx b/mobile/providers/index.tsx index aaac483..6113f5f 100644 --- a/mobile/providers/index.tsx +++ b/mobile/providers/index.tsx @@ -1,4 +1,5 @@ import { LinkingProvider } from "./linking-provider"; import { ReduxProvider } from "@/providers/redux-provider"; +import { IndexerProvider } from "@/providers/indexer-provider"; -export { LinkingProvider, ReduxProvider }; \ No newline at end of file +export { IndexerProvider, LinkingProvider, ReduxProvider }; diff --git a/mobile/providers/indexer-provider.tsx b/mobile/providers/indexer-provider.tsx new file mode 100644 index 0000000..d30c068 --- /dev/null +++ b/mobile/providers/indexer-provider.tsx @@ -0,0 +1,80 @@ +import { createContext, useContext } from "react"; + +export interface GasPriceResponse { + low: bigint; + average: bigint; + high: bigint; + denom: string; +} + +export interface IndexerContextProps { + getGasPrice: () => Promise; +} + +interface ConfigProps { + remote: string; +} + +interface IndexerProviderProps { + config: ConfigProps; + children: React.ReactNode; +} + +const IndexerContext = createContext(null); + +const IndexerProvider: React.FC = ({ children, config }) => { + if (!config.remote) { + throw new Error("IndexerProvider requires a remote config"); + } + + const getGasPrice = async (): Promise => { + const response = await fetch(config.remote, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "getGasPrice", + }), + }); + + const text = await response.text(); + // convert number to bigint that is not automatically converted by JSON.parse + const jsonData = JSON.parse(text, (_, value) => { + if (typeof value === "number") { + return BigInt(value); + } + return value; + }); + + const data: GasPriceResponse[] = jsonData.result; + + for (const item of data) { + if (item.denom == "ugnot") { + console.log("getGasPrice found: ", item); + return item; + } + } + + throw new Error("No gas price found"); + }; + + const value = { + getGasPrice, + }; + + return {children}; +}; + +function useIndexerContext() { + const context = useContext(IndexerContext) as IndexerContextProps; + + if (context === undefined) { + throw new Error("useIndexerContext must be used within a IndexerProvider"); + } + return context; +} + +export { IndexerProvider, useIndexerContext }; diff --git a/mobile/redux/features/linkingSlice.ts b/mobile/redux/features/linkingSlice.ts index e75e54c..62a0c0b 100644 --- a/mobile/redux/features/linkingSlice.ts +++ b/mobile/redux/features/linkingSlice.ts @@ -4,6 +4,8 @@ import { GnoNativeApi, KeyInfo, SignTxResponse } from "@gnolang/gnonative"; import * as Linking from 'expo-linking'; import { RootState } from "../root-reducer"; +const DEFAULT_GAS_MARGIN = 110; // 1.1% + export interface LinkingState { chainId?: string; remote?: string; @@ -13,6 +15,8 @@ export interface LinkingState { /* The keyinfo of the selected account 'bech32Address' */ keyinfo?: KeyInfo; txInput?: string; + /* Update the transaction with the new estimated gas wanted value */ + updateTx?: boolean; /* The callback URL to return to after each operation */ callback?: string; /* The path of the requested screen */ @@ -30,6 +34,7 @@ const initialState: LinkingState = { reason: undefined, bech32Address: undefined, txInput: undefined, + updateTx: undefined, callback: undefined, path: undefined, hostname: undefined, @@ -81,6 +86,43 @@ export const signTx = createAsyncThunk( + "linking/estimateGas", + async ({ keyInfo, updateTx }, thunkAPI) => { + const gnonative = thunkAPI.extra.gnonative as GnoNativeApi; + const { txInput } = (thunkAPI.getState() as RootState).linking; + const { masterPassword } = (thunkAPI.getState() as RootState).signIn; + + if (!masterPassword) { + throw new Error("No keyInfo found."); + } + + const txJson = decodeURIComponent(txInput || ""); + + await gnonative.activateAccount(keyInfo.name); + await gnonative.setPassword(masterPassword, keyInfo.address); + + // Estimate the gas used + const response = await gnonative.estimateGas(txJson, keyInfo?.address, DEFAULT_GAS_MARGIN, updateTx); + const gasWanted = response.gasWanted as bigint; + console.log("estimateGas: ", gasWanted); + + // Update the transaction + if (updateTx) { + return { tx: response.txJson, gasWanted: gasWanted }; + } + + return { tx: txJson, gasWanted }; + } +); + interface SetLinkResponse { chainId?: string; remote?: string; @@ -88,6 +130,7 @@ interface SetLinkResponse { clientName?: string; bech32Address?: string; txInput?: string; + updateTx?: boolean; callback?: string; path: string; keyinfo?: KeyInfo; @@ -117,6 +160,11 @@ export const setLinkingData = createAsyncThunk state.chainId, selectRemote: (state) => state.remote, selectTxInput: (state) => state.txInput, + selectUpdateTx: (state) => state.updateTx, selectCallback: (state) => state.callback, selectBech32Address: (state) => state.bech32Address, selectClientName: (state) => state.clientName, @@ -180,7 +231,7 @@ const expo_default = 'expo-development-client'; export const { clearLinking } = linkingSlice.actions; -export const { selectTxInput, selectCallback, selectBech32Address, selectClientName, +export const { selectTxInput, selectUpdateTx, selectCallback, selectBech32Address, selectClientName, reasonSelector, selectKeyInfo, selectAction, selectChainId, selectRemote, selectSession, selectSessionWanted } = linkingSlice.selectors; diff --git a/mobile/redux/features/vaultAddSlice.ts b/mobile/redux/features/vaultAddSlice.ts index 7dc48a1..1c65e43 100644 --- a/mobile/redux/features/vaultAddSlice.ts +++ b/mobile/redux/features/vaultAddSlice.ts @@ -1,9 +1,8 @@ import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { GnoNativeApi, KeyInfo } from "@gnolang/gnonative"; +import { Coin, GnoNativeApi, KeyInfo } from "@gnolang/gnonative"; import { ThunkExtra } from "@/providers/redux-provider"; import { Alert } from "react-native"; import { NetworkMetainfo } from "@/types"; -import { Coin } from '@buf/gnolang_gnonative.bufbuild_es/gnonativetypes_pb'; import { RootState } from "../root-reducer"; export enum VaultCreationState { From ead7a549035da42121d7c6e342dc61bc4b4e33e6 Mon Sep 17 00:00:00 2001 From: D4ryl00 Date: Tue, 8 Apr 2025 16:08:30 +0200 Subject: [PATCH 2/4] fix: route to add new vault Signed-off-by: D4ryl00 --- mobile/app/(app)/home/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/app/(app)/home/home.tsx b/mobile/app/(app)/home/home.tsx index a7bd247..c8da2f7 100644 --- a/mobile/app/(app)/home/home.tsx +++ b/mobile/app/(app)/home/home.tsx @@ -57,7 +57,7 @@ export default function Page() { }; const navigateToAddKey = () => { - route.push("/tosign"); + route.push("/home/vault-add-modal"); } const onBookmarkPress = (keyInfo: Vault) => async () => { From 09a25f972c0455c68908570f8e192365b2ca47f1 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Tue, 8 Apr 2025 17:12:54 +0200 Subject: [PATCH 3/4] chore: README: In the tosign API, add update_tx and remove experimental session support Signed-off-by: Jeff Thompson --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2820623..a11e5e9 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,10 @@ land.gno.gnokey://tosign?tx=%7B%22msg%22%3A%5B%7B%22%40type%22%3A%22%2Fvm.m_call - address: bech32 address of whoever you want to sign the transaction. - remote: the connection address for the remote node where the transaction will be sent - chain_id: the chain ID for the remote - - client_name: the name of the app that is calling the Gnokey Mobile app. It will be displayed to the user (if no session). - - reason: the reason behind this action. It will be displayed to the user (if no session). + - client_name: the name of the app that is calling the Gnokey Mobile app. It will be displayed to the user. + - reason: the reason behind this action. It will be displayed to the user. - callback: the URL that Gnokey Mobile will call after signing the tx. - - want_session: boolean, if true and no session then create and return a new session key - - session (optional): the session key json from the previous call with want_session true. If present, sign immediately and return + - update_tx (optional): if "true" then estimate the gas and update gas_wanted in the tx. Example response: @@ -75,8 +74,7 @@ tech.berty.dsocial://post?tx=%7B%22msg%22%3A%5B%7B%22%40type%22%3A%22%2Fvm.m_cal - Base URL: The `callback` from the request. In this case, `tech.berty.dsocial://post` - Parameters (values are percent-escaped, to be decoded with `decodeURIComponent`): - tx: the signed transaction json to pass to `gnonative.broadcastTxCommit(...)` - - session: if want_session then the session key json to use in future `tosign`. Example: `{"expires_at":"2025-03-24T14:35:16.970Z","key":"673768235734692"}` - - status: either "success" or an error such as "session expired" + - status: either "success" or an error ### Testing on iOS simulator From 92c57d2321746477994586e240c61ef1aa39261a Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Tue, 8 Apr 2025 17:38:04 +0200 Subject: [PATCH 4/4] chore: README: Clarify update_tx parameter Signed-off-by: Jeff Thompson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a11e5e9..2c2781a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ land.gno.gnokey://tosign?tx=%7B%22msg%22%3A%5B%7B%22%40type%22%3A%22%2Fvm.m_call - client_name: the name of the app that is calling the Gnokey Mobile app. It will be displayed to the user. - reason: the reason behind this action. It will be displayed to the user. - callback: the URL that Gnokey Mobile will call after signing the tx. - - update_tx (optional): if "true" then estimate the gas and update gas_wanted in the tx. + - update_tx (optional): if "true" then update gas_wanted in the tx with the estimated gas. Example response: