From 3eed683a9be294c9fd5b9553075dfd76e912d1ab Mon Sep 17 00:00:00 2001 From: Himanshu garg Date: Thu, 30 Nov 2023 00:54:00 +0530 Subject: [PATCH 1/4] Add custom session storge tutorial --- docs/Account/methods/userOpMethods.md | 12 +- .../customSessionKeys/_category_.json | 8 + .../customSessionKeys/createSession.md | 257 ++++++++++++++++ .../customSessionKeys/customSessionStorage.md | 267 +++++++++++++++++ .../customSessionKeys/environmentsetup.md | 63 ++++ .../customSessionKeys/erc20Transfer.md | 277 ++++++++++++++++++ .../customSessionKeys/initializeaccount.md | 115 ++++++++ 7 files changed, 998 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/customSessionKeys/_category_.json create mode 100644 docs/tutorials/customSessionKeys/createSession.md create mode 100644 docs/tutorials/customSessionKeys/customSessionStorage.md create mode 100644 docs/tutorials/customSessionKeys/environmentsetup.md create mode 100644 docs/tutorials/customSessionKeys/erc20Transfer.md create mode 100644 docs/tutorials/customSessionKeys/initializeaccount.md diff --git a/docs/Account/methods/userOpMethods.md b/docs/Account/methods/userOpMethods.md index 8a46f989..b76ca0ff 100644 --- a/docs/Account/methods/userOpMethods.md +++ b/docs/Account/methods/userOpMethods.md @@ -171,13 +171,14 @@ const userOpResponse = await smartAccount.sendUserOp(userOp); ``` -The userOpresPonce object looks like this: +The userOpResponse object looks like this: ```ts type UserOpResponse = { userOpHash: string; wait(_confirmations?: number): Promise; + waitForTxHash(): Promise; } ``` @@ -208,8 +209,17 @@ type UserOpReceipt = { } ``` +You can use `waitForTxHash` to get the userOpHash or transactionHash, without waiting for the transaction to be mined. +```ts + +const transactionDetails: UserOpStatus = await userOpResponse.waitForTxHash(); +console.log('transaction hash', transactionDetails.transactionHash) + + +``` + The userOpResponse has one method that you will use ### Using modules with sendUserOp() diff --git a/docs/tutorials/customSessionKeys/_category_.json b/docs/tutorials/customSessionKeys/_category_.json new file mode 100644 index 00000000..9d405451 --- /dev/null +++ b/docs/tutorials/customSessionKeys/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Custom Session Keys Tutorial", + "position": 5, + "link": { + "type": "generated-index", + "description": "Use Custom storage for session keys module with Node JS" + } +} diff --git a/docs/tutorials/customSessionKeys/createSession.md b/docs/tutorials/customSessionKeys/createSession.md new file mode 100644 index 00000000..a316d00e --- /dev/null +++ b/docs/tutorials/customSessionKeys/createSession.md @@ -0,0 +1,257 @@ +--- +sidebar_label: 'Session Module' +sidebar_position: 4 +--- +# Session Module + +In this guide, we will walk through creating a basic Node.js script using +TypeScript that allows user to create a session. + +:::info This tutorial has a previous other steps in the previous sections: +[Environment Setup](environmentsetup) and +[Initializing Account](initializeaccount) + ::: + +This tutorial will be done on the Polygon Mumbai Network. We will be using session Module for this. + +First we will import sessionKeyManagerModule and DEFAULT_SESSION_KEY_MANAGER_MODULE from Biconomy Modules package. + +First of all we will initialise the sessionFileStorage using our custom File storage + +```typescript + const sessionFileStorage: SessionFileStorage = new SessionFileStorage(address) +``` + + +We will now use the Session Key Manager Module to create a session module using the module address and smart account address and the custom session storage. This is an important relationship to establish - the module provided by the SDK gives you an easy way to interact with modules you write on a smart contract with whatever arbitrary validation logic you need. + + +```typescript + const sessionModule = await SessionKeyManagerModule.create({ + moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, + smartAccountAddress: address, + sessionStorageClient: sessionFileStorage + }); + +``` + +In this section we will cover a deployed contract that validates specific permissions to execute ERC20 token transfers. Using this ERC20 Validation module you will be able to create a dApp that allows user to send a limited amount of funds to a specific address without needing to sign a transaction every single time. In the following example user can only transfer 50 tokens at once. + +```typescript + const sessionKeyData = defaultAbiCoder.encode( + ["address", "address", "address", "uint256"], + [ + sessionKeyEOA, + "0xdA5289fCAAF71d52a80A254da614a192b693e977", // erc20 token address + "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address + ethers.utils.parseUnits("50".toString(), 6).toHexString(), // 50 usdc amount + ] + ); + +``` + +Now we create the session data itself. We specify how long this should be valid until or how long it is valid after. This should be a unix timestamp to represent the time. Passing 0 on both makes this session never expire, do not do this in production. Next we pass the module address, session key address, and pass the session key data we just created. + + +```typescript + + const sessionTxData = await sessionModule.createSessionData([ + { + validUntil: 0, + validAfter: 0, + sessionValidationModule: erc20ModuleAddr, + sessionPublicKey: sessionKeyEOA, + sessionKeyData: sessionKeyData, + }, + ]); +``` +We're going to be tracking if the session key module is already enabled. +This will check if a session module is enabled - it will return if we do not have an address, smart Account, or provider and will then enable the module for our smartAccount if needed. + +```typescript + const isEnabled = await smartAccount.isModuleEnabled(DEFAULT_SESSION_KEY_MANAGER_MODULE) + if (!isEnabled) + { + const enableModuleTrx = await smartAccount.getEnableModuleData( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ); + transactionArray.push( enableModuleTrx ); + } + +``` + +Finally if we need to enable session key module we create a transactione here as well using the getEnableModuleData and pass the session key manager module address and push this to the array. Additionally we push the session transaction to the array as well, we will be batching these transactions together. + +Next we will build a userOp and use the smart account to send it to Bundler. + +```typescript + + let partialUserOp = await smartAccount.buildUserOp( transactionArray, { + paymasterServiceData: { + mode: PaymasterMode.SPONSORED, + } + } ); + console.log( partialUserOp ) + const userOpResponse = await smartAccount.sendUserOp( + partialUserOp + ); + console.log( `userOp Hash: ${ userOpResponse.userOpHash }` ); + const transactionDetails = await userOpResponse.wait(); + console.log( "txHash", transactionDetails.receipt.transactionHash ); +``` +Now with this implemented let's take a look at executing the ERC20 token transfer with a session in the next section. + + +Checkout below for entire code snippet +
+ Expand for Code + +```typescript + +import { defaultAbiCoder } from "ethers/lib/utils"; +import { ECDSAOwnershipValidationModule, DEFAULT_ECDSA_OWNERSHIP_MODULE, SessionKeyManagerModule, DEFAULT_SESSION_KEY_MANAGER_MODULE } from "@biconomy/modules"; +import { config } from "dotenv" +import { IBundler, Bundler } from '@biconomy/bundler' +import { BiconomySmartAccountV2, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account" +import { Wallet, providers, ethers } from 'ethers' +import { ChainId } from "@biconomy/core-types" +import + { + IPaymaster, + BiconomyPaymaster, + PaymasterMode, + } from '@biconomy/paymaster' +import { SessionFileStorage } from "./customSession"; + +let smartAccount: BiconomySmartAccountV2 +let address: string + +config(); + +const bundler: IBundler = new Bundler( { + bundlerUrl: + "https://bundler.biconomy.io/api/v2/80001/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44", + chainId: ChainId.POLYGON_MUMBAI, + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, +} ); + +console.log( { ep: DEFAULT_ENTRYPOINT_ADDRESS } ); + +const paymaster: IPaymaster = new BiconomyPaymaster( { + paymasterUrl: + "https://paymaster.biconomy.io/api/v1/80001/HvwSf9p7Q.a898f606-37ed-48d7-b79a-cbe9b228ce43", +} ); + +const provider = new providers.JsonRpcProvider( + "https://rpc.ankr.com/polygon_mumbai" +); +const wallet = new Wallet( process.env.PRIVATE_KEY || "", provider ); + +async function createAccount () +{ + const module = await ECDSAOwnershipValidationModule.create( { + signer: wallet, + moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE + } ) + let biconomySmartAccount = await BiconomySmartAccountV2.create( { + chainId: ChainId.POLYGON_MUMBAI, + bundler: bundler, + paymaster: paymaster, + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, + defaultValidationModule: module, + activeValidationModule: module + } ) + address = await biconomySmartAccount.getAccountAddress() + console.log( address ) + smartAccount = biconomySmartAccount; + + return biconomySmartAccount; +} + + +const createSession = async () => +{ + await createAccount(); + try + { + const erc20ModuleAddr = "0x000000D50C68705bd6897B2d17c7de32FB519fDA" + // -----> setMerkle tree tx flow + // create dapp side session key + const sessionSigner = ethers.Wallet.createRandom(); + const sessionKeyEOA = await sessionSigner.getAddress(); + console.log( "sessionKeyEOA", sessionKeyEOA ); + const sessionFileStorage: SessionFileStorage = new SessionFileStorage( address ) + + // generate sessionModule + console.log( "Adding session signer", sessionSigner.publicKey, sessionSigner ); + + await sessionFileStorage.addSigner( sessionSigner ) + const sessionModule = await SessionKeyManagerModule.create( { + moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, + smartAccountAddress: address, + sessionStorageClient: sessionFileStorage + } ); + + // cretae session key data + const sessionKeyData = defaultAbiCoder.encode( + [ "address", "address", "address", "uint256" ], + [ + sessionKeyEOA, + "0xdA5289fCAAF71d52a80A254da614a192b693e977", // erc20 token address + "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address + ethers.utils.parseUnits( "50".toString(), 6 ).toHexString(), // 50 usdc amount + ] + ); + const sessionTxData = await sessionModule.createSessionData( [ + { + validUntil: 0, + validAfter: 0, + sessionValidationModule: erc20ModuleAddr, + sessionPublicKey: sessionKeyEOA, + sessionKeyData: sessionKeyData, + }, + ] ); + + // tx to set session key + const setSessiontrx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, // session manager module address + data: sessionTxData.data, + }; + + const transactionArray = []; + + + const isEnabled = await smartAccount.isModuleEnabled( DEFAULT_SESSION_KEY_MANAGER_MODULE ) + if ( !isEnabled ) + { + const enableModuleTrx = await smartAccount.getEnableModuleData( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ); + transactionArray.push( enableModuleTrx ); + } + + transactionArray.push( setSessiontrx ) + let partialUserOp = await smartAccount.buildUserOp( transactionArray, { + paymasterServiceData: { + mode: PaymasterMode.SPONSORED, + } + } ); + console.log( partialUserOp ) + const userOpResponse = await smartAccount.sendUserOp( + partialUserOp + ); + console.log( `userOp Hash: ${ userOpResponse.userOpHash }` ); + const transactionDetails = await userOpResponse.wait(); + console.log( "txHash", transactionDetails.receipt.transactionHash ); + + } catch ( err: any ) { + console.error( err ) + } + +} + +createSession(); + + +``` +
\ No newline at end of file diff --git a/docs/tutorials/customSessionKeys/customSessionStorage.md b/docs/tutorials/customSessionKeys/customSessionStorage.md new file mode 100644 index 00000000..88145e0c --- /dev/null +++ b/docs/tutorials/customSessionKeys/customSessionStorage.md @@ -0,0 +1,267 @@ +--- +sidebar_label: 'Create Custom session' +sidebar_position: 3 +--- + +# Create custom session + +Now users can make their own implementation of Session Storage by implementing ISessionStorage interface and pass it to the SessionKeyManager module instance. Let's create a new file called customSession and start writing our own session stoage. + +we will import following + +```typescript +import { ISessionStorage, SessionLeafNode, SessionSearchParam, SessionStatus } from "@biconomy/modules/dist/src/interfaces/ISessionStorage"; +``` + +We will need to implement all the interface methods. + +Here is an example of File storage implementation. It saves the session keys and signers in files. Users will need to create two file in the root folder with $smartAccountAddress_sessions.json and $smartAccountAddress_signers.json names. For instance, if the account address is 0x123 then create 0x123_sessions.json and 0x123_signers.json. + +```typescript +import * as fs from "fs"; +import { Wallet, Signer } from "ethers"; +import { ISessionStorage, SessionLeafNode, SessionSearchParam, SessionStatus } from "@biconomy/modules/dist/src/interfaces/ISessionStorage"; + +export class SessionFileStorage implements ISessionStorage +{ + private smartAccountAddress: string; + + constructor ( smartAccountAddress: string ) + { + this.smartAccountAddress = smartAccountAddress.toLowerCase(); + } + + private async readDataFromFile ( type: "sessions" | "signers" ): Promise + { + return new Promise( ( resolve ) => + { + fs.readFile( this.getStorageFilePath( type ), "utf8", ( err, data ) => + { + if ( err ) + { + // Handle errors appropriately + resolve( undefined ); + } else + { + if ( !data ) + { + resolve( null ) + } else + { + resolve( JSON.parse( data ) ); + + } + // resolve(JSON.parse(data)); + } + } ); + } ); + } + + private getStorageFilePath ( type: "sessions" | "signers" ): string + { + return `./${ this.smartAccountAddress }_${ type }.json`; + } + + private async writeDataToFile ( data: any, type: "sessions" | "signers" ): Promise + { + return new Promise( ( resolve, reject ) => + { + const filePath = this.getStorageFilePath( type ); + fs.writeFile( filePath, JSON.stringify( data ), "utf8", ( err ) => + { + if ( err ) + { + // Handle errors appropriately + reject( err ); + } else + { + resolve(); + } + } ); + } ); + } + + private validateSearchParam ( param: SessionSearchParam ): void + { + if ( param.sessionID ) + { + return; + } else if ( !param.sessionID && param.sessionPublicKey && param.sessionValidationModule ) + { + return; + } else + { + throw new Error( "Either pass sessionId or a combination of sessionPublicKey and sessionValidationModule address." ); + } + } + + private async getSessionStore (): Promise + { + try + { + const data = await this.readDataFromFile( "sessions" ); + return data || { merkleRoot: "", leafNodes: [] }; + } catch ( error ) + { + // Handle errors appropriately + throw error; + } + } + + private async getSignerStore (): Promise + { + try + { + const data = await this.readDataFromFile( "signers" ); + return data || {}; + } catch ( error ) + { + // Handle errors appropriately + throw error; + } + } + + private getStorageKey ( type: "sessions" | "signers" ): string + { + return `${ this.smartAccountAddress }_${ type }`; + } + + private toLowercaseAddress ( address: string ): string + { + return address.toLowerCase(); + } + + async getSessionData ( param: SessionSearchParam ): Promise + { + // this.validateSearchParam(param); + + const sessions = ( await this.getSessionStore() ).leafNodes; + console.log( "Got sessions", sessions ) + const session = sessions[ 0 ]; + // const session = sessions.find((s: SessionLeafNode) => { + // if (param.sessionID) { + // return s.sessionID === param.sessionID && (!param.status || s.status === param.status); + // } else if (param.sessionPublicKey && param.sessionValidationModule) { + // return ( + // s.sessionPublicKey === this.toLowercaseAddress(param.sessionPublicKey) && + // s.sessionValidationModule === this.toLowercaseAddress(param.sessionValidationModule) && + // (!param.status || s.status === param.status) + // ); + // } else { + // return undefined; + // } + // }); + + if ( !session ) + { + throw new Error( "Session not found." ); + } + return session; + } + async addSessionData ( leaf: SessionLeafNode ): Promise + { + console.log( "Add session Data", leaf ) + const data = await this.getSessionStore(); + leaf.sessionValidationModule = this.toLowercaseAddress( leaf.sessionValidationModule ); + leaf.sessionPublicKey = this.toLowercaseAddress( leaf.sessionPublicKey ); + data.leafNodes.push( leaf ); + await this.writeDataToFile( data, "sessions" ); // Use 'sessions' as the type + } + + async updateSessionStatus ( param: SessionSearchParam, status: SessionStatus ): Promise + { + this.validateSearchParam( param ); + + const data = await this.getSessionStore(); + const session = data.leafNodes.find( ( s: SessionLeafNode ) => + { + if ( param.sessionID ) + { + return s.sessionID === param.sessionID; + } else if ( param.sessionPublicKey && param.sessionValidationModule ) + { + return ( + s.sessionPublicKey === this.toLowercaseAddress( param.sessionPublicKey ) && + s.sessionValidationModule === this.toLowercaseAddress( param.sessionValidationModule ) + ); + } else + { + return undefined; + } + } ); + + if ( !session ) + { + throw new Error( "Session not found." ); + } + + session.status = status; + await this.writeDataToFile( data, "sessions" ); // Use 'sessions' as the type + } + + async clearPendingSessions (): Promise + { + const data = await this.getSessionStore(); + data.leafNodes = data.leafNodes.filter( ( s: SessionLeafNode ) => s.status !== "PENDING" ); + await this.writeDataToFile( data, "sessions" ); // Use 'sessions' as the type + } + + async addSigner ( signer?: Wallet ): Promise + { + const signers = await this.getSignerStore(); + if ( !signer ) + { + signer = Wallet.createRandom(); + } + signers[ this.toLowercaseAddress( signer.address ) ] = { + privateKey: signer.privateKey, + publicKey: signer.publicKey, + }; + await this.writeDataToFile( signers, "signers" ); // Use 'signers' as the type + return signer; + } + + async getSignerByKey ( sessionPublicKey: string ): Promise + { + const signers = await this.getSignerStore(); + console.log( "Got signers", signers ) + const signerData = signers[ this.toLowercaseAddress( sessionPublicKey ) ]; + if ( !signerData ) + { + throw new Error( "Signer not found." ); + } + const signer = new Wallet( signerData.privateKey ); + return signer; + } + + async getSignerBySession ( param: SessionSearchParam ): Promise + { + const session = await this.getSessionData( param ); + console.log( "got session", session ) + return this.getSignerByKey( session.sessionPublicKey ); + } + + async getAllSessionData ( param?: SessionSearchParam ): Promise + { + const sessions = ( await this.getSessionStore() ).leafNodes; + if ( !param || !param.status ) + { + return sessions; + } + return sessions.filter( ( s: SessionLeafNode ) => s.status === param.status ); + } + + async getMerkleRoot (): Promise + { + return ( await this.getSessionStore() ).merkleRoot; + } + + async setMerkleRoot ( merkleRoot: string ): Promise + { + const data = await this.getSessionStore(); + data.merkleRoot = merkleRoot; + await this.writeDataToFile( data, "sessions" ); // Use 'sessions' as the type + } +} + +``` \ No newline at end of file diff --git a/docs/tutorials/customSessionKeys/environmentsetup.md b/docs/tutorials/customSessionKeys/environmentsetup.md new file mode 100644 index 00000000..da7838a8 --- /dev/null +++ b/docs/tutorials/customSessionKeys/environmentsetup.md @@ -0,0 +1,63 @@ +--- +sidebar_label: 'Environment Set up' +sidebar_position: 1 +--- +# Environment set up + +:::info +If you already went through the quick start guide this section follows the same initial steps. You can skip this and go straight to the next section if you want to implement into your existing quick start codebase. +::: + +We will clone a preconfigured Node.js project with TypeScript support to get started. Follow these steps to clone the repository to your local machine using your preferred command line interface: + +1. Open your command line interface, Terminal, Command Prompt, or PowerShell. +2. Navigate to the desired directory where you would like to clone the repository. +3. Execute the following command to clone the repository from the provided [GitHub link](https://github.com/bcnmy/quickstart) + +```bash +git clone git@github.com:bcnmy/quickstart.git +``` + +Note that this is the ssh example, use http or GithubCli options if you prefer. + +```bash +git clone https://github.com/bcnmy/quickstart.git +``` + +Once you have the repository on your local machine - start by installing all dependencies using your preferred package manager. In this tutorial we will use yarn. + +```bash +yarn install +yarn dev +``` +After running these two commands you should see the printed statement “Hello World!” in your terminal. Any changes made to the `index.ts` file in the src directory should now automatically run in your terminal upon save. + +All packages you need for this guide are all configured and installed for you, check out the `package.json` file if you want to explore the dependencies. + +
+ Click to learn more about the packages + +- The account package will help you with creating smart contract accounts and an interface with them to create transactions. +- The bundler package helps you with interacting with our bundler or alternatively another bundler of your choice. +- The core types package will give us Enums for the proper ChainId we may want to use +- The paymaster package works similarly to the bundler package in that you can use our paymaster or any other one of your choice. +- The core types package will give us Enums for the proper ChainId we may want to use. +- The common package is needed by our accounts package as another dependency. +- Finally the ethers package at version 5.7.2 will help us with giving our accounts an owner which will be our own EOA. + +
+ +Let’s first set up a .env file in the root of our project, this will need a Private Key of any Externally Owned Account (EOA) you would like to serve as the owner of the smart account we create. This is a private key you can get from wallets like MetaMask, TrustWallet, Coinbase Wallet etc. All of these wallets will have tutorials on how to export the Private key. + +```bash +PRIVATE_KEY = "enter some private key" +``` + +Let’s give our script the ability to access this environment variable. Delete the console log inside of `src/index.ts` and replace it with the code below. All of our work for the remainder of the tutorial will be in this file. + +```typescript +import { config } from "dotenv" + +config() +``` +Now our code is configured to access the environment variable as needed. Let's continue in the next section with setting up our smart account \ No newline at end of file diff --git a/docs/tutorials/customSessionKeys/erc20Transfer.md b/docs/tutorials/customSessionKeys/erc20Transfer.md new file mode 100644 index 00000000..41ddab33 --- /dev/null +++ b/docs/tutorials/customSessionKeys/erc20Transfer.md @@ -0,0 +1,277 @@ +--- +sidebar_label: 'ERC20 transfer' +sidebar_position: 5 +--- + +# ERC20 transfer + +We will need the bundler however the paymaster is optional in this case. If you want to sponsor transactions later or allow users to pay for gas in erc20 tokens make sure to set this up but for the purposes of this tutorial we will not be sponsoring any transactions. The paymaster URL here will only work for tutorial contracts. + +First we will get the smartAccount. This is similar to the initialize account. +We specify the erc20 module address and get the private key we stored in our storage and create a new session signer from it. + + +```typescript + const erc20ModuleAddr = "0x000000D50C68705bd6897B2d17c7de32FB519fDA"; + const sessionKeyPrivKey = sessionFileStorage.getSignerBySession({}); + if (!sessionKeyPrivKey) { + console.log("Session key not found please create session"); + return; + } + const sessionSigner = new ethers.Wallet(sessionKeyPrivKey); + console.log("sessionSigner", sessionSigner); +``` + +Now we'll generate a session module using the Session Key Manager Module and then set the active validation module to be the session module. This updates the original configureation on the smart account. + + +```typescript +// generate sessionModule + const sessionModule = await SessionKeyManagerModule.create({ + moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, + smartAccountAddress: address, + }); + + // set active module to sessionModule + smartAccount = smartAccount.setActiveValidationModule(sessionModule); +``` + +We now create an instance of the contract. Note that USDC does not have 18 decimals so we update the decimals based on the USDC contract. +```typescript + + const tokenContract = new ethers.Contract( + // polygon mumbai usdc address + "0xdA5289fCAAF71d52a80A254da614a192b693e977", + usdcAbi, + provider + ); + let decimals = 18; + + try { + decimals = await tokenContract.decimals(); + } catch (error) { + throw new Error("invalid token address supplied"); + } + +``` + + +Now we will get raw transaction data for a transfer of 1 usdc to the receiver address we specified. Using any other reciever other than the one registered on the session key will result in an error. We are sending 1 USDC in this case but can send up to 50 with this transaction as that is the maximum amount that we specified. + +```typescript + const { data } = await tokenContract.populateTransaction.transfer( + "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address + ethers.utils.parseUnits("1".toString(), decimals) + ); + + const tx1 = { + to: "0xdA5289fCAAF71d52a80A254da614a192b693e977", //erc20 token address + data: data, + value: "0", + }; + + +``` + +Now we build the user op and send it for execution. Note the additional arguments you can add in the `buildUserOp` method such as overrides if needed, ability to skip bundler gas estimations, and most importantly params object that will contain information about the session signer and session validation module. + +```typescript + let userOp = await smartAccount.buildUserOp([tx1], { + overrides: { + // signature: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000456b395c4e107e0302553b90d1ef4a32e9000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000db3d753a1da5a6074a9f74f39a0a779d3300000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000bfe121a6dcf92c49f6c2ebd4f306ba0ba0ab6f1c000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e97700000000000000000000000042138576848e839827585a3539305774d36b96020000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041feefc797ef9e9d8a6a41266a85ddf5f85c8f2a3d2654b10b415d348b150dabe82d34002240162ed7f6b7ffbc40162b10e62c3e35175975e43659654697caebfe1c00000000000000000000000000000000000000000000000000000000000000" + // callGasLimit: 2000000, // only if undeployed account + // verificationGasLimit: 700000 + }, + skipBundlerGasEstimation: false, + params: { + sessionSigner: sessionSigner, + sessionValidationModule: erc20ModuleAddr, + }, + }); + + // send user op + const userOpResponse = await smartAccount.sendUserOp(userOp, { + sessionSigner: sessionSigner, + sessionValidationModule: erc20ModuleAddr, + }); + +``` +Finaly to give the user a succesful feedback we provide them with a link to the transaction once it has been executed. + +Running this code should now allow you to sign in using your EOA, create a session, and then send USDC without the need to sign any further transactions! + +Checkout below for entire code snippet +
+ Expand for Code + +```typescript +import usdcAbi from "./usdcabi.json" +import { ECDSAOwnershipValidationModule, DEFAULT_ECDSA_OWNERSHIP_MODULE, SessionKeyManagerModule, DEFAULT_SESSION_KEY_MANAGER_MODULE } from "@biconomy/modules"; +import { config } from "dotenv" +import { IBundler, Bundler } from '@biconomy/bundler' +import { BiconomySmartAccountV2, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account" +import { Wallet, providers, ethers } from 'ethers' +import { ChainId } from "@biconomy/core-types" +import +{ + IPaymaster, + BiconomyPaymaster, +} from '@biconomy/paymaster' + +import { SessionFileStorage } from "./customSession"; +config() + +let smartAccount: BiconomySmartAccountV2 +let address: string +const bundler: IBundler = new Bundler( { + bundlerUrl: + "https://bundler.biconomy.io/api/v2/80001/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44", + chainId: ChainId.POLYGON_MUMBAI, + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, +} ); + +console.log( { ep: DEFAULT_ENTRYPOINT_ADDRESS } ); + +const paymaster: IPaymaster = new BiconomyPaymaster( { + paymasterUrl: + "https://paymaster.biconomy.io/api/v1/80001/HvwSf9p7Q.a898f606-37ed-48d7-b79a-cbe9b228ce43", +} ); + +const provider = new providers.JsonRpcProvider( + "https://rpc.ankr.com/polygon_mumbai" +); +const wallet = new Wallet( process.env.PRIVATE_KEY || "", provider ); + + +async function createAccount () +{ + const module = await ECDSAOwnershipValidationModule.create( { + signer: wallet, + moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE + } ) + let biconomySmartAccount = await BiconomySmartAccountV2.create( { + chainId: ChainId.POLYGON_MUMBAI, + bundler: bundler, + paymaster: paymaster, + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, + defaultValidationModule: module, + activeValidationModule: module + } ) + address = await biconomySmartAccount.getAccountAddress() + console.log( address ) + smartAccount = biconomySmartAccount; + + + return biconomySmartAccount; +} + +const erc20Transfer = async ( sessionFileStorage: SessionFileStorage, amount: string ) => +{ + + if ( !address || !smartAccount ) + { + console.log( "Please connect wallet first" ); + return; + } + try + { + + const erc20ModuleAddr = "0x000000D50C68705bd6897B2d17c7de32FB519fDA"; + // get session key from file storage + const sessionKeyPrivKey = await sessionFileStorage.getSignerBySession( {} ); + console.log( "sessionKeyPrivKey", sessionKeyPrivKey ); + if ( !sessionKeyPrivKey ) + { + console.log( "Session key not found please create session" ); + return; + } + //@ts-ignore + const sessionSigner = new ethers.Wallet( sessionKeyPrivKey ); + console.log( "sessionSigner", sessionSigner ); + console.log( "created -1" ) + // generate sessionModule + const sessionModule = await SessionKeyManagerModule.create( { + moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, + smartAccountAddress: address, + sessionStorageClient: sessionFileStorage + } ); + console.log( "created 0" ) + // set active module to sessionModule + smartAccount = smartAccount.setActiveValidationModule( sessionModule ); + + const tokenContract = new ethers.Contract( + // polygon mumbai usdc address + "0xdA5289fCAAF71d52a80A254da614a192b693e977", + usdcAbi, + provider + ); + let decimals = 18; + + try + { + decimals = await tokenContract.decimals(); + } catch ( error ) + { + throw new Error( "invalid token address supplied" ); + } + console.log( "created 1 decimals", decimals ) + const { data } = await tokenContract.populateTransaction.transfer( + "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address + ethers.utils.parseUnits( amount, decimals ) + ); + + console.log( "created 2" ) + // generate tx data to erc20 transfer + const tx1 = { + to: "0xdA5289fCAAF71d52a80A254da614a192b693e977", //erc20 token address + data: data, + value: "0", + }; + + // build user op + // const ifModuleEnabled = await smartAccount.isModuleEnabled("") + let userOp = await smartAccount.buildUserOp( [ tx1 ], { + overrides: { + // signature: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000456b395c4e107e0302553b90d1ef4a32e9000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000db3d753a1da5a6074a9f74f39a0a779d3300000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000bfe121a6dcf92c49f6c2ebd4f306ba0ba0ab6f1c000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e97700000000000000000000000042138576848e839827585a3539305774d36b96020000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041feefc797ef9e9d8a6a41266a85ddf5f85c8f2a3d2654b10b415d348b150dabe82d34002240162ed7f6b7ffbc40162b10e62c3e35175975e43659654697caebfe1c00000000000000000000000000000000000000000000000000000000000000" + // callGasLimit: 2000000, // only if undeployed account + // verificationGasLimit: 700000 + }, + skipBundlerGasEstimation: false, + params: { + sessionSigner: sessionSigner, + sessionValidationModule: erc20ModuleAddr, + }, + } ); + console.log( "created 3" ) + // send user op + const userOpResponse = await smartAccount.sendUserOp( userOp, { + sessionSigner: sessionSigner, + sessionValidationModule: erc20ModuleAddr, + } ); + console.log( "created 4" ) + console.log( "userOpHash", userOpResponse ); + const { receipt } = await userOpResponse.wait( 1 ); + console.log( "txHash", receipt.transactionHash ); + const polygonScanlink = `https://mumbai.polygonscan.com/tx/${ receipt.transactionHash }` + + } catch ( err: any ) + { + console.error( err ); + } +} + + +async function executeTransaction () +{ + await createAccount(); + const sessionFileStorage: SessionFileStorage = new SessionFileStorage( address ) + await erc20Transfer( sessionFileStorage, "0.019" ) + await erc20Transfer( sessionFileStorage, "0.018" ) +} + +executeTransaction(); + +``` +
+ +If you would like to see the completed project on custom session storage, do checkout: https://github.com/bcnmy/custom-session-storage-tutorial \ No newline at end of file diff --git a/docs/tutorials/customSessionKeys/initializeaccount.md b/docs/tutorials/customSessionKeys/initializeaccount.md new file mode 100644 index 00000000..70e3a926 --- /dev/null +++ b/docs/tutorials/customSessionKeys/initializeaccount.md @@ -0,0 +1,115 @@ +--- +sidebar_label: 'Initialize Smart Account' +sidebar_position: 2 +--- +# Initialize Smart Account + +In this step we'll set up our node js script to create or display a smart account in our command prompt. + +:::info +This tutorial has a setup step in the previous section: [Environment Setup](environmentsetup) +::: + +## Initialization + +Let’s import our bundler package, and providers from the ethers package: + +```typescript +import { IBundler, Bundler } from '@biconomy/bundler' +import { DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account" +import { providers } from 'ethers' +import { ChainId } from "@biconomy/core-types" +``` + +IBundler is the typing for the Bundler class that we will create a new instance of. + + +### **Initial Configuration** + +```typescript +const bundler: IBundler = new Bundler({ + bundlerUrl: 'https://bundler.biconomy.io/api/v2/80001/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44', + chainId: ChainId.POLYGON_MUMBAI, + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, + }) +``` + +- Now we create an instance of our bundler with the following: + - a bundler url which you can retrieve from the Biconomy Dashboard + - chain ID, in this case we’re using Polygon Mumbai + - and default entry point address imported from the account package + + +```typescript +import { BiconomySmartAccountV2, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account" +import { Wallet, providers, ethers } from 'ethers'; +``` + +Update your import from the account package to also include BiconomySmartAccountV2 and also import Wallet, providers, and ethers from the ethers package. + +```typescript +const provider = new providers.JsonRpcProvider("https://rpc.ankr.com/polygon_mumbai") +const wallet = new Wallet(process.env.PRIVATE_KEY || "", provider); +``` + +- We create a provider using a public RPC provider endpoint from ankr, feel free to use any service here such as Infura or Alchemy if you wish. We encourage you to use a private RPC endpoint for better efficiency in userOp creation. +- Next we create an instance of the wallet associated to our Private key. + +Now lets sup our paymaster. Update your imports to contain the following values: + +```typescript +import { + IPaymaster, + BiconomyPaymaster, + IHybridPaymaster, + PaymasterMode, + SponsorUserOperationDto, +} from '@biconomy/paymaster' +``` + +We'll need these to help us execute our gasless transaction. The first thing we want to do is create an instance of our paymaster: + +```typescript +const paymaster: IPaymaster = new BiconomyPaymaster({ + paymasterUrl: 'https://paymaster.biconomy.io/api/v1/80001/Tpk8nuCUd.70bd3a7f-a368-4e5a-af14-80c7f1fcda1a' +}) +``` + +:::info +Note that using the above paymaster URL will only work on Polygon Mumbai network and will only allow sponsored transactions from the contract address mentioned at the start of this tutorial. If you would like to learn how to use our dashboard to get your own paymaster url on any of our supported chains make sure to check out our [Dashboard Documentation](/category/biconomy-dashboard/) +::: + +Next step is to specify that we want the ECDSA module for our smart account. Update the imports to include the following: + +```typescript +import { ECDSAOwnershipValidationModule, DEFAULT_ECDSA_OWNERSHIP_MODULE } from "@biconomy/modules"; +``` + + + +Now lets initialize the module and use the `create` method on the `BiconomySmartAccountV2` class to generate a new instance of our smart account. Additional information about this method can be found [here](https://docs.biconomy.io/Account/methods/create). + +```typescript +async function createAccount() { + + const module = await ECDSAOwnershipValidationModule.create({ + signer: wallet, + moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE + }) + + let biconomySmartAccount = await BiconomySmartAccountV2.create({ + chainId: ChainId.POLYGON_MUMBAI, + bundler: bundler, + paymaster: paymaster, + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, + defaultValidationModule: module, + activeValidationModule: module +}) + console.log("address: ", await biconomySmartAccount.getAccountAddress()) + return biconomySmartAccount; +} + +createAccount() +``` + +After running this script your command prompt should then display the address of your smart account. In the next section we'll execute our first gasless transaction by minting an NFT. \ No newline at end of file From a01b47fec819d90e1ee2d2f474627d5e452aac16 Mon Sep 17 00:00:00 2001 From: Himanshu garg Date: Thu, 30 Nov 2023 15:28:10 +0530 Subject: [PATCH 2/4] add more comments --- .../customSessionKeys/createSession.md | 106 ++++++++-------- .../customSessionKeys/customSessionStorage.md | 7 +- .../customSessionKeys/environmentsetup.md | 2 +- .../customSessionKeys/erc20Transfer.md | 116 +++++++++--------- .../customSessionKeys/initializeaccount.md | 2 +- .../customSessionKeys/introduction.md | 15 +++ 6 files changed, 127 insertions(+), 121 deletions(-) create mode 100644 docs/tutorials/customSessionKeys/introduction.md diff --git a/docs/tutorials/customSessionKeys/createSession.md b/docs/tutorials/customSessionKeys/createSession.md index a316d00e..877ec627 100644 --- a/docs/tutorials/customSessionKeys/createSession.md +++ b/docs/tutorials/customSessionKeys/createSession.md @@ -1,6 +1,6 @@ --- sidebar_label: 'Session Module' -sidebar_position: 4 +sidebar_position: 5 --- # Session Module @@ -14,92 +14,88 @@ TypeScript that allows user to create a session. This tutorial will be done on the Polygon Mumbai Network. We will be using session Module for this. -First we will import sessionKeyManagerModule and DEFAULT_SESSION_KEY_MANAGER_MODULE from Biconomy Modules package. +We will import sessionKeyManagerModule and DEFAULT_SESSION_KEY_MANAGER_MODULE from Biconomy Modules package. First of all we will initialise the sessionFileStorage using our custom File storage ```typescript - const sessionFileStorage: SessionFileStorage = new SessionFileStorage(address) +const sessionFileStorage: SessionFileStorage = new SessionFileStorage(address) ``` +We store the private key in the file using following method. This saves the signer in the file. -We will now use the Session Key Manager Module to create a session module using the module address and smart account address and the custom session storage. This is an important relationship to establish - the module provided by the SDK gives you an easy way to interact with modules you write on a smart contract with whatever arbitrary validation logic you need. +```typescript +const sessionSigner = ethers.Wallet.createRandom(); +const sessionKeyEOA = await sessionSigner.getAddress(); +await sessionFileStorage.addSigner(sessionSigner) +``` + +We use the Session Key Manager Module to create a session module using the module address and smart account address and the custom session storage. This is an important relationship to establish - the module provided by the SDK gives you an easy way to interact with modules you write on a smart contract with whatever arbitrary validation logic you need. ```typescript - const sessionModule = await SessionKeyManagerModule.create({ - moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, - smartAccountAddress: address, - sessionStorageClient: sessionFileStorage - }); +const sessionModule = await SessionKeyManagerModule.create({ + moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, + smartAccountAddress: address, + sessionStorageClient: sessionFileStorage +}); ``` -In this section we will cover a deployed contract that validates specific permissions to execute ERC20 token transfers. Using this ERC20 Validation module you will be able to create a dApp that allows user to send a limited amount of funds to a specific address without needing to sign a transaction every single time. In the following example user can only transfer 50 tokens at once. +now, we will cover a deployed contract that validates specific permissions to execute ERC20 token transfers. Using this ERC20 Validation module you will be able to create a dApp that allows user to send a limited amount of funds to a specific address without needing to sign a transaction every single time. In the following example user can only transfer 50 tokens at once. ```typescript - const sessionKeyData = defaultAbiCoder.encode( - ["address", "address", "address", "uint256"], - [ - sessionKeyEOA, - "0xdA5289fCAAF71d52a80A254da614a192b693e977", // erc20 token address - "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address - ethers.utils.parseUnits("50".toString(), 6).toHexString(), // 50 usdc amount - ] - ); +const sessionKeyData = defaultAbiCoder.encode( + ["address", "address", "address", "uint256"], + [sessionKeyEOA, + "0xdA5289fCAAF71d52a80A254da614a192b693e977", // erc20 token address + "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address + ethers.utils.parseUnits("50".toString(), 6).toHexString(), // 50 usdc amount + ] +); ``` -Now we create the session data itself. We specify how long this should be valid until or how long it is valid after. This should be a unix timestamp to represent the time. Passing 0 on both makes this session never expire, do not do this in production. Next we pass the module address, session key address, and pass the session key data we just created. +next we create the session data. We specify how long this should be valid until or how long it is valid after. This should be a unix timestamp to represent the time. Passing 0 on both makes this session never expire, do not do this in production. Next we pass the module address, session key address, and pass the session key data we just created. ```typescript - - const sessionTxData = await sessionModule.createSessionData([ - { - validUntil: 0, - validAfter: 0, - sessionValidationModule: erc20ModuleAddr, - sessionPublicKey: sessionKeyEOA, - sessionKeyData: sessionKeyData, - }, - ]); +const sessionTxData = await sessionModule.createSessionData([{ + validUntil: 0, + validAfter: 0, + sessionValidationModule: erc20ModuleAddr, + sessionPublicKey: sessionKeyEOA, + sessionKeyData: sessionKeyData, +}]); ``` -We're going to be tracking if the session key module is already enabled. -This will check if a session module is enabled - it will return if we do not have an address, smart Account, or provider and will then enable the module for our smartAccount if needed. +We're going to be tracking if the session key module is already enabled. If we need to enable session key module, we create a transaction using the getEnableModuleData and pass the session key manager module address and push this to the array. + ```typescript - const isEnabled = await smartAccount.isModuleEnabled(DEFAULT_SESSION_KEY_MANAGER_MODULE) - if (!isEnabled) - { - const enableModuleTrx = await smartAccount.getEnableModuleData( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ); - transactionArray.push( enableModuleTrx ); - } +const isEnabled = await smartAccount.isModuleEnabled(DEFAULT_SESSION_KEY_MANAGER_MODULE) +if (!isEnabled) { + const enableModuleTrx = await smartAccount.getEnableModuleData(DEFAULT_SESSION_KEY_MANAGER_MODULE); + transactionArray.push(enableModuleTrx); +} ``` -Finally if we need to enable session key module we create a transactione here as well using the getEnableModuleData and pass the session key manager module address and push this to the array. Additionally we push the session transaction to the array as well, we will be batching these transactions together. - Next we will build a userOp and use the smart account to send it to Bundler. ```typescript +let partialUserOp = await smartAccount.buildUserOp(transactionArray, { + paymasterServiceData: { + mode: PaymasterMode.SPONSORED, + } +}); + +const userOpResponse = await smartAccount.sendUserOp(partialUserOp); +console.log(`userOp Hash: ${ userOpResponse.userOpHash }`); - let partialUserOp = await smartAccount.buildUserOp( transactionArray, { - paymasterServiceData: { - mode: PaymasterMode.SPONSORED, - } - } ); - console.log( partialUserOp ) - const userOpResponse = await smartAccount.sendUserOp( - partialUserOp - ); - console.log( `userOp Hash: ${ userOpResponse.userOpHash }` ); - const transactionDetails = await userOpResponse.wait(); - console.log( "txHash", transactionDetails.receipt.transactionHash ); +const transactionDetails = await userOpResponse.wait(); +console.log("txHash", transactionDetails.receipt.transactionHash); ``` -Now with this implemented let's take a look at executing the ERC20 token transfer with a session in the next section. +Now with this implemented, let's take a look at executing the ERC20 token transfer with this session in the next section. Checkout below for entire code snippet diff --git a/docs/tutorials/customSessionKeys/customSessionStorage.md b/docs/tutorials/customSessionKeys/customSessionStorage.md index 88145e0c..34f24916 100644 --- a/docs/tutorials/customSessionKeys/customSessionStorage.md +++ b/docs/tutorials/customSessionKeys/customSessionStorage.md @@ -1,6 +1,6 @@ --- sidebar_label: 'Create Custom session' -sidebar_position: 3 +sidebar_position: 4 --- # Create custom session @@ -30,7 +30,7 @@ export class SessionFileStorage implements ISessionStorage { this.smartAccountAddress = smartAccountAddress.toLowerCase(); } - + // This method reads data from the file and returns it in the JSON format private async readDataFromFile ( type: "sessions" | "signers" ): Promise { return new Promise( ( resolve ) => @@ -94,7 +94,7 @@ export class SessionFileStorage implements ISessionStorage throw new Error( "Either pass sessionId or a combination of sessionPublicKey and sessionValidationModule address." ); } } - + // Session store is in the form of mekrleRoot and leafnodes, each object will have a root and an array of leafNodes. private async getSessionStore (): Promise { try @@ -158,6 +158,7 @@ export class SessionFileStorage implements ISessionStorage } return session; } + async addSessionData ( leaf: SessionLeafNode ): Promise { console.log( "Add session Data", leaf ) diff --git a/docs/tutorials/customSessionKeys/environmentsetup.md b/docs/tutorials/customSessionKeys/environmentsetup.md index da7838a8..5cf2c179 100644 --- a/docs/tutorials/customSessionKeys/environmentsetup.md +++ b/docs/tutorials/customSessionKeys/environmentsetup.md @@ -1,6 +1,6 @@ --- sidebar_label: 'Environment Set up' -sidebar_position: 1 +sidebar_position: 2 --- # Environment set up diff --git a/docs/tutorials/customSessionKeys/erc20Transfer.md b/docs/tutorials/customSessionKeys/erc20Transfer.md index 41ddab33..7f245f23 100644 --- a/docs/tutorials/customSessionKeys/erc20Transfer.md +++ b/docs/tutorials/customSessionKeys/erc20Transfer.md @@ -1,6 +1,6 @@ --- sidebar_label: 'ERC20 transfer' -sidebar_position: 5 +sidebar_position: 6 --- # ERC20 transfer @@ -12,14 +12,14 @@ We specify the erc20 module address and get the private key we stored in our sto ```typescript - const erc20ModuleAddr = "0x000000D50C68705bd6897B2d17c7de32FB519fDA"; - const sessionKeyPrivKey = sessionFileStorage.getSignerBySession({}); - if (!sessionKeyPrivKey) { - console.log("Session key not found please create session"); - return; - } - const sessionSigner = new ethers.Wallet(sessionKeyPrivKey); - console.log("sessionSigner", sessionSigner); +const erc20ModuleAddr = "0x000000D50C68705bd6897B2d17c7de32FB519fDA"; +const sessionKeyPrivKey = sessionFileStorage.getSignerBySession({}); +if (!sessionKeyPrivKey) { + console.log("Session key not found please create session"); + return; +} +const sessionSigner = new ethers.Wallet(sessionKeyPrivKey); +console.log("sessionSigner", sessionSigner); ``` Now we'll generate a session module using the Session Key Manager Module and then set the active validation module to be the session module. This updates the original configureation on the smart account. @@ -27,79 +27,73 @@ Now we'll generate a session module using the Session Key Manager Module and the ```typescript // generate sessionModule - const sessionModule = await SessionKeyManagerModule.create({ - moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, - smartAccountAddress: address, - }); +const sessionModule = await SessionKeyManagerModule.create({ + moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, + smartAccountAddress: address, +}); - // set active module to sessionModule - smartAccount = smartAccount.setActiveValidationModule(sessionModule); +// set active module to sessionModule +smartAccount = smartAccount.setActiveValidationModule(sessionModule); ``` We now create an instance of the contract. Note that USDC does not have 18 decimals so we update the decimals based on the USDC contract. ```typescript +const tokenContract = new ethers.Contract( + // polygon mumbai usdc address + "0xdA5289fCAAF71d52a80A254da614a192b693e977", + usdcAbi, + provider +); +let decimals = 18; - const tokenContract = new ethers.Contract( - // polygon mumbai usdc address - "0xdA5289fCAAF71d52a80A254da614a192b693e977", - usdcAbi, - provider - ); - let decimals = 18; - - try { - decimals = await tokenContract.decimals(); - } catch (error) { - throw new Error("invalid token address supplied"); - } - +try { + decimals = await tokenContract.decimals(); +} catch (error) { + throw new Error("invalid token address supplied"); +} ``` - Now we will get raw transaction data for a transfer of 1 usdc to the receiver address we specified. Using any other reciever other than the one registered on the session key will result in an error. We are sending 1 USDC in this case but can send up to 50 with this transaction as that is the maximum amount that we specified. - ```typescript - const { data } = await tokenContract.populateTransaction.transfer( - "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address - ethers.utils.parseUnits("1".toString(), decimals) - ); - - const tx1 = { - to: "0xdA5289fCAAF71d52a80A254da614a192b693e977", //erc20 token address - data: data, - value: "0", - }; - +const { data } = await tokenContract.populateTransaction.transfer( + "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address + ethers.utils.parseUnits("1".toString(), decimals) +); +const tx1 = { + to: "0xdA5289fCAAF71d52a80A254da614a192b693e977", //erc20 token address + data: data, + value: "0", +}; ``` Now we build the user op and send it for execution. Note the additional arguments you can add in the `buildUserOp` method such as overrides if needed, ability to skip bundler gas estimations, and most importantly params object that will contain information about the session signer and session validation module. ```typescript - let userOp = await smartAccount.buildUserOp([tx1], { - overrides: { - // signature: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000456b395c4e107e0302553b90d1ef4a32e9000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000db3d753a1da5a6074a9f74f39a0a779d3300000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000bfe121a6dcf92c49f6c2ebd4f306ba0ba0ab6f1c000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e97700000000000000000000000042138576848e839827585a3539305774d36b96020000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041feefc797ef9e9d8a6a41266a85ddf5f85c8f2a3d2654b10b415d348b150dabe82d34002240162ed7f6b7ffbc40162b10e62c3e35175975e43659654697caebfe1c00000000000000000000000000000000000000000000000000000000000000" - // callGasLimit: 2000000, // only if undeployed account - // verificationGasLimit: 700000 - }, - skipBundlerGasEstimation: false, - params: { - sessionSigner: sessionSigner, - sessionValidationModule: erc20ModuleAddr, - }, - }); - - // send user op - const userOpResponse = await smartAccount.sendUserOp(userOp, { - sessionSigner: sessionSigner, - sessionValidationModule: erc20ModuleAddr, - }); - +let userOp = await smartAccount.buildUserOp([tx1], { + overrides: { + // signature: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000456b395c4e107e0302553b90d1ef4a32e9000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000db3d753a1da5a6074a9f74f39a0a779d3300000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000bfe121a6dcf92c49f6c2ebd4f306ba0ba0ab6f1c000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e97700000000000000000000000042138576848e839827585a3539305774d36b96020000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041feefc797ef9e9d8a6a41266a85ddf5f85c8f2a3d2654b10b415d348b150dabe82d34002240162ed7f6b7ffbc40162b10e62c3e35175975e43659654697caebfe1c00000000000000000000000000000000000000000000000000000000000000" + // callGasLimit: 2000000, // only if undeployed account + // verificationGasLimit: 700000 + }, + skipBundlerGasEstimation: false, + params: { + sessionSigner: sessionSigner, + sessionValidationModule: erc20ModuleAddr, + }, +}); + +// send user op +const userOpResponse = await smartAccount.sendUserOp(userOp, { + sessionSigner: sessionSigner, + sessionValidationModule: erc20ModuleAddr, +}); ``` Finaly to give the user a succesful feedback we provide them with a link to the transaction once it has been executed. Running this code should now allow you to sign in using your EOA, create a session, and then send USDC without the need to sign any further transactions! + Checkout below for entire code snippet
Expand for Code diff --git a/docs/tutorials/customSessionKeys/initializeaccount.md b/docs/tutorials/customSessionKeys/initializeaccount.md index 70e3a926..765e70e2 100644 --- a/docs/tutorials/customSessionKeys/initializeaccount.md +++ b/docs/tutorials/customSessionKeys/initializeaccount.md @@ -1,6 +1,6 @@ --- sidebar_label: 'Initialize Smart Account' -sidebar_position: 2 +sidebar_position: 3 --- # Initialize Smart Account diff --git a/docs/tutorials/customSessionKeys/introduction.md b/docs/tutorials/customSessionKeys/introduction.md new file mode 100644 index 00000000..d998cbce --- /dev/null +++ b/docs/tutorials/customSessionKeys/introduction.md @@ -0,0 +1,15 @@ +--- +sidebar_label: 'Introduction' +sidebar_position: 1 +--- + +# Introduction + +This tutorial will cover how to create a custom Session storage for Dapps. In this tutorial we will: + +- Create File based custom session storage, which will be used to save the session keys and signers +- Go over a smart contract that allows for sessions to be validated for ERC20 token transfers. +- Go over initilization and creation of a session module in Node JS. +- Execute a basic ERC20 token transfer without the need to sign + + From 14701ffd9bb3fd4fbfc4ca051e6351375aff489e Mon Sep 17 00:00:00 2001 From: Himanshu garg Date: Thu, 30 Nov 2023 20:25:53 +0530 Subject: [PATCH 3/4] resolve comments --- docs/tutorials/customSessionKeys/erc20Transfer.md | 6 +----- docs/tutorials/customSessionKeys/introduction.md | 9 ++++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/customSessionKeys/erc20Transfer.md b/docs/tutorials/customSessionKeys/erc20Transfer.md index 7f245f23..849c3fad 100644 --- a/docs/tutorials/customSessionKeys/erc20Transfer.md +++ b/docs/tutorials/customSessionKeys/erc20Transfer.md @@ -71,11 +71,7 @@ Now we build the user op and send it for execution. Note the additional argument ```typescript let userOp = await smartAccount.buildUserOp([tx1], { - overrides: { - // signature: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000456b395c4e107e0302553b90d1ef4a32e9000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000db3d753a1da5a6074a9f74f39a0a779d3300000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000bfe121a6dcf92c49f6c2ebd4f306ba0ba0ab6f1c000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e97700000000000000000000000042138576848e839827585a3539305774d36b96020000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041feefc797ef9e9d8a6a41266a85ddf5f85c8f2a3d2654b10b415d348b150dabe82d34002240162ed7f6b7ffbc40162b10e62c3e35175975e43659654697caebfe1c00000000000000000000000000000000000000000000000000000000000000" - // callGasLimit: 2000000, // only if undeployed account - // verificationGasLimit: 700000 - }, + overrides: {}, skipBundlerGasEstimation: false, params: { sessionSigner: sessionSigner, diff --git a/docs/tutorials/customSessionKeys/introduction.md b/docs/tutorials/customSessionKeys/introduction.md index d998cbce..4f1b299b 100644 --- a/docs/tutorials/customSessionKeys/introduction.md +++ b/docs/tutorials/customSessionKeys/introduction.md @@ -5,7 +5,14 @@ sidebar_position: 1 # Introduction -This tutorial will cover how to create a custom Session storage for Dapps. In this tutorial we will: +This tutorial will cover how to create a custom Session storage for Dapps. +:::tip + +Check out an end-to-end integration of custom session storage [repo](https://github.com/bcnmy/custom-session-storage-tutorial)! + +::: + +In this tutorial we will: - Create File based custom session storage, which will be used to save the session keys and signers - Go over a smart contract that allows for sessions to be validated for ERC20 token transfers. From d737eaf64c7f416b0c933fe7f0dcecd1bf6ec8db Mon Sep 17 00:00:00 2001 From: Himanshu garg Date: Mon, 4 Dec 2023 15:18:20 +0530 Subject: [PATCH 4/4] PR comments fix --- docs/Account/methods/userOpMethods.md | 4 +- .../customSessionKeys/introduction.md | 22 ----------- .../_category_.json | 2 +- .../createSession.md | 39 +++++++++++++------ .../customSessionStorage.md | 16 +------- .../environmentsetup.md | 0 .../erc20Transfer.md | 6 --- .../initializeaccount.md | 0 .../introduction.md | 22 +++++++++++ 9 files changed, 53 insertions(+), 58 deletions(-) delete mode 100644 docs/tutorials/customSessionKeys/introduction.md rename docs/tutorials/{customSessionKeys => customSessionStorageClient}/_category_.json (75%) rename docs/tutorials/{customSessionKeys => customSessionStorageClient}/createSession.md (70%) rename docs/tutorials/{customSessionKeys => customSessionStorageClient}/customSessionStorage.md (88%) rename docs/tutorials/{customSessionKeys => customSessionStorageClient}/environmentsetup.md (100%) rename docs/tutorials/{customSessionKeys => customSessionStorageClient}/erc20Transfer.md (98%) rename docs/tutorials/{customSessionKeys => customSessionStorageClient}/initializeaccount.md (100%) create mode 100644 docs/tutorials/customSessionStorageClient/introduction.md diff --git a/docs/Account/methods/userOpMethods.md b/docs/Account/methods/userOpMethods.md index b76ca0ff..cf285adb 100644 --- a/docs/Account/methods/userOpMethods.md +++ b/docs/Account/methods/userOpMethods.md @@ -209,7 +209,7 @@ type UserOpReceipt = { } ``` -You can use `waitForTxHash` to get the userOpHash or transactionHash, without waiting for the transaction to be mined. +You can use `waitForTxHash` to get the transactionHash and status, without waiting for the transaction to be mined. ```ts @@ -224,7 +224,7 @@ The userOpResponse has one method that you will use ### Using modules with sendUserOp() -Similar to building a `userOp` we need to ensure that any modules used for additional validation or execution logic are specified in the `sendUserOp` method. +Similar to building a `userOp` we need to ensure that any modules used for additional validation or execution logic are specified in the `sendUserOp` method. Currently, this only applies for session key module requirements. These params will be the same ModuleInfo params as outlined in the `buildUserOp` flow. diff --git a/docs/tutorials/customSessionKeys/introduction.md b/docs/tutorials/customSessionKeys/introduction.md deleted file mode 100644 index 4f1b299b..00000000 --- a/docs/tutorials/customSessionKeys/introduction.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -sidebar_label: 'Introduction' -sidebar_position: 1 ---- - -# Introduction - -This tutorial will cover how to create a custom Session storage for Dapps. -:::tip - -Check out an end-to-end integration of custom session storage [repo](https://github.com/bcnmy/custom-session-storage-tutorial)! - -::: - -In this tutorial we will: - -- Create File based custom session storage, which will be used to save the session keys and signers -- Go over a smart contract that allows for sessions to be validated for ERC20 token transfers. -- Go over initilization and creation of a session module in Node JS. -- Execute a basic ERC20 token transfer without the need to sign - - diff --git a/docs/tutorials/customSessionKeys/_category_.json b/docs/tutorials/customSessionStorageClient/_category_.json similarity index 75% rename from docs/tutorials/customSessionKeys/_category_.json rename to docs/tutorials/customSessionStorageClient/_category_.json index 9d405451..95c53978 100644 --- a/docs/tutorials/customSessionKeys/_category_.json +++ b/docs/tutorials/customSessionStorageClient/_category_.json @@ -1,5 +1,5 @@ { - "label": "Custom Session Keys Tutorial", + "label": "Custom Session Storage Tutorial", "position": 5, "link": { "type": "generated-index", diff --git a/docs/tutorials/customSessionKeys/createSession.md b/docs/tutorials/customSessionStorageClient/createSession.md similarity index 70% rename from docs/tutorials/customSessionKeys/createSession.md rename to docs/tutorials/customSessionStorageClient/createSession.md index 877ec627..44e39776 100644 --- a/docs/tutorials/customSessionKeys/createSession.md +++ b/docs/tutorials/customSessionStorageClient/createSession.md @@ -4,8 +4,7 @@ sidebar_position: 5 --- # Session Module -In this guide, we will walk through creating a basic Node.js script using -TypeScript that allows user to create a session. +As described in the last section, an off-chain storage solution is essential for the session leafs. In this context, we will employ Files for storing this data. Any customized storage layer must adhere to a specific interface, ensuring the correct implementation of methods for saving or retrieving information. Alternatively, one can develop their own implementation using a Database server layer. This guide will lead you through the process of creating a fundamental Node.js script using TypeScript that enables users to initiate a session.. :::info This tutorial has a previous other steps in the previous sections: [Environment Setup](environmentsetup) and @@ -30,7 +29,12 @@ const sessionKeyEOA = await sessionSigner.getAddress(); await sessionFileStorage.addSigner(sessionSigner) ``` -We use the Session Key Manager Module to create a session module using the module address and smart account address and the custom session storage. This is an important relationship to establish - the module provided by the SDK gives you an easy way to interact with modules you write on a smart contract with whatever arbitrary validation logic you need. +The Session Key Manager module is responsible for overseeing the storage of session leaf data generated by any type of session key. It utilizes a storage client to keep track of all leaves, enabling the generation of a Merkle proof for a specific leaf. Module provided by the SDK gives you easy way to + +* create session key generation data +* manage leaf information for merkle proof generation +* gain dummy signature for userop estimation +* generate userop.signature for transaction that utilises session key ```typescript @@ -39,10 +43,9 @@ const sessionModule = await SessionKeyManagerModule.create({ smartAccountAddress: address, sessionStorageClient: sessionFileStorage }); - ``` -now, we will cover a deployed contract that validates specific permissions to execute ERC20 token transfers. Using this ERC20 Validation module you will be able to create a dApp that allows user to send a limited amount of funds to a specific address without needing to sign a transaction every single time. In the following example user can only transfer 50 tokens at once. +now, we will use a deployed contract that validates specific permissions to execute ERC20 token transfers. ERC20 Session Validation Module is one kind of Session Validation Module. Using this ERC20 Validation module you will be able to create a dApp that allows user to send a limited amount of funds to a specific address without needing to sign a transaction every single time. In the following example user can only transfer 50 tokens at once. Individuals have the flexibility to create their own Session Validation Module (SVM), deploy it, utilize its address as the sessionValidationModule, and generate sessionKeyData based on how permissions are configured. ```typescript const sessionKeyData = defaultAbiCoder.encode( @@ -53,10 +56,9 @@ const sessionKeyData = defaultAbiCoder.encode( ethers.utils.parseUnits("50".toString(), 6).toHexString(), // 50 usdc amount ] ); - ``` -next we create the session data. We specify how long this should be valid until or how long it is valid after. This should be a unix timestamp to represent the time. Passing 0 on both makes this session never expire, do not do this in production. Next we pass the module address, session key address, and pass the session key data we just created. +next, we create the session data. We specify how long this should be valid until or how long it is valid after. This should be a unix timestamp to represent the time. Passing 0 on both makes this session never expire, do not do this in production. Next we pass the session validation module address, session public key, and the session key data we just created. ```typescript @@ -67,20 +69,33 @@ const sessionTxData = await sessionModule.createSessionData([{ sessionPublicKey: sessionKeyEOA, sessionKeyData: sessionKeyData, }]); + ``` -We're going to be tracking if the session key module is already enabled. If we need to enable session key module, we create a transaction using the getEnableModuleData and pass the session key manager module address and push this to the array. +We initialise a transaction array and push the create session Transaction. These give (to, value, data) as traditional transaction. Subsequently, we invoke the buildUserOp function to create a user operation for either an array of transactions or a single transaction. + +```typescript + +const transactionArray = []; +const setSessiontrx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, + data: sessionTxData.data, +}; +transactionArray.push( setSessiontrx ) + +``` + +We're going to be tracking if the session key module is already enabled. If we need to enable session key module, we create a transaction using the getEnableModuleData and pass the session key manager module address and push this to the array. ```typescript const isEnabled = await smartAccount.isModuleEnabled(DEFAULT_SESSION_KEY_MANAGER_MODULE) if (!isEnabled) { const enableModuleTrx = await smartAccount.getEnableModuleData(DEFAULT_SESSION_KEY_MANAGER_MODULE); transactionArray.push(enableModuleTrx); -} - +} ``` -Next we will build a userOp and use the smart account to send it to Bundler. +Next we will build a userOp and use the smart account to send it to Bundler. Ensure that a paymaster is setup and the corresponding gas tank has sufficient funds to sponsor the transactions. Also enable the session validation module address in the policy section for the paymaster. ```typescript let partialUserOp = await smartAccount.buildUserOp(transactionArray, { @@ -95,7 +110,7 @@ console.log(`userOp Hash: ${ userOpResponse.userOpHash }`); const transactionDetails = await userOpResponse.wait(); console.log("txHash", transactionDetails.receipt.transactionHash); ``` -Now with this implemented, let's take a look at executing the ERC20 token transfer with this session in the next section. +Once this transaction is successful, a session gets enabled on chain. Anyone holding the session private key can make use of the session (till te time session is valid). Now with this implemented, let's take a look at executing the ERC20 token transfer with this session in the next section. Checkout below for entire code snippet diff --git a/docs/tutorials/customSessionKeys/customSessionStorage.md b/docs/tutorials/customSessionStorageClient/customSessionStorage.md similarity index 88% rename from docs/tutorials/customSessionKeys/customSessionStorage.md rename to docs/tutorials/customSessionStorageClient/customSessionStorage.md index 34f24916..b6729f4d 100644 --- a/docs/tutorials/customSessionKeys/customSessionStorage.md +++ b/docs/tutorials/customSessionStorageClient/customSessionStorage.md @@ -15,7 +15,7 @@ import { ISessionStorage, SessionLeafNode, SessionSearchParam, SessionStatus } f We will need to implement all the interface methods. -Here is an example of File storage implementation. It saves the session keys and signers in files. Users will need to create two file in the root folder with $smartAccountAddress_sessions.json and $smartAccountAddress_signers.json names. For instance, if the account address is 0x123 then create 0x123_sessions.json and 0x123_signers.json. +Here is an example of File storage implementation. It saves the session leafs data and signers in files. For testing purpose developer will need to create two files in the root folder for each user with $smartAccountAddress_sessions.json and $smartAccountAddress_signers.json names. These files can be created automatically, based on where and how it gets stored. For instance, if the account address is 0x123 then create 0x123_sessions.json and 0x123_signers.json to run this tutorial. ```typescript import * as fs from "fs"; @@ -133,24 +133,10 @@ export class SessionFileStorage implements ISessionStorage async getSessionData ( param: SessionSearchParam ): Promise { - // this.validateSearchParam(param); const sessions = ( await this.getSessionStore() ).leafNodes; console.log( "Got sessions", sessions ) const session = sessions[ 0 ]; - // const session = sessions.find((s: SessionLeafNode) => { - // if (param.sessionID) { - // return s.sessionID === param.sessionID && (!param.status || s.status === param.status); - // } else if (param.sessionPublicKey && param.sessionValidationModule) { - // return ( - // s.sessionPublicKey === this.toLowercaseAddress(param.sessionPublicKey) && - // s.sessionValidationModule === this.toLowercaseAddress(param.sessionValidationModule) && - // (!param.status || s.status === param.status) - // ); - // } else { - // return undefined; - // } - // }); if ( !session ) { diff --git a/docs/tutorials/customSessionKeys/environmentsetup.md b/docs/tutorials/customSessionStorageClient/environmentsetup.md similarity index 100% rename from docs/tutorials/customSessionKeys/environmentsetup.md rename to docs/tutorials/customSessionStorageClient/environmentsetup.md diff --git a/docs/tutorials/customSessionKeys/erc20Transfer.md b/docs/tutorials/customSessionStorageClient/erc20Transfer.md similarity index 98% rename from docs/tutorials/customSessionKeys/erc20Transfer.md rename to docs/tutorials/customSessionStorageClient/erc20Transfer.md index 849c3fad..5a6cb567 100644 --- a/docs/tutorials/customSessionKeys/erc20Transfer.md +++ b/docs/tutorials/customSessionStorageClient/erc20Transfer.md @@ -178,14 +178,12 @@ const erc20Transfer = async ( sessionFileStorage: SessionFileStorage, amount: st //@ts-ignore const sessionSigner = new ethers.Wallet( sessionKeyPrivKey ); console.log( "sessionSigner", sessionSigner ); - console.log( "created -1" ) // generate sessionModule const sessionModule = await SessionKeyManagerModule.create( { moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, smartAccountAddress: address, sessionStorageClient: sessionFileStorage } ); - console.log( "created 0" ) // set active module to sessionModule smartAccount = smartAccount.setActiveValidationModule( sessionModule ); @@ -204,13 +202,11 @@ const erc20Transfer = async ( sessionFileStorage: SessionFileStorage, amount: st { throw new Error( "invalid token address supplied" ); } - console.log( "created 1 decimals", decimals ) const { data } = await tokenContract.populateTransaction.transfer( "0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address ethers.utils.parseUnits( amount, decimals ) ); - console.log( "created 2" ) // generate tx data to erc20 transfer const tx1 = { to: "0xdA5289fCAAF71d52a80A254da614a192b693e977", //erc20 token address @@ -232,13 +228,11 @@ const erc20Transfer = async ( sessionFileStorage: SessionFileStorage, amount: st sessionValidationModule: erc20ModuleAddr, }, } ); - console.log( "created 3" ) // send user op const userOpResponse = await smartAccount.sendUserOp( userOp, { sessionSigner: sessionSigner, sessionValidationModule: erc20ModuleAddr, } ); - console.log( "created 4" ) console.log( "userOpHash", userOpResponse ); const { receipt } = await userOpResponse.wait( 1 ); console.log( "txHash", receipt.transactionHash ); diff --git a/docs/tutorials/customSessionKeys/initializeaccount.md b/docs/tutorials/customSessionStorageClient/initializeaccount.md similarity index 100% rename from docs/tutorials/customSessionKeys/initializeaccount.md rename to docs/tutorials/customSessionStorageClient/initializeaccount.md diff --git a/docs/tutorials/customSessionStorageClient/introduction.md b/docs/tutorials/customSessionStorageClient/introduction.md new file mode 100644 index 00000000..426f15b9 --- /dev/null +++ b/docs/tutorials/customSessionStorageClient/introduction.md @@ -0,0 +1,22 @@ +--- +sidebar_label: 'Introduction' +sidebar_position: 1 +--- + +# Introduction + +The session storage module provides a way to create sessions for an App, where users can make use of the session for signing purpose. These session keys are stored on chain, and information for each leaf is stored in offchain data storage. By default it gets stored in the browser local storage. This tutorial covers how one can create a custom Session storage for their Dapps. It can be a database or file. In this we will go over the implementation of the File storage for their session leaf. +:::tip + +Check out an end-to-end integration of custom session storage [repo](https://github.com/bcnmy/custom-session-storage-tutorial)! + +::: + +In this tutorial we will: + +- Create File based custom session storage client, which will be used to save the session keys and signers +- Go over a smart contract that allows for sessions to be validated for ERC20 token transfers. +- Go over initilization and creation of a session module in Node JS. +- Execute a basic ERC20 token transfer without the need to sign + +