diff --git a/app/next/package.json b/app/next/package.json index c15df1b..f772b19 100644 --- a/app/next/package.json +++ b/app/next/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --experimental-https --hostname wisemrmusa.fun", + "dev": "next dev --experimental-https --hostname wisemrmusa.fun --port 3005", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/app/next/pages/paymaster.tsx b/app/next/pages/paymaster.tsx new file mode 100644 index 0000000..7720e96 --- /dev/null +++ b/app/next/pages/paymaster.tsx @@ -0,0 +1,332 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { createPaymasterClient, createWalletClient, custom, http, Signature, WalletClient, withRetry } from 'starkweb'; +import { mainnet, sepolia } from 'starkweb/chains'; +import 'starkweb/window' + + +interface TransactionCall { + entrypoint: string; + contractAddress: string; + calldata: string[]; +} + +interface BuildTransactionResult { + types: any; // Update this type based on actual response structure + primaryType: any; // Update this type based on actual response structure + domain: any; // Update this type based on actual response structure + message: any; // Update this type based on actual response structure +} + +export default function PaymasterPage() { + + + + + // Group all useState hooks at the top + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [network, setNetwork] = useState<'mainnet' | 'sepolia'>('sepolia'); + const [accountAddress, setAccountAddress] = useState(''); + const [accountRewards, setAccountRewards] = useState(null); + const [rewardsLoading, setRewardsLoading] = useState(false); + const [rewardsError, setRewardsError] = useState(null); + const [transactionResult, setTransactionResult] = useState(null); + const [buildingTransaction, setBuildingTransaction] = useState(false); + const [transactionError, setTransactionError] = useState(null); + const [signingTransaction, setSigningTransaction] = useState(false); + const [walletClient, setWalletClient] = useState(null); + const [ signature, setSignature] = useState(null); + + useEffect(() => { + // Only run on client-side + if (typeof window === 'undefined') return; + const walletClient = createWalletClient({ + chain: network === 'mainnet' ? mainnet : sepolia, + transport: custom(window.starknet_argentX!), + }); + setWalletClient(walletClient); + + async function fetchPaymasterStatus() { + try { + setLoading(true); + const chain = network === 'mainnet' ? mainnet : sepolia; + const paymasterClient = createPaymasterClient({ + chain, + transport: http(`http://localhost:3003/paymaster/${network}`), + }); + + const paymasterStatus = await paymasterClient.getPaymasterStatus(); + console.log(paymasterStatus); + setStatus(paymasterStatus); + setError(null); + } catch (err) { + console.error('Error fetching paymaster status:', err); + setError('Failed to fetch paymaster status'); + } finally { + setLoading(false); + } + } + + fetchPaymasterStatus(); + }, [network]); + + const fetchAccountRewards = async () => { + if (!accountAddress) { + setRewardsError('Please enter an account address'); + return; + } + + try { + setRewardsLoading(true); + const chain = network === 'mainnet' ? mainnet : sepolia; + const paymasterClient = createPaymasterClient({ + chain, + transport: http(`http://localhost:3003/paymaster/${network}`), + }); + + const rewards = await paymasterClient.getAccountRewards({ + accountAddress, + }); + + console.log('Account rewards:', rewards); + setAccountRewards(rewards); + setRewardsError(null); + } catch (err) { + console.error('Error fetching account rewards:', err); + setRewardsError('Failed to fetch account rewards'); + setAccountRewards(null); + } finally { + setRewardsLoading(false); + } + }; + + const buildTransaction = async () => { + // Check if we're on client-side + if (typeof window === 'undefined') return; + + try { + setBuildingTransaction(true); + const chain = network === 'mainnet' ? mainnet : sepolia; + const paymasterClient = createPaymasterClient({ + chain, + transport: http(`http://localhost:3003/paymaster/${network}`), + }); + + const transaction = await paymasterClient.buildTypedData({ + userAddress: "0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91", + calls: [ + { + entrypoint: "approve", + contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + calldata: ['0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91', '0xf', '0x0'], + }, + ], + }); + + console.log('Built transaction:', transaction); + setTransactionResult(transaction as unknown as BuildTransactionResult); + setTransactionError(null); + } catch (err) { + console.error('Error building transaction:', err); + setTransactionError('Failed to build transaction'); + setTransactionResult(null); + } finally { + setBuildingTransaction(false); + } + }; + + const signTransaction = async () => { + if (!transactionResult) { + setTransactionError('No transaction to sign'); + return; + } + + try { + setSigningTransaction(true); + // const signature = await useSignTypedData(transactionResult.typedData); + const permissions = await walletClient?.getPermissions({}); + console.log('Permissions:', permissions); + const signature = await walletClient?.signTypedData({ + typed_data: transactionResult, + } ); + // const signature = await walletClient?.signTypedData({ + // typed_data: { + // types: transactionResult.types, + // primaryType: transactionResult.primaryType, + // domain: transactionResult.domain, + // message: transactionResult.message + // } + // } ); + console.log('Signed transaction:', signature); + setSignature(signature); + } catch (err) { + console.error('Error signing transaction:', err); + setTransactionError('Failed to sign transaction'); + setTransactionResult(null); + } finally { + setSigningTransaction(false); + } + }; + + const executeTransaction = async () => { + if (!walletClient || !signature || !transactionResult) { + setTransactionError('Missing required data for execution'); + return; + } + + try { + const chain = network === 'mainnet' ? mainnet : sepolia; + const paymasterClient = createPaymasterClient({ + chain, + transport: http(`http://localhost:3003/paymaster/${network}`), + }); + + const executionResult = await paymasterClient.executeTransaction({ + userAddress: "0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91", + typedData: JSON.stringify(transactionResult), + signature: signature, + + }); + + console.log('Transaction executed:', executionResult); + return executionResult; + } catch (err) { + console.error('Error executing transaction:', err); + setTransactionError('Failed to execute transaction'); + throw err; + } + }; + + return ( +
+

Starknet Paymaster

+ +
+ +
+ + +
+
+ +
+

Paymaster Status

+ + {loading ? ( +
Loading...
+ ) : error ? ( +
{error}
+ ) : ( +
+            {JSON.stringify(status, null, 2)}
+          
+ )} +
+ +
+

Account Rewards

+ +
+ +
+ setAccountAddress(e.target.value)} + className="flex-1 px-4 py-2 border rounded dark:bg-gray-700" + placeholder="Enter account address" + /> + +
+
+ + {rewardsError && ( +
{rewardsError}
+ )} + + {accountRewards && ( +
+            {JSON.stringify(accountRewards, null, 2)}
+          
+ )} +
+ +
+

Build Transaction

+ + + + {transactionError && ( +
{transactionError}
+ )} + + {transactionResult && ( + <> +
+
+              {JSON.stringify(transactionResult, null, 2)}
+            
+ +
+ + {signature && ( +
+              {JSON.stringify(signature, null, 2)}
+            
+ )} + + )} +
+ + {signature && ( + + )} +
+ ); +} diff --git a/app/starkweb-client/src/index.ts b/app/starkweb-client/src/index.ts index acfd4d3..01209e5 100644 --- a/app/starkweb-client/src/index.ts +++ b/app/starkweb-client/src/index.ts @@ -1,54 +1,47 @@ -/** - * StarkWeb Client - A client for interacting with Starknet - */ +import { createPaymasterClient, http, parseEther } from "starkweb" +import { mainnet, sepolia } from "starkweb/chains" -// Import from the starkweb workspace package -import { - createPublicClient, - createWalletClient, - custom, - erc20Abi, - http, -} from 'starkweb'; -import { getStarknetId, getStarknetIdName } from 'starkweb/actions'; -import { mainnet, sepolia } from 'starkweb/chains'; +const paymasterClient = createPaymasterClient({ + chain: sepolia, + transport: http('http://localhost:3000/gasless/sepolia'), + type: 'paymasterClient', +}) +// get paymaster status +const status = await paymasterClient.getPaymasterStatus() +// console.log(status) -const publicClient = createPublicClient({ - chain: mainnet, - transport: http('https://starknet-mainnet.infura.io/v3/db72641028ee47f5b18bcbb791a3f829'), -}); -const id = await getStarknetId(publicClient, { - domain: 'solene.stark', -}) -// const name = await getStarknetIdName(publicClient, { -// address: '0x061b6c0a78f9edf13cea17b50719f3344533fadd470b8cb29c2b4318014f52d3', +// // check account compatibility +// const compatibility = await paymasterClient.checkAccountCompatibility({ +// accountAddress: "0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91", // }) -console.log(id) -// console.log(name) +// console.log(compatibility) -// const result = await publicClient.readContracts({ -// contracts: [ -// { -// abi: erc20Abi, -// functionName: 'balanceOf', -// args: -// { -// account: '0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91', -// }, -// address: -// '0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D', -// }, -// { -// abi: erc20Abi, -// functionName: 'balanceOf', -// args: -// { -// account: '0x034abecf49cedc634d0c3145da7b1caea99d8d4f2da5b5d41e532ea05192d523', -// }, -// address: -// '0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D', -// }, -// ], -// }) +// get gas token prices +const gasTokenPrices = await paymasterClient.getGasTokenPrices() +// console.log(gasTokenPrices) + +// get account rewards +const accountRewards = await paymasterClient.getAccountRewards({ + accountAddress: "0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91", +}) +// console.log(accountRewards) -// console.log(result); +// Check if account is compatible with paymaster +const isCompatible = await paymasterClient.checkAccountCompatibility({ + accountAddress: "0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91", +}) +console.log(isCompatible) + + +// build transaction +const transaction = await paymasterClient.buildTypedData({ + userAddress: "0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91", + calls: [ + { + entrypoint: "approve", + contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7', + calldata: ['0x005c475b6089156c0CD4Fc9d64De149992431c442AF882d6332e7c736c99DE91', '0xf', '0x0'], + }, + ], +}) +console.log(transaction) diff --git a/app/starkweb-client/src/paymaster.ts b/app/starkweb-client/src/paymaster.ts new file mode 100644 index 0000000..f0ec9fc --- /dev/null +++ b/app/starkweb-client/src/paymaster.ts @@ -0,0 +1,12 @@ +import { createPaymasterClient, http } from "starkweb" +import { mainnet } from "starkweb/chains" + +const paymasterClient = createPaymasterClient({ + chain: mainnet, + transport: http(), + type: 'paymasterClient', +}) + +const status = await paymasterClient.getPaymasterStatus() +console.log(status) + diff --git a/packages/starkweb/src/actions/paymaster/buildTypedData.ts b/packages/starkweb/src/actions/paymaster/buildTypedData.ts new file mode 100644 index 0000000..86a5274 --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/buildTypedData.ts @@ -0,0 +1,28 @@ +import type { Call } from '../../strk-types/lib.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { Chain } from '../../types/chain.js' +import type { TypedData } from '@starknet-io/types-js' + +export type BuildTypedDataParameters = { + userAddress: string + calls: Call[] + gasTokenAddress?: string + maxGasTokenAmount?: string + accountClassHash?: string +} + +export type BuildTypedDataReturnType = TypedData + +export type BuildTypedDataErrorType = Error + +export async function buildTypedData( + client: Client, + parameters: BuildTypedDataParameters +): Promise { + const response = await client.request({ + method: 'pm_buildTypedData', + params: parameters + }) + return response as BuildTypedDataReturnType +} \ No newline at end of file diff --git a/packages/starkweb/src/actions/paymaster/checkAccountCompatibility.ts b/packages/starkweb/src/actions/paymaster/checkAccountCompatibility.ts new file mode 100644 index 0000000..9d300d0 --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/checkAccountCompatibility.ts @@ -0,0 +1,23 @@ +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { Chain } from '../../types/chain.js' +import type { GaslessCompatibility } from '../../types/paymaster.js' +import type { ADDRESS } from '../../types/components.js' +export type CheckAccountCompatibilityParameters = { + accountAddress: ADDRESS +} + +export type CheckAccountCompatibilityReturnType = GaslessCompatibility + +export type CheckAccountCompatibilityErrorType = Error + +export async function checkAccountCompatibility( + client: Client, + { accountAddress }: CheckAccountCompatibilityParameters +): Promise { + const response = await client.request({ + method: 'pm_checkAccountCompatibility', + params: { accountAddress } + }) + return response as CheckAccountCompatibilityReturnType +} \ No newline at end of file diff --git a/packages/starkweb/src/actions/paymaster/executeTransaction.ts b/packages/starkweb/src/actions/paymaster/executeTransaction.ts new file mode 100644 index 0000000..c8b6d81 --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/executeTransaction.ts @@ -0,0 +1,27 @@ +import type { Signature } from "@starknet-io/types-js" +import type { DeploymentData, InvokeResponse } from "../../types/paymaster.js" +import type { Transport } from "../../clients/transports/createTransport.js" +import type { Client } from "../../clients/createClient.js" +import type { Chain } from "../../types/chain.js" + +export type ExecuteTransactionParameters = { + userAddress: string + typedData: string + signature: Signature + deploymentData?: DeploymentData +} + +export type ExecuteTransactionReturnType = InvokeResponse + +export type ExecuteTransactionErrorType = Error + +export async function executeTransaction( + client: Client, + parameters: ExecuteTransactionParameters +): Promise { + const response = await client.request({ + method: 'pm_executeTransaction', + params: parameters + }) + return response as ExecuteTransactionReturnType +} \ No newline at end of file diff --git a/packages/starkweb/src/actions/paymaster/getAccountRewards.ts b/packages/starkweb/src/actions/paymaster/getAccountRewards.ts new file mode 100644 index 0000000..0875bda --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/getAccountRewards.ts @@ -0,0 +1,24 @@ +import type { PaymasterReward } from '../../types/paymaster.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { Chain } from '../../types/chain.js' +import type { ADDRESS } from '../../types/components.js' + +export type GetAccountRewardsParameters = { + accountAddress: ADDRESS +} + +export type GetAccountRewardsReturnType = PaymasterReward[] + +export type GetAccountRewardsErrorType = Error + +export async function getAccountRewards( + client: Client, + { accountAddress }: GetAccountRewardsParameters +): Promise { + const response = await client.request({ + method: 'pm_getAccountRewards', + params: { accountAddress } + }) + return response as GetAccountRewardsReturnType +} \ No newline at end of file diff --git a/packages/starkweb/src/actions/paymaster/getGasTokenPrices.ts b/packages/starkweb/src/actions/paymaster/getGasTokenPrices.ts new file mode 100644 index 0000000..325532d --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/getGasTokenPrices.ts @@ -0,0 +1,21 @@ +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { Chain } from '../../types/chain.js' +import type { GasTokenPrice } from '../../types/paymaster.js' + +export type GetGasTokenPricesParameters = undefined + +export type GetGasTokenPricesReturnType = GasTokenPrice[] + +export type GetGasTokenPricesErrorType = Error + +export async function getGasTokenPrices( + client: Client, + args: GetGasTokenPricesParameters, +): Promise { + const response = await client.request({ + method: 'pm_getGasTokenPrices', + params: args, + }) + return response as GetGasTokenPricesReturnType +} \ No newline at end of file diff --git a/packages/starkweb/src/actions/paymaster/getPaymasterStatus.ts b/packages/starkweb/src/actions/paymaster/getPaymasterStatus.ts new file mode 100644 index 0000000..307cf82 --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/getPaymasterStatus.ts @@ -0,0 +1,21 @@ + + +import type { Client } from '../../clients/createClient.js'; +import type { Transport } from '../../clients/transports/createTransport.js'; +import type { Chain } from '../../types/chain.js'; +import type { GaslessStatus } from '../../types/paymaster.js'; + +export type GetPaymasterStatusParameters = undefined +export type GetPaymasterStatusReturnType = GaslessStatus +export type GetPaymasterStatusErrorType = any + +export async function getPaymasterStatus( + client: Client, + args: GetPaymasterStatusParameters, +): Promise { + const result = await client.request({ + method: 'pm_getPaymasterStatus', + params: args, + }) + return result +} diff --git a/packages/starkweb/src/actions/paymaster/index.ts b/packages/starkweb/src/actions/paymaster/index.ts new file mode 100644 index 0000000..637229c --- /dev/null +++ b/packages/starkweb/src/actions/paymaster/index.ts @@ -0,0 +1,53 @@ +export { + getPaymasterStatus, + type GetPaymasterStatusParameters, + type GetPaymasterStatusReturnType, + type GetPaymasterStatusErrorType, +} from './getPaymasterStatus.js' + +export { + checkAccountCompatibility, + type CheckAccountCompatibilityParameters, + type CheckAccountCompatibilityReturnType, + type CheckAccountCompatibilityErrorType, +} from './checkAccountCompatibility.js' + +export { + getGasTokenPrices, + type GetGasTokenPricesParameters, + type GetGasTokenPricesReturnType, + type GetGasTokenPricesErrorType, +} from './getGasTokenPrices.js' + +export { + getAccountRewards, + type GetAccountRewardsParameters, + type GetAccountRewardsReturnType, + type GetAccountRewardsErrorType, +} from './getAccountRewards.js' + +export { + buildTypedData, + type BuildTypedDataParameters, + type BuildTypedDataReturnType, + type BuildTypedDataErrorType, +} from './buildTypedData.js' + +export { + executeTransaction, + type ExecuteTransactionParameters, + type ExecuteTransactionReturnType, + type ExecuteTransactionErrorType, +} from './executeTransaction.js' + +// export type { +// ADDRESS, +// GaslessStatus, +// GaslessCompatibility, +// GasTokenPrice, +// PaymasterReward, +// TypedData, +// DeploymentData, +// InvokeResponse, +// Signature, +// } from './types.js' \ No newline at end of file diff --git a/packages/starkweb/src/clients/createPaymasterClient.ts b/packages/starkweb/src/clients/createPaymasterClient.ts new file mode 100644 index 0000000..f40ed5f --- /dev/null +++ b/packages/starkweb/src/clients/createPaymasterClient.ts @@ -0,0 +1,52 @@ +import type { Address } from 'abitype' +import type { Account, ParseAccount } from '../types/account.js' +import type { Chain } from '../types/chain.js' +import type { RpcSchema } from '../types/eip1193.js' +import type { Prettify } from '../types/utils.js' +import { + type Client, + type ClientConfig, + createClient, + type CreateClientErrorType, +} from './createClient.js' +import { type PaymasterActions, paymasterActions } from './decorators/paymaster.js' +import type { Transport } from './transports/createTransport.js' +import type { ErrorType } from '../errors/utils.js' + +export type PaymasterClientConfig< + transport extends Transport, + chain extends Chain, + accountOrAddress extends Account | Address | undefined = undefined, + rpcSchema extends RpcSchema | undefined = undefined, +> = ClientConfig + +export type PaymasterClient< + transport extends Transport, + chain extends Chain, + accountOrAddress extends Account | Address | undefined = undefined, + rpcSchema extends RpcSchema | undefined = undefined, +> = Client, rpcSchema, PaymasterActions> + +export type CreatePaymasterClientErrorType = CreateClientErrorType | ErrorType + +export function createPaymasterClient< + transport extends Transport, + chain extends Chain, + accountOrAddress extends Account | Address | undefined = undefined, + rpcSchema extends RpcSchema | undefined = undefined, +>( + parameters: PaymasterClientConfig, +): Prettify< + Client, rpcSchema, PaymasterActions> +> { + const { key = 'paymaster', name = 'Paymaster Client' } = parameters + const client = createClient({ + ...parameters, + key, + name, + type: 'paymasterClient', + }) + return client.extend(paymasterActions) as Prettify< + Client, rpcSchema, PaymasterActions> + > +} \ No newline at end of file diff --git a/packages/starkweb/src/clients/decorators/paymaster.ts b/packages/starkweb/src/clients/decorators/paymaster.ts new file mode 100644 index 0000000..a7cf30d --- /dev/null +++ b/packages/starkweb/src/clients/decorators/paymaster.ts @@ -0,0 +1,82 @@ +import type { Client } from '../createClient.js' +import type { GaslessStatus, GaslessCompatibility, GasTokenPrice, DeploymentData, InvokeResponse, PaymasterReward } from '../../types/paymaster.js' +import type { Signature } from '@starknet-io/types-js' +import type { ADDRESS } from '../../types/components.js' + +import { + getPaymasterStatus, + checkAccountCompatibility, + getGasTokenPrices, + getAccountRewards, + buildTypedData, + executeTransaction, + type BuildTypedDataParameters, + type BuildTypedDataReturnType, +} from '../../actions/paymaster/index.js' + + +export type PaymasterActions = { + /** + * Gets the current status of the paymaster service. + * + * @returns The paymaster service status. + */ + getPaymasterStatus: () => Promise + + /** + * Checks if an account is compatible with the paymaster service. + * + * @param args - The account address to check. + * @returns The compatibility status of the account. + */ + checkAccountCompatibility: (args: { accountAddress: ADDRESS }) => Promise + + /** + * Gets the current gas token prices supported by the paymaster. + * + * @returns An array of gas token prices. + */ + getGasTokenPrices: () => Promise + + /** + * Gets the rewards available for an account. + * + * @param args - The account address to check. + * @returns The rewards available for the account. + */ + getAccountRewards: (args: { accountAddress: ADDRESS }) => Promise + + /** + * Builds typed data for a transaction to be executed via the paymaster. + * + * @param args - The parameters for building typed data. + * @returns The typed data for the transaction. + */ + buildTypedData: (args: BuildTypedDataParameters) => Promise + + /** + * Executes a transaction via the paymaster service. + * + * @param args - The transaction parameters including user address, typed data, and signature. + * @returns The transaction response. + */ + executeTransaction: (args: { + userAddress: string + typedData: string + signature: Signature + deploymentData?: DeploymentData + }) => Promise +} + +export function paymasterActions( + client: Client +): PaymasterActions { + return { + getPaymasterStatus: () => getPaymasterStatus(client, undefined), + checkAccountCompatibility: (args) => checkAccountCompatibility(client, args), + getGasTokenPrices: () => getGasTokenPrices(client, undefined), + getAccountRewards: (args) => getAccountRewards(client, args), + buildTypedData: (args) => buildTypedData(client, args), + executeTransaction: (args) => executeTransaction(client, args), + } +} diff --git a/packages/starkweb/src/core/actions/buildTypedData.ts b/packages/starkweb/src/core/actions/buildTypedData.ts new file mode 100644 index 0000000..afebb0c --- /dev/null +++ b/packages/starkweb/src/core/actions/buildTypedData.ts @@ -0,0 +1,38 @@ +import { + type BuildTypedDataErrorType as strkjs_BuildTypedDataErrorType, + type BuildTypedDataParameters as strkjs_BuildTypedDataParameters, + type BuildTypedDataReturnType as strkjs_BuildTypedDataReturnType, + buildTypedData as strkjs_buildTypedData, +} from "../../actions/paymaster/buildTypedData.js"; +import type { Hex } from "../../types/misc.js"; + +import type { Config } from "../createConfig.js"; +import type { ChainIdParameter } from "../types/properties.js"; +import type { Evaluate } from "../types/utils.js"; +import { getAction } from "../utils/getAction.js"; + +export type BuildTypedDataParameters = Evaluate< + strkjs_BuildTypedDataParameters & ChainIdParameter +>; + +export type BuildTypedDataReturnType = Evaluate< + strkjs_BuildTypedDataReturnType & { + chainId: Hex; + } +>; + +export type BuildTypedDataErrorType = strkjs_BuildTypedDataErrorType; + +export async function buildTypedData( + config: Config, + parameters: BuildTypedDataParameters +): Promise { + const { chainId, ...rest } = parameters; + const client = config.getClient({ chainId }); + const action = getAction( + client, + strkjs_buildTypedData, + "buildTypedData" + ); + return action(rest) as Promise; +} diff --git a/packages/starkweb/src/core/actions/checkAccountCompatibility.ts b/packages/starkweb/src/core/actions/checkAccountCompatibility.ts new file mode 100644 index 0000000..57ae66e --- /dev/null +++ b/packages/starkweb/src/core/actions/checkAccountCompatibility.ts @@ -0,0 +1,39 @@ +import { + type CheckAccountCompatibilityErrorType as strkjs_CheckAccountCompatibilityErrorType, + type CheckAccountCompatibilityParameters as strkjs_CheckAccountCompatibilityParameters, + type CheckAccountCompatibilityReturnType as strkjs_CheckAccountCompatibilityReturnType, + checkAccountCompatibility as strkjs_checkAccountCompatibility, +} from "../../actions/paymaster/checkAccountCompatibility.js"; +import type { Hex } from "../../types/misc.js"; + +import type { Config } from "../createConfig.js"; +import type { ChainIdParameter } from "../types/properties.js"; +import type { Evaluate } from "../types/utils.js"; +import { getAction } from "../utils/getAction.js"; + +export type CheckAccountCompatibilityParameters = Evaluate< + strkjs_CheckAccountCompatibilityParameters & ChainIdParameter +>; + +export type CheckAccountCompatibilityReturnType = Evaluate< + strkjs_CheckAccountCompatibilityReturnType & { + chainId: Hex; + } +>; + +export type CheckAccountCompatibilityErrorType = + strkjs_CheckAccountCompatibilityErrorType; + +export async function checkAccountCompatibility( + config: Config, + parameters: CheckAccountCompatibilityParameters +): Promise { + const { chainId, ...rest } = parameters; + const client = config.getClient({ chainId }); + const action = getAction( + client, + strkjs_checkAccountCompatibility, + "checkAccountCompatibility" + ); + return action(rest) as Promise; +} diff --git a/packages/starkweb/src/core/actions/getAccountRewards.ts b/packages/starkweb/src/core/actions/getAccountRewards.ts new file mode 100644 index 0000000..0134f2d --- /dev/null +++ b/packages/starkweb/src/core/actions/getAccountRewards.ts @@ -0,0 +1,39 @@ +import { + type GetAccountRewardsErrorType as strkjs_GetAccountRewardsErrorType, + type GetAccountRewardsParameters as strkjs_GetAccountRewardsParameters, + type GetAccountRewardsReturnType as strkjs_GetAccountRewardsReturnType, + getAccountRewards as strkjs_getAccountRewards, +} from "../../actions/paymaster/getAccountRewards.js"; +import type { Hex } from "../../types/misc.js"; + +import type { Config } from "../createConfig.js"; +import type { ChainIdParameter } from "../types/properties.js"; +import type { Evaluate } from "../types/utils.js"; +import { getAction } from "../utils/getAction.js"; + +export type GetAccountRewardsParameters = Evaluate< + strkjs_GetAccountRewardsParameters & ChainIdParameter +>; + +export type GetAccountRewardsReturnType = Evaluate< + strkjs_GetAccountRewardsReturnType & { + chainId: Hex; + } +>; + +export type GetAccountRewardsErrorType = + strkjs_GetAccountRewardsErrorType; + +export async function getAccountRewards( + config: Config, + parameters: GetAccountRewardsParameters +): Promise { + const { chainId, ...rest } = parameters; + const client = config.getClient({ chainId }); + const action = getAction( + client, + strkjs_getAccountRewards, + "getAccountRewards" + ); + return action(rest) as Promise; +} diff --git a/packages/starkweb/src/core/actions/getPaymasterStatus.ts b/packages/starkweb/src/core/actions/getPaymasterStatus.ts new file mode 100644 index 0000000..a22dc7d --- /dev/null +++ b/packages/starkweb/src/core/actions/getPaymasterStatus.ts @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { createPaymasterClient } from '../../exports/starkweb.js'; +import { http } from '../../exports/starkweb.js'; +import { mainnet, sepolia } from '../../exports/chains.js'; +import type { GaslessStatus } from '../../types/paymaster.js'; + +/** + * Fetches the current status of the Paymaster. + * + * @param {('mainnet' | 'sepolia')} network - The network to connect to, either 'mainnet' or 'sepolia'. + * @param {string} [url='http://localhost:3003/paymaster'] - Optional URL for the Paymaster service. Defaults to 'http://localhost:3003/paymaster. + * @returns {Object} An object containing the status, loading state, and error message. + */ +export const fetchPaymasterStatus = async (network: 'mainnet' | 'sepolia', url: string = 'http://localhost:3003/paymaster') => { + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const chain = network === 'mainnet' ? mainnet : sepolia; + const paymasterClient = createPaymasterClient({ + chain, + transport: http(`${url}/paymaster/${network}`), + }); + + setLoading(true); + try { + const paymasterStatus = await paymasterClient.getPaymasterStatus(); + setStatus(paymasterStatus); + } catch (err) { + setError('Failed to fetch paymaster status'); + } finally { + setLoading(false); + } + + return { status, loading, error }; +}; \ No newline at end of file diff --git a/packages/starkweb/src/core/exports/actions.ts b/packages/starkweb/src/core/exports/actions.ts index 5a8f2e9..03eb395 100644 --- a/packages/starkweb/src/core/exports/actions.ts +++ b/packages/starkweb/src/core/exports/actions.ts @@ -411,3 +411,26 @@ export { type VerifyTypedDataParameters, type VerifyTypedDataReturnType, } from "../actions/verifyTypedData.js"; + + +export { + fetchAccountRewards +} from "../actions/getAccountRewards.js"; + +export { + checkAccountCompatibility +} from "../actions/checkAccountCompatibility.js"; + +export { + fetchPaymasterStatus, +} from "../actions/getPaymasterStatus.js"; + +export { + buildTypedData, +} from '../../actions/paymaster/buildTypedData.js'; +export { + getGasTokenPrices, +} from '../../actions/paymaster/getGasTokenPrices.js'; +export { + executeTransaction, +} from '../../actions/paymaster/executeTransaction.js'; \ No newline at end of file diff --git a/packages/starkweb/src/core/query/buildTypedData.ts b/packages/starkweb/src/core/query/buildTypedData.ts new file mode 100644 index 0000000..27a6698 --- /dev/null +++ b/packages/starkweb/src/core/query/buildTypedData.ts @@ -0,0 +1,27 @@ + +import type { QueryOptions } from '@tanstack/query-core' + +import { + type BuildTypedDataErrorType, + type BuildTypedDataParameters, + type BuildTypedDataReturnType, + buildTypedData, +} from "../actions/buildTypedData.js"; +import type { Config } from "../createConfig.js"; +import { filterQueryOptions } from "./utils.js"; + +export function buildTypedDataQueryOptions( + config: Config, + parameters: BuildTypedDataParameters +): QueryOptions { + return { + queryKey: buildTypedDataQueryKey(parameters), + queryFn: () => buildTypedData(config, parameters), + } +} + +export function buildTypedDataQueryKey(parameters: BuildTypedDataParameters) { + return ['buildTypedData', filterQueryOptions(parameters)] as const +} + +export type BuildTypedDataQueryKey = ReturnType diff --git a/packages/starkweb/src/core/utils/getAction.ts b/packages/starkweb/src/core/utils/getAction.ts index b8974c6..cb0960a 100644 --- a/packages/starkweb/src/core/utils/getAction.ts +++ b/packages/starkweb/src/core/utils/getAction.ts @@ -15,6 +15,7 @@ import type { RpcSchema } from '../../types/snip1193.js' import type { Client } from '../../clients/createClient.js' import type { WalletActions } from '../../clients/decorators/wallet.js' import type { PublicActions } from '../../clients/decorators/public.js' +import type { PaymasterActions } from 'src/exports/starkweb.js' /** * Retrieves and returns an action from the client (if exists), and falls @@ -38,7 +39,7 @@ export function getAction< // Some minifiers drop `Function.prototype.name`, or replace it with short letters, // meaning that `actionFn.name` will not always work. For that case, the consumer // needs to pass the name explicitly. - name: keyof PublicActions | keyof WalletActions, + name: keyof PublicActions | keyof WalletActions | keyof PaymasterActions, ): (parameters: parameters) => returnType { const action_implicit = client[actionFn.name] if (typeof action_implicit === 'function') diff --git a/packages/starkweb/src/exports/chains/utils.ts b/packages/starkweb/src/exports/chains/utils.ts index 489cb78..08ac4af 100644 --- a/packages/starkweb/src/exports/chains/utils.ts +++ b/packages/starkweb/src/exports/chains/utils.ts @@ -16,3 +16,5 @@ export { getChainContractAddress, } from '../../utils/chain/getChainContractAddress.js' export { type AddStarknetChainParameters } from '../../utils/chain/addStarknetChainParameters.js' +export { mainnet} from "../../chains/definitions/mainnet.js" +export { sepolia} from "../../chains/definitions/sepolia.js" diff --git a/packages/starkweb/src/exports/starkweb.ts b/packages/starkweb/src/exports/starkweb.ts index bdc95fc..01e5226 100644 --- a/packages/starkweb/src/exports/starkweb.ts +++ b/packages/starkweb/src/exports/starkweb.ts @@ -110,6 +110,13 @@ export { type CreateWalletClientErrorType, createWalletClient, } from '../clients/createWalletClient.js' + + export { + type PaymasterClient, + type PaymasterClientConfig, + type CreatePaymasterClientErrorType, + createPaymasterClient, + } from '../clients/createPaymasterClient.js' export { type PublicActions, @@ -120,6 +127,11 @@ export { type WalletActions, walletActions, } from '../clients/decorators/wallet.js' + + export { + type PaymasterActions, + paymasterActions, + } from '../clients/decorators/paymaster.js' export { type Transport, diff --git a/packages/starkweb/src/react/exports/index.ts b/packages/starkweb/src/react/exports/index.ts index 35b050e..dee5b9b 100644 --- a/packages/starkweb/src/react/exports/index.ts +++ b/packages/starkweb/src/react/exports/index.ts @@ -364,6 +364,8 @@ export { useWriteContract, } from '../hooks/useWriteContract.js' + + //////////////////////////////////////////////////////////////////////////////// // Hydrate //////////////////////////////////////////////////////////////////////////////// @@ -373,6 +375,23 @@ export { Hydrate, } from '../hydrate.js' + +//////////////////////////////////////////////////////////////////////////////// +// Paymaster +//////////////////////////////////////////////////////////////////////////////// + +export { + type UsePaymasterProps, + type UsePaymasterReturn, + usePaymaster +} from '../hooks/usePaymaster.js' + +export { + type UseExecuteTransactionProps, + type UseExecuteTransactionReturn, + useExecuteTransaction, +} from '../hooks/useExecuteTransaction.js' + //////////////////////////////////////////////////////////////////////////////// // @wagmi/core //////////////////////////////////////////////////////////////////////////////// diff --git a/packages/starkweb/src/react/hooks/useExecuteTransaction.ts b/packages/starkweb/src/react/hooks/useExecuteTransaction.ts new file mode 100644 index 0000000..e505c29 --- /dev/null +++ b/packages/starkweb/src/react/hooks/useExecuteTransaction.ts @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { createPaymasterClient } from '../../exports/starkweb.js'; +import { http } from '../../exports/starkweb.js'; +import { mainnet, sepolia } from '../../exports/chains.js'; +import type { InvokeResponse } from '../../types/paymaster.js'; +import type { ADDRESS, SIGNATURE } from '../../types/components.js'; + +export type UseExecuteTransactionProps = { + network: 'mainnet' | 'sepolia'; + userAddress: ADDRESS; + typedData: string; + signature: SIGNATURE; + clientUrl?: string; +}; + +export type UseExecuteTransactionReturn = { + loading: boolean; + error: string | null; + executeTransaction: () => Promise; +}; + +export const useExecuteTransaction = ({ + network, + userAddress, + typedData, + signature, + clientUrl, +}: UseExecuteTransactionProps): UseExecuteTransactionReturn => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const executeTransaction = async (): Promise => { + setLoading(true); + setError(null); + const chain = network === 'mainnet' ? mainnet : sepolia; + const paymasterClient = createPaymasterClient({ + chain, + transport: http(clientUrl || `http://localhost:3003/paymaster/${network}`), + }); + + try { + const response = await paymasterClient.executeTransaction({ + userAddress, + typedData, + signature, + }); + return response; + } catch (err) { + setError('Failed to execute transaction'); + console.error(err); + return null; + } finally { + setLoading(false); + } + }; + + return { loading, error, executeTransaction }; +}; \ No newline at end of file diff --git a/packages/starkweb/src/react/hooks/usePaymaster.ts b/packages/starkweb/src/react/hooks/usePaymaster.ts new file mode 100644 index 0000000..f4ac4ac --- /dev/null +++ b/packages/starkweb/src/react/hooks/usePaymaster.ts @@ -0,0 +1,117 @@ +"use client"; +import { useState, useEffect, useCallback } from "react"; +import { + fetchAccountRewards, + checkAccountCompatibility, + fetchPaymasterStatus, + getGasTokenPrices, + buildTypedData, + executeTransaction, +} from "../../core/exports/actions.js"; +import type { + GaslessStatus, + GaslessCompatibility, + PaymasterReward, +} from "../../types/paymaster.js"; +import type { ADDRESS } from "../../types/components.js"; +import { createPaymasterClient } from "../../clients/createPaymasterClient.js"; +import { http } from "../../clients/transports/http.js"; +import { mainnet } from "../../chains/definitions/mainnet.js"; +import { sepolia } from "../../chains/definitions/sepolia.js"; +import { type BuildTypedDataParameters } from "../../actions/paymaster/buildTypedData.js"; +import { type ExecuteTransactionParameters } from "../../actions/paymaster/executeTransaction.js"; + +export type UsePaymasterProps = { + network: "mainnet" | "sepolia"; + accountAddress?: ADDRESS; + clientUrl?: string; +}; + + +export type UsePaymasterReturn = { + status: GaslessStatus | null; + compatibility: GaslessCompatibility | null; + rewards: PaymasterReward[]; + loading: boolean; + error: string | null; + refetch: { + fetchPaymasterStatus: () => Promise; + checkAccountCompatibility: () => Promise; + fetchAccountRewards: () => Promise; + getGasTokenPrices: () => Promise; + buildTypedData: (params: BuildTypedDataParameters) => Promise; + executeTransaction: (params: ExecuteTransactionParameters) => Promise; + }; +}; + +export const usePaymaster = ({ network, accountAddress, clientUrl }: UsePaymasterProps): UsePaymasterReturn => { + const [status, setStatus] = useState(null); + const [compatibility, setCompatibility] = useState(null); + const [rewards, setRewards] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const paymasterClient = createPaymasterClient({ + chain: network === "mainnet" ? mainnet : sepolia, + transport: http(clientUrl || `http://localhost:3003/paymaster/${network}`), + }); + + const fetchData = useCallback(async () => { + setLoading(true); + setError(null); + try { + const { status, error: statusError } = await fetchPaymasterStatus(network); + setStatus(status); + setError(statusError); + + + if (accountAddress) { + const { compatibility, error: compatibilityError } = await checkAccountCompatibility(network, accountAddress); + setCompatibility(compatibility); + setError(compatibilityError); + + const { rewards, error: rewardsError } = await fetchAccountRewards(network, accountAddress); + setRewards(rewards); + setError(rewardsError); + + } + } catch (err: any) { + setError(err.message || "An error occurred while fetching data"); + } finally { + setLoading(false); + } + }, [network, accountAddress]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleAsyncRequest = async (asyncFunc: () => Promise, setState: (data: any) => void) => { + setLoading(true); + setError(null); + try { + const response = await asyncFunc(); + setState(response); + } catch (err: any) { + setError(err.message || "An error occurred during request"); + } finally { + setLoading(false); + } + }; + + return { + status, + compatibility, + rewards, + loading, + error, + refetch: { + fetchPaymasterStatus: () => handleAsyncRequest(() => fetchPaymasterStatus(network), setStatus), + checkAccountCompatibility: () => handleAsyncRequest(() => checkAccountCompatibility(network, accountAddress!), setCompatibility), + fetchAccountRewards: () => handleAsyncRequest(() => fetchAccountRewards(network, accountAddress!), setRewards), + getGasTokenPrices: () => handleAsyncRequest(() => getGasTokenPrices(paymasterClient, undefined), () => {}), + buildTypedData: (params) => handleAsyncRequest(() => buildTypedData(paymasterClient, params), () => {}), + executeTransaction: (params) => handleAsyncRequest(() => executeTransaction(paymasterClient, params), () => {}), + }, + }; +}; \ No newline at end of file diff --git a/packages/starkweb/src/types/paymaster.ts b/packages/starkweb/src/types/paymaster.ts new file mode 100644 index 0000000..abc08c3 --- /dev/null +++ b/packages/starkweb/src/types/paymaster.ts @@ -0,0 +1,96 @@ +export interface GaslessStatus { + //The gasless status + status: boolean; + } + + export interface GaslessCompatibility { + //Indicates if the account is compatible with the gasless service + isCompatible: boolean; + //The validation's gas consumed overhead + gasConsumedOverhead: bigint; + //The validation's data gas consumed overhead + dataGasConsumedOverhead: bigint; + } + + export interface GasTokenPrice { + // The gas token's address + tokenAddress: string; + // The price of 1 token in ETH + priceInETH: bigint; + // The price of 1 token in USD + priceInUSD: number; + // The token's number of decimals + decimals: number; + } + + export interface PaymasterReward { + // Reward's creation date + date: Date; + // The user's address + address: string; + // The company that will pay the gas fees + sponsor: string; + // The name of the company's campaign + campaign: string; + // The protocol where the reward can be used + protocol: string | undefined; + // The number of free transaction + freeTx: number; + // The number of remaining transactions + remainingTx: number; + // Reward's expiration date + expirationDate: Date | undefined; + // The list of whitelisted calls + whitelistedCalls: WhitelistedCall[]; + } + + export interface WhitelistedCall { + // The value can be '*' if all contracts are whitelisted or can be the contract address (hex format) + contractAddress: string; + // The value can be '*' if all entrypoint are whitelisted or can be the entrypoint name (string format) + entrypoint: string; + } + + export interface AccountsRewardsOptions { + sponsor?: string; + campaign?: string; + protocol?: string; + } + + export interface DeploymentData { + class_hash: string; + salt: string; + unique: string; + calldata: string[]; + sigdata?: string[]; + } + + export interface ExecuteCallsOptions { + gasTokenAddress?: string; + maxGasTokenAmount?: bigint; + deploymentData?: DeploymentData; + } + + export interface GaslessOptions { + baseUrl?: string; + // The api key allows you to sponsor the gas fees for your users + apiKey?: string; + abortSignal?: AbortSignal; + apiPublicKey?: string; + } + + export interface RequestError { + messages: string[]; + revertError: string | undefined; + } + + export interface InvokeResponse { + transactionHash: string; + } + + export class ContractError { + constructor( + public message: string, + public revertError: string, + ) {} + } \ No newline at end of file diff --git a/packages/starkweb/src/types/snip1193.ts b/packages/starkweb/src/types/snip1193.ts index 69d9419..508012b 100644 --- a/packages/starkweb/src/types/snip1193.ts +++ b/packages/starkweb/src/types/snip1193.ts @@ -1,4 +1,4 @@ -import type { PADDED_FELT, PADDED_TXN_HASH } from '@starknet-io/types-js' +import type { PADDED_FELT, PADDED_TXN_HASH, Signature, TypedData } from '@starknet-io/types-js' import type { AddInvokeTransactionParameters, AddInvokeTransactionResult, @@ -54,11 +54,13 @@ import type { TXN_STATUS, TYPED_DATA, } from './components.js' +import type { DeploymentData, GaslessCompatibility, GasTokenPrice, GaslessStatus, PaymasterReward, InvokeResponse } from './paymaster.js' +import type { Call } from '../strk-types/lib.js' ////////////////////////////////////////////////// // Provider -export type SNIP1474Methods = [...PublicRpcSchema, ...WalletRpcSchema] +export type SNIP1474Methods = [...PublicRpcSchema, ...WalletRpcSchema, ...PaymasterRpcSchema] export type SNIP1193Provider = Prettify< SNIP1193Events & { @@ -645,6 +647,55 @@ export type WalletRpcSchema = [ }, ] +export type PaymasterRpcSchema = [ + { + Method: 'pm_getPaymasterStatus' + Parameters?: undefined + ReturnType: GaslessStatus + }, + { + Method: 'pm_checkAccountCompatibility' + Parameters?: { + accountAddress: ADDRESS + } + ReturnType: GaslessCompatibility + }, + { + Method: 'pm_getGasTokenPrices' + Parameters?: undefined + ReturnType: GasTokenPrice[] + }, + { + Method: 'pm_getAccountRewards' + Parameters?: { + accountAddress: ADDRESS + } + ReturnType: PaymasterReward[] + }, + { + Method: 'pm_buildTypedData' + Parameters?: { + userAddress: string, + calls: Call[], + gasTokenAddress?: string, + maxGasTokenAmount?: string, + accountClassHash?: string + } + ReturnType: TypedData + }, + { + Method: 'pm_executeTransaction' + Parameters?: { + userAddress: string, + typedData: string, + signature: Signature, + deploymentData?: DeploymentData + } + ReturnType: InvokeResponse + }, + +] + // export type TestRpcSchema = [ // ]