-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #123 from bcnmy/himanshu/initial-feedback
Add custom session storage tutorial
- Loading branch information
Showing
8 changed files
with
1,003 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"label": "Custom Session Storage Tutorial", | ||
"position": 5, | ||
"link": { | ||
"type": "generated-index", | ||
"description": "Use Custom storage for session keys module with Node JS" | ||
} | ||
} |
268 changes: 268 additions & 0 deletions
268
docs/tutorials/customSessionStorageClient/createSession.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
--- | ||
sidebar_label: 'Session Module' | ||
sidebar_position: 5 | ||
--- | ||
# Session Module | ||
|
||
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 | ||
[Initializing Account](initializeaccount) | ||
::: | ||
|
||
This tutorial will be done on the Polygon Mumbai Network. We will be using session Module for this. | ||
|
||
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 store the private key in the file using following method. This saves the signer in the file. | ||
|
||
```typescript | ||
const sessionSigner = ethers.Wallet.createRandom(); | ||
const sessionKeyEOA = await sessionSigner.getAddress(); | ||
await sessionFileStorage.addSigner(sessionSigner) | ||
``` | ||
|
||
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 | ||
const sessionModule = await SessionKeyManagerModule.create({ | ||
moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, | ||
smartAccountAddress: address, | ||
sessionStorageClient: sessionFileStorage | ||
}); | ||
``` | ||
|
||
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( | ||
["address", "address", "address", "uint256"], | ||
[sessionKeyEOA, | ||
"0xdA5289fCAAF71d52a80A254da614a192b693e977", // erc20 token address | ||
"0x322Af0da66D00be980C7aa006377FCaaEee3BDFD", // receiver address | ||
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 session validation module address, session public key, and the session key data we just created. | ||
|
||
|
||
```typescript | ||
const sessionTxData = await sessionModule.createSessionData([{ | ||
validUntil: 0, | ||
validAfter: 0, | ||
sessionValidationModule: erc20ModuleAddr, | ||
sessionPublicKey: sessionKeyEOA, | ||
sessionKeyData: sessionKeyData, | ||
}]); | ||
|
||
``` | ||
|
||
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. 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, { | ||
paymasterServiceData: { | ||
mode: PaymasterMode.SPONSORED, | ||
} | ||
}); | ||
|
||
const userOpResponse = await smartAccount.sendUserOp(partialUserOp); | ||
console.log(`userOp Hash: ${ userOpResponse.userOpHash }`); | ||
|
||
const transactionDetails = await userOpResponse.wait(); | ||
console.log("txHash", transactionDetails.receipt.transactionHash); | ||
``` | ||
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 | ||
<details> | ||
<summary> Expand for Code </summary> | ||
|
||
```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(); | ||
|
||
|
||
``` | ||
</details> |
Oops, something went wrong.