Skip to content

feat: add gasEstimate function #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ jobs:
uses: actions/checkout@v4

- name: Setup asdf
uses: asdf-vm/actions/setup@v3
uses: ynab/[email protected]
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
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 update gas_wanted in the tx with the estimated gas.

Example response:

Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions mobile/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion mobile/app/(app)/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function Page() {
};

const navigateToAddKey = () => {
route.push("/tosign");
route.push("/home/vault-add-modal");
}

const onBookmarkPress = (keyInfo: Vault) => async () => {
Expand Down
32 changes: 29 additions & 3 deletions mobile/app/(app)/tosign/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string | undefined>(undefined);
const [gasWanted, setGasWanted] = useState<bigint>(BigInt(0));
// const session = useAppSelector(selectSession);
// const sessionWanted = useAppSelector(selectSessionWanted);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -137,8 +163,8 @@ export default function Page() {

<Ruller />

<FormItemInline label="Max Amount" >
<TextBodyWhite>{gasFee} ugnot</TextBodyWhite>
<FormItemInline label="Gas Wanted" >
<TextBodyWhite>{gasWanted?.toString()}</TextBodyWhite>
</FormItemInline>

{/* {sessionWanted &&
Expand Down
43 changes: 25 additions & 18 deletions mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 (
<GnoNativeProvider config={gnoDefaultConfig}>
<ReduxProvider>
<ThemeProvider>
<ThemeProvider2 value={DefaultTheme}>
<LinkingProvider>
<Guard>
<Stack
screenOptions={{
headerShown: false,
headerLargeTitle: true,
headerBackVisible: false,
}}
/>
</Guard>
</LinkingProvider>
</ThemeProvider2>
</ThemeProvider>
</ReduxProvider>
<IndexerProvider config={indexerConfig}>
<ReduxProvider>
<ThemeProvider>
<ThemeProvider2 value={DefaultTheme}>
<LinkingProvider>
<Guard>
<Stack
screenOptions={{
headerShown: false,
headerLargeTitle: true,
headerBackVisible: false,
}}
/>
</Guard>
</LinkingProvider>
</ThemeProvider2>
</ThemeProvider>
</ReduxProvider>
</IndexerProvider>
</GnoNativeProvider>
);
}
34 changes: 4 additions & 30 deletions mobile/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion mobile/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LinkingProvider } from "./linking-provider";
import { ReduxProvider } from "@/providers/redux-provider";
import { IndexerProvider } from "@/providers/indexer-provider";

export { LinkingProvider, ReduxProvider };
export { IndexerProvider, LinkingProvider, ReduxProvider };
80 changes: 80 additions & 0 deletions mobile/providers/indexer-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<GasPriceResponse>;
}

interface ConfigProps {
remote: string;
}

interface IndexerProviderProps {
config: ConfigProps;
children: React.ReactNode;
}

const IndexerContext = createContext<IndexerContextProps | null>(null);

const IndexerProvider: React.FC<IndexerProviderProps> = ({ children, config }) => {
if (!config.remote) {
throw new Error("IndexerProvider requires a remote config");
}

const getGasPrice = async (): Promise<GasPriceResponse> => {
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 <IndexerContext.Provider value={value}>{children}</IndexerContext.Provider>;
};

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 };
Loading