diff --git a/.gitignore b/.gitignore index 3dda774..285fd18 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log* .DS_Store Thumbs.db api-doc.json +example/build \ No newline at end of file diff --git a/example/package.json b/example/package.json index 7db1c89..34a512f 100644 --- a/example/package.json +++ b/example/package.json @@ -12,33 +12,31 @@ }, "dependencies": { "@avnu/gasless-sdk": "file:../dist", - "@starknet-react/chains": "0.1.7", - "@starknet-react/core": "2.8.0", - "ethers": "6.12.1", - "get-starknet-core": "3.3.0", - "react": "18.3.1", - "react-dom": "18.3.1", - "starknet": "6.8.0" + "ethers": "6.13.4", + "get-starknet": "3.3.3", + "react": "19.0.0", + "react-dom": "19.0.0", + "starknet": "6.11.0" }, "devDependencies": { - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "15.0.6", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", - "@types/jest": "29.5.12", - "@types/node": "20.12.8", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.8.0", - "@typescript-eslint/parser": "7.8.0", - "eslint": "8.54.0", - "eslint-config-prettier": "9.0.0", + "@types/jest": "29.5.14", + "@types/node": "22.10.1", + "@types/react": "19.0.1", + "@types/react-dom": "19.0.2", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", + "eslint": "8.57.1", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-prettier": "5.2.1", + "eslint-plugin-simple-import-sort": "12.1.1", "eslint-config-react": "1.1.7", - "eslint-plugin-import": "2.29.0", - "eslint-plugin-prettier": "5.0.1", - "eslint-plugin-simple-import-sort": "10.0.0", - "prettier": "3.2.5", + "prettier": "3.4.2", "react-scripts": "5.0.1", - "typescript": "5.4.5" + "typescript": "5.7.2" }, "browserslist": { "production": [ diff --git a/example/src/App.tsx b/example/src/App.tsx index d6174ee..0777d2c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,11 +1,190 @@ -import React, { FC } from 'react'; -import Form from './Form'; -import StarknetProvider from './StarknetProvider'; - -const App: FC = () => ( - -
- -); +import { FC, useCallback, useEffect, useState } from 'react'; +import { + executeCalls, + fetchAccountCompatibility, + fetchAccountsRewards, + fetchGasTokenPrices, + GaslessCompatibility, + GaslessOptions, + GasTokenPrice, + getGasFeesInGasToken, + PaymasterReward, + SEPOLIA_BASE_URL, +} from '@avnu/gasless-sdk'; +import { formatUnits } from 'ethers'; +import { connect } from 'get-starknet'; +import { Account, AccountInterface, Call, EstimateFeeResponse, Provider, stark, transaction } from 'starknet'; + +const options: GaslessOptions = { baseUrl: SEPOLIA_BASE_URL }; +const NODE_URL = 'https://starknet-sepolia.public.blastapi.io'; +const initialValue: Call[] = [ + { + entrypoint: 'approve', + contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + calldata: ['0x0498E484Da80A8895c77DcaD5362aE483758050F22a92aF29A385459b0365BFE', '0xf', '0x0'], + }, +]; +const isValidJSON = (str: string): boolean => { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } +}; + +const App: FC = () => { + const [account, setAccount] = useState(); + const [loading, setLoading] = useState(false); + const [tx, setTx] = useState(); + const [paymasterRewards, setPaymasterRewards] = useState([]); + const [calls, setCalls] = useState(JSON.stringify(initialValue, null, 2)); + const [gasTokenPrices, setGasTokenPrices] = useState([]); + const [gasTokenPrice, setGasTokenPrice] = useState(); + const [maxGasTokenAmount, setMaxGasTokenAmount] = useState(); + const [gaslessCompatibility, setGaslessCompatibility] = useState(); + const [errorMessage, setErrorMessage] = useState(); + + const handleConnect = async () => { + const starknet = await connect({ modalMode: 'alwaysAsk' }); + if (!starknet) return; + await starknet.enable(); + if (starknet.isConnected && starknet.provider && starknet.account.address) { + setAccount(starknet.account); + } + }; + + useEffect(() => { + if (!account) return; + fetchAccountCompatibility(account.address, options).then(setGaslessCompatibility); + fetchAccountsRewards(account.address, { ...options, protocol: 'gasless-sdk' }).then(setPaymasterRewards); + }, [account]); + + // The account.estimateInvokeFee doesn't work... + const estimateCalls = useCallback( + async (account: AccountInterface, calls: Call[]): Promise => { + const provider = new Provider({ nodeUrl: NODE_URL }); + const contractVersion = await provider.getContractVersion(account.address); + const nonce = await provider.getNonceForAddress(account.address); + const details = stark.v3Details({ skipValidate: true }); + const invocation = { + ...details, + contractAddress: account.address, + calldata: transaction.getExecuteCalldata(calls, contractVersion.cairo), + signature: [], + }; + return provider.getInvokeEstimateFee(invocation, { ...details, nonce, version: 1 }, 'pending', true); + }, + [account], + ); + + // Retrieve estimated gas fees + useEffect(() => { + if (!account || !gasTokenPrice || !gaslessCompatibility) return; + setErrorMessage(undefined); + if (!isValidJSON(calls)) { + setErrorMessage('Invalid calls'); + return; + } + const parsedCalls: Call[] = JSON.parse(calls); + estimateCalls(account, parsedCalls).then((fees) => { + const estimatedGasFeesInGasToken = getGasFeesInGasToken( + BigInt(fees.overall_fee), + gasTokenPrice, + BigInt(fees.gas_price!), + BigInt(fees.data_gas_price ?? '0x1'), + gaslessCompatibility.gasConsumedOverhead, + gaslessCompatibility.dataGasConsumedOverhead, + ); + setMaxGasTokenAmount(estimatedGasFeesInGasToken * BigInt(2)); + }); + }, [calls, account, gasTokenPrice, gaslessCompatibility, estimateCalls]); + + const onClickExecute = async () => { + if (!account) return; + setLoading(true); + setTx(undefined); + return executeCalls( + account, + JSON.parse(calls), + { + gasTokenAddress: gasTokenPrice?.tokenAddress, + maxGasTokenAmount, + }, + options, + ) + .then((response) => { + setTx(response.transactionHash); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + console.error(error); + }); + }; + + useEffect(() => { + fetchGasTokenPrices(options).then(setGasTokenPrices); + }, []); + + if (!account) { + return ; + } + + return ( +
+

Connected with account: {account.address}

+

Execute tx:

+