This demo showcases a decentralized dice-rolling game that utilizes smart accounts (ERC-7579) and session-based execution. By using session keys, users can roll dice without needing to sign every transaction, enhancing UX while maintaining security.
It also features optional social recovery functionality.
Currently, the demo utilizes Startale's smart contract wallet implementation with Rhinestone's 7579 modules.
- A web3 wallet (e.g., MetaMask) connected to the Soneium Minato test network.
- This is needed to instantiate a smart contract wallet. In a production environment, this step would be done by the service backend, or via social login.
- All operations are funded by a paymaster, so no funds are needed in the wallet
-
Connect Your Wallet
- Open the application and connect your web3 wallet.
-
Instantiate a Smart Account
- An account is automatically instantiated, the address is displayed in the output area
-
(Optional) Add recovery keys
- Input a guardian address, and click "Add guardian".
- On the firs guardian add, the SocialRecovery module is installed first, so two signatures are needed
- You can add and remove more guardian addresses
- A minimum of one address must be present after the module is installed
-
Start the game
- Clicking the "New game" button will install the SmartSession module (only on first use), and create a session.
- Session will be stored in local storage so the game can be played instantly on the next visit
-
Play the Dice Game
- Once a session is active, roll the dice using the UI.
- The result is written on-chain without additional signature prompts.
- Your roll history and score are fetched directly from the smart contract.
This section details the core technologies, smart contracts, and SDKs used in the demo so you can create your own custom interface.
@rhinestone/module-sdk
, for interaction with ERC-7579 modules@startale-scs/aa-sdk
instantiate and manage accountsviem
for SC interaction from TS- and optionally
wagmi
for ReactJs integration
# Standard entrypoint v0.7.0 address
ENTRY_POINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032
# smart contract wallet related contracts
ACCOUNT_RECOVERY_MODULE_ADDRESS=0xA04D053b3C8021e8D5bF641816c42dAA75D8b597
# ERC-7579 compatible modules
ACCOUNT_RECOVERY_MODULE_ADDRESS=0x29c3e3268e36f14A4D1fEe97DD94EF2F60496a2D
SMART_SESSIONS_MODULE_ADDRESS=0x716BC27e1b904331C58891cC3AB13889127189a7
# Demo contract
DICE_ROLL_LEDGER_ADDRESS=0x298D8873bA2B2879580105b992049201B60c1975
MINATO_RPC=https://rpc.minato.soneium.org
BUNDLER_URL=https://soneium-minato.bundler.scs.startale.com?apikey=[API_KEY]
PAYMASTER_SERVICE_URL=https://paymaster.scs.startale.com/v1?apikey=[API_KEY]
-
Initialize clients
- Use
viem
to connect to the target chain.
import {createBundlerClient, createPaymasterClient } from "viem/account-abstraction"; import { soneiumMinato } from "viem/chains"; import { createPublicClient encodePacked, encodeAbiParameters, encodeFunctionData, getAccountNonce, } from "viem"; const chain = soneiumMinato; const publicClient = createPublicClient({ transport: http(MINATO_RPC), chain, }); const scsPaymasterClient = createSCSPaymasterClient({ transport: http(PAYMASTER_SERVICE_URL) }); const bundlerClient = createBundlerClient({ client: publicClient, transport: http(BUNDLER_URL), });
- Use
-
Create a Smart Account and a client
- Utilize
@startale-scs/aa-sdk
to instantiate a smart account. - Use
window.ethereum
provider as a signer - for backend use a different
signer
instance (f.ex.viem
's local wallet) - use
createSmartAccountClient
for further interaction with the account
import { type StartaleSmartAccount, type StartaleAccountClient, createSmartAccountClient, toStartaleSmartAccount } from "@startale-scs/aa-sdk"; // Create an account const startaleAccountInstance = await toStartaleSmartAccount({ signer: window.ethereum, chain, transport: http(), index: BigInt(0), //Nonce for account instance }); const scsContext = { calculateGasLimits: false, paymasterId: <YOUR_PAYMASTER_ID_FROM_PORTAL> }; const accountClientInstance = createSmartAccountClient({ account: startaleAccountInstance, transport: http(BUNDLER_URL), client: publicClient, paymaster: scsPaymasterClient, paymasterContext: scsContext });
- Utilize
Note: Paymaster actions and userOperation gas estimation are overridden for compatibility with the current version of SCS paymaster.
-
Install Social Recovery Module (Optional)
- Set up recovery guardians using
getSocialRecoveryValidator
from@rhinestone/module-sdk
. - Install it via the
accountClientInstance.installModule()
function.
const socialRecovery = getSocialRecoveryValidator({ // SET INITIAL CONFIG threshold: 1, guardians: [guardianAddress], }); const installModuleUserOpHash = await accountClientInstance.installModule({ module: socialRecoveryModule, }); //Add a new guardian const calls = [ { to: ACCOUNT_RECOVERY_MODULE_ADDRESS, value: BigInt(0), data: encodeFunctionData({ abi: SocialRecoveryAbi, functionName: "addGuardian", args: [guardian], }), }, ]; const addGuardianUserOpHash = await accountClientInstance.sendUserOperation({ callData: await accountClientInstance.account.encodeCalls(calls), }); // Remove guardian const SENTINEL_ADDRESS = "0x0000000000000000000000000000000000000001"; const index = guardians.indexOf(guardian); if (index < 0) { console.error("Guardian not found in list"); return; } const prevGuardian = index === 0 ? SENTINEL_ADDRESS : guardians[index - 1]; await displayGasOutput(); const calls = [ { to: ACCOUNT_RECOVERY_MODULE_ADDRESS, value: BigInt(0), data: encodeFunctionData({ abi: SocialRecoveryAbi, functionName: "removeGuardian", args: [prevGuardian, guardian], }), }, ]; const removeGuardianUserOpHash = await accountClientInstance.sendUserOperation({ callData: await accountClientInstance.account.encodeCalls(calls), }); // Get guardians list const accountGuardians = await publicClient.readContract({ address: ACCOUNT_RECOVERY_MODULE_ADDRESS, abi: SocialRecoveryAbi, functionName: "getGuardians", args: [accountClientInstance.account.address], });
- Set up recovery guardians using
-
Enable Smart Session Module
- Instantiate the session module with
getSmartSessionsValidator
. - Install the module and configure it for executing transactions without signing.
const sessionsModule = getSmartSessionsValidator({}); const opHash = await accountClientInstance.installModule({ module: sessionsModule, }); // Check const isSmartSessionsModuleInstalled = await accountClientInstance.isModuleInstalled({ module: sessionsModule, });
- Instantiate the session module with
-
Create a Session for Transaction Execution
- Define permissions for allowed contract calls (e.g., the dice roll function).
- Enable the session by calling the
enableSessions
function on the Smart Session contract.
const sessionOwner = privateKeyToAccount(ownerKey as `0x${string}`); const sessionsModule = toSmartSessionsValidator({ account: accountClientInstance.account, signer: sessionOwner, }); const startaleSessionClient = accountClientInstance.extend(smartSessionCreateActions(sessionsModule)); const selector = toFunctionSelector("writeDiceRoll(uint256)"); const sessionRequestedInfo: CreateSessionDataParams[] = [ { sessionPublicKey: sessionOwner.address, // session key signer actionPoliciesInfo: [ { contractAddress: DICE_ROLL_LEDGER_ADDRESS, functionSelector: selector, sudo: true, }, ], }, ]; const createSessionsResponse = await startaleSessionClient.grantPermission({ sessionRequestedInfo, }); const sessionData: SessionData = { granter: accountClientInstance.account.address, description: `Session to increment a counter for ${DICE_ROLL_LEDGER_ADDRESS}`, sessionPublicKey: sessionOwner.address, moduleData: { permissionIds: createSessionsResponse.permissionIds, action: createSessionsResponse.action, mode: SmartSessionMode.USE, sessions: createSessionsResponse.sessions, }, };
-
Send Transactions Using Session Keys
- Sign transactions using a generated session key.
- The app automatically prepares and sends user operations via the smart account client.
const isEnabled = await isSessionEnabled({ client: accountClientInstance.account.client as PublicClient, account: { type: "erc7579-implementation", address: accountClientInstance.account.address, deployedOnChains: [chain.id], }, permissionId: activeSession.moduleData.permissionIds[0], }); const sessionOwner = privateKeyToAccount(ownerKey); const smartSessionClient = createSmartAccountClient({ account: await toStartaleSmartAccount({ signer: sessionOwner, accountAddress: activeSession.granter, chain: chain, transport: http(), }), transport: http(BUNDLER_URL), client: publicClient, paymaster: { async getPaymasterData(pmDataParams: GetPaymasterDataParameters) { pmDataParams.paymasterPostOpGasLimit = BigInt(100000); pmDataParams.paymasterVerificationGasLimit = BigInt(200000); pmDataParams.verificationGasLimit = BigInt(500000); const paymasterResponse = await paymasterClient.getPaymasterData(pmDataParams); return paymasterResponse; }, async getPaymasterStubData(pmStubDataParams: GetPaymasterDataParameters) { const paymasterStubResponse = await paymasterClient.getPaymasterStubData(pmStubDataParams); return paymasterStubResponse; }, }, paymasterContext: scsContext, userOperation: { estimateFeesPerGas: async () => { return { maxFeePerGas: BigInt(10000000), maxPriorityFeePerGas: BigInt(10000000), }; }, }, mock: true, }); const usePermissionsModule = toSmartSessionsValidator({ account: smartSessionClient.account, signer: sessionOwner, moduleData: activeSession.moduleData, }); const useSmartSessionClient = smartSessionClient.extend( smartSessionUseActions(usePermissionsModule), ); const callData = encodeFunctionData({ abi: DiceRollLedgerAbi, functionName: "writeDiceRoll", args: [BigInt(value)], }); const userOpHash = await useSmartSessionClient.usePermission({ calls: [ { to: DICE_ROLL_LEDGER_ADDRESS, data: callData, }, ], });