Skip to content

Commit

Permalink
Merge pull request #123 from bcnmy/himanshu/initial-feedback
Browse files Browse the repository at this point in the history
Add custom session storage tutorial
  • Loading branch information
himanshugarg06 authored Dec 11, 2023
2 parents 39d705f + d737eaf commit 93b78eb
Show file tree
Hide file tree
Showing 8 changed files with 1,003 additions and 2 deletions.
14 changes: 12 additions & 2 deletions docs/Account/methods/userOpMethods.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserOpReceipt>;
waitForTxHash(): Promise<UserOpStatus>;
}

```
Expand Down Expand Up @@ -208,13 +209,22 @@ type UserOpReceipt = {
}

```
You can use `waitForTxHash` to get the transactionHash and status, 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()

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.

Expand Down
8 changes: 8 additions & 0 deletions docs/tutorials/customSessionStorageClient/_category_.json
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 docs/tutorials/customSessionStorageClient/createSession.md
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>
Loading

0 comments on commit 93b78eb

Please sign in to comment.