Skip to content

Commit

Permalink
refactor: improve x-sessions code
Browse files Browse the repository at this point in the history
BREAKING CHANGE: removed classes in favour of functions and refactored params
  • Loading branch information
bluecco committed Nov 28, 2024
1 parent b6b9961 commit dc16f10
Show file tree
Hide file tree
Showing 17 changed files with 1,321 additions and 1,086 deletions.
130 changes: 81 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ A demo dapp using both sessions and offchain sessions can be found here [https:/

First you need to have a deployed account. This is the account that will authorise the session and interact with the contracts of your dapp.

To sign the session message the method needed is `openSession`. After the user sign the message, a session account can be created using `buildSessionAccount`.
To sign the session message the method needed is `createSession`. After the user sign the message, a session account can be created using `buildSessionAccount`.

This example session will allow the dapp to execute an example endpoint on an example contract without asking the user to approve the transaction again. After signing the session the dapp can execute all transactions listed in `allowedMethods` whenever it wants and as many times as it wants.

Expand All @@ -45,7 +45,7 @@ export type SessionMetadata = {
projectSignature?: Signature
}

type SessionParams = {
type CreateSessionParams = {
sessionKey?: Uint8Array // this is optional. This sdk generate a sessionKey using ec.starkCurve.utils.randomPrivateKey() if not provided
allowedMethods: AllowedMethod[]
expiry: bigint
Expand All @@ -58,13 +58,21 @@ The following snippet show how to create and use a session account
```typescript
import {
SignSessionError,
SessionParams,
openSession,
CreateSessionParams,
createSession,
buildSessionAccount
} from "@argent/x-sessions"
import { ec } from "starknet"

const sessionParams: SessionParams = {
const privateKey = ec.starkCurve.utils.randomPrivateKey()

const sessionKey: SessionKey = {
privateKey,
publicKey: ec.starkCurve.getStarkKey(privateKey)
}

const sessionParams: CreateSessionParams = {
sessionKey,
allowedMethods: [
{
"Contract Address": contractAddress,
Expand All @@ -87,33 +95,22 @@ const sessionParams: SessionParams = {
}

// open session and sign message
const accountSessionSignature = await openSession({
const session = await createSession({
wallet, // StarknetWindowObject
sessionParams, // SessionParams
chainId // StarknetChainId
address, // Account address
chainId, // StarknetChainId
sessionParams // CreateSessionParams
})

// create the session account from the current one that will be used to submit transactions
const sessionRequest = createSessionRequest(
sessionParams.allowedMethods,
sessionParams.expiry,
sessionParams.metaData,
sessionParams.sessionKey
)

const sessionAccount = await buildSessionAccount({
useCacheAuthorisation: false, // optional and defaulted to false, will be added in future developments
accountSessionSignature: stark.formatSignature(
accountSessionSignature
),
sessionRequest,
chainId, // StarknetChainId
session,
sessionKey,
provider: new RpcProvider({
nodeUrl: "https://starknet-sepolia.public.blastapi.io/rpc/v0_7",
chainId: constants.StarknetChainId.SN_SEPOLIA
}),
address, // account address
sessionKey
argentSessionServiceBaseUrl: ARGENT_SESSION_SERVICE_BASE_URL // Optional: defaulted to mainnet url
})

try {
Expand All @@ -138,24 +135,59 @@ Executing transactions “from outside” allows an account to submit transactio
This package expose a method in order to get the Call required to perform an execution from outside.

```typescript
// instantiate argent session service
const beService = new ArgentSessionService(
sessionKey.publicKey,
accountSessionSignature
)
const privateKey = ec.starkCurve.utils.randomPrivateKey()

// instantiate dapp session service
const sessionDappService = new SessionDappService(
beService,
chainId,
sessionKey
)
const sessionKey: SessionKey = {
privateKey,
publicKey: ec.starkCurve.getStarkKey(privateKey)
}

const sessionParams: CreateSessionParams = {
sessionKey,
allowedMethods: [
{
"Contract Address": contractAddress,
selector: "method_selector"
}
],
expiry: Math.floor(
(Date.now() + 1000 * 60 * 60 * 24) / 1000
) as any, // ie: 1 day
sessionKey: ec.starkCurve.utils.randomPrivateKey(),
metaData: {
projectID: "test-dapp",
txFees: [
{
tokenAddress: ETHTokenAddress,
maxAmount: parseUnits("0.1", 18).value.toString()
}
]
}
}

const session = await createSession({
wallet, // StarknetWindowObject
address, // Account address
chainId, // StarknetChainId
sessionParams // CreateSessionParams
})

const sessionAccount = await buildSessionAccount({
useCacheAuthorisation: false, // optional and defaulted to false, will be added in future developments
session,
sessionKey,
provider: new RpcProvider({
nodeUrl: "https://starknet-sepolia.public.blastapi.io/rpc/v0_7",
chainId: constants.StarknetChainId.SN_SEPOLIA
}),
argentSessionServiceBaseUrl: ARGENT_SESSION_SERVICE_BASE_URL // Optional: defaulted to mainnet url
})

// example for creating the calldata
const erc20Contract = new Contract(
Erc20Abi as Abi,
ETHTokenAddress,
sessionAccount as any
sessionAccount
)
const calldata = erc20Contract.populate("transfer", {
recipient: address,
Expand All @@ -164,18 +196,18 @@ const calldata = erc20Contract.populate("transfer", {

// get execute from outside data
const { contractAddress, entrypoint, calldata } =
await sessionDappService.getOutsideExecutionCall(
sessionRequest,
stark.formatSignature(accountSessionSignature),
false,
[calldata],
address, // the account address
chainId,
shortString.encodeShortString("ANY_CALLER"), // Optional: default value ANY_CALLER
execute_after, // Optional: timestamp in seconds - this is the lower value in the range. Default value: 5 mins before Date.now()
execute_before, // Optional: timestamp in seconds - this is the upper value in the range. Default value: 20 mins after Date.now()
nonce: BigNumberish, // Optional: nonce, default value is a random nonce
)
```
await createOutsideExecutionCall({
session,
sessionKey,
calls: [transferCallData],
argentSessionServiceUrl: ARGENT_SESSION_SERVICE_BASE_URL
})

Another account can then use object `{ contractAddress, entrypoint, calldata }` to execute the transaction.
const { signature, outsideExecutionTypedData } =
await createOutsideExecutionTypedData({
session,
sessionKey,
calls: [transferCallData],
argentSessionServiceUrl: ARGENT_SESSION_SERVICE_BASE_URL
})
```
207 changes: 207 additions & 0 deletions src/SessionAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
Account,
ArraySignatureType,
Call,
CallData,
InvocationsSignerDetails,
RPC,
V2InvocationsSignerDetails,
V3InvocationsSignerDetails,
hash,
shortString,
stark,
transaction,
} from "starknet"
import { argentSignTxAndSession } from "./argentBackendUtils"
import { ARGENT_SESSION_SERVICE_BASE_URL } from "./constants"
import { OffChainSession, Session, SessionKey } from "./session.types"
import {
GetAccountWithSessionSignerParams,
GetSessionSignatureForTransactionParams,
} from "./SessionAccount.types"
import { SessionSigner } from "./SessionSigner"
import {
compileSessionHelper,
compileSessionTokenHelper,
signTxAndSession,
} from "./sessionUtils"
import { getSessionTypedData } from "./utils"

const SESSION_MAGIC = shortString.encodeShortString("session-token")

/**
* Class representing a session account for managing transactions with session-based authorization.
*/
export class SessionAccount {
public argentSessionServiceUrl: string

/**
* Creates an instance of SessionAccount.
* @param session - The session object containing session details.
* @param sessionKey - The session key used for signing transactions.
* @param argentSessionServiceUrl - The base URL for the Argent session service.
*/
constructor(
public session: Session,
public sessionKey: SessionKey,
argentSessionServiceUrl: string = ARGENT_SESSION_SERVICE_BASE_URL,
) {
this.argentSessionServiceUrl = argentSessionServiceUrl
}

/**
* Retrieves an account with a session signer.
*
* @param {Object} params - The parameters for the function.
* @param {Provider} params.provider - The provider to use for the account.
* @param {Session} params.session - The session information.
* @param {boolean} [params.cacheAuthorisation=false] - Whether to cache the authorisation signature.
* @returns {Account} The account with the session signer.
*/
public getAccountWithSessionSigner({
provider,
session,
cacheAuthorisation = false,
}: GetAccountWithSessionSignerParams) {
const sessionSigner = new SessionSigner(
(calls: Call[], invocationSignerDetails: InvocationsSignerDetails) => {
return this.signTransaction(
stark.formatSignature(session.authorisationSignature),
session,
calls,
invocationSignerDetails,
cacheAuthorisation,
)
},
)

return new Account(provider, session.address, sessionSigner)
}

private async signTransaction(
sessionAuthorizationSignature: ArraySignatureType,
session: Session,
calls: Call[],
invocationSignerDetails: InvocationsSignerDetails,
cacheAuthorisation: boolean,
): Promise<ArraySignatureType> {
const compiledCalldata = transaction.getExecuteCalldata(
calls,
invocationSignerDetails.cairoVersion,
)

let txHash
if (
Object.values(RPC.ETransactionVersion2).includes(
invocationSignerDetails.version as any,
)
) {
const invocationsSignerDetailsV2 =
invocationSignerDetails as V2InvocationsSignerDetails
txHash = hash.calculateInvokeTransactionHash({
...invocationsSignerDetailsV2,
senderAddress: invocationsSignerDetailsV2.walletAddress,
compiledCalldata,
version: invocationsSignerDetailsV2.version,
})
} else if (
Object.values(RPC.ETransactionVersion3).includes(
invocationSignerDetails.version as any,
)
) {
const invocationsSignerDetailsV3 =
invocationSignerDetails as V3InvocationsSignerDetails
txHash = hash.calculateInvokeTransactionHash({
...invocationsSignerDetailsV3,
senderAddress: invocationsSignerDetailsV3.walletAddress,
compiledCalldata,
version: invocationsSignerDetailsV3.version,
nonceDataAvailabilityMode: stark.intDAM(
invocationsSignerDetailsV3.nonceDataAvailabilityMode,
),
feeDataAvailabilityMode: stark.intDAM(
invocationsSignerDetailsV3.feeDataAvailabilityMode,
),
})
} else {
throw Error("unsupported signTransaction version")
}
return this.getSessionSignatureForTransaction({
sessionAuthorizationSignature,
session,
transactionHash: txHash,
calls,
accountAddress: invocationSignerDetails.walletAddress,
invocationSignerDetails,
cacheAuthorisation,
})
}

/**
* Generates a session signature for a transaction.
*
* @param sessionAuthorizationSignature - The authorization signature for the session.
* @param session - The session object containing session details.
* @param transactionHash - The hash of the transaction.
* @param calls - An array of calls to be made.
* @param accountAddress - The address of the account.
* @param invocationSignerDetails - Details of the invocation signer.
* @param cacheAuthorisation - A boolean indicating whether to cache the authorization.
* @returns A promise that resolves to an array containing the session signature.
*/
public async getSessionSignatureForTransaction({
sessionAuthorizationSignature,
session,
transactionHash,
calls,
accountAddress,
invocationSignerDetails,
cacheAuthorisation,
}: GetSessionSignatureForTransactionParams): Promise<ArraySignatureType> {
const offchainSession: OffChainSession = {
allowed_methods: session.allowedMethods,
expires_at: session.expiresAt,
metadata: session.metadata,
session_key_guid: session.sessionKeyGuid,
}

const compiledSession = compileSessionHelper(offchainSession)

const sessionTypedData = getSessionTypedData(
offchainSession,
this.session.chainId,
)

const sessionSignature = signTxAndSession(
transactionHash,
accountAddress,
sessionTypedData,
cacheAuthorisation,
this.sessionKey,
)

const guardianSignature = await argentSignTxAndSession({
sessionKey: this.sessionKey,
authorisationSignature: this.session.authorisationSignature,
argentSessionServiceBaseUrl: this.argentSessionServiceUrl,
calls,
transactionsDetail: invocationSignerDetails,
sessionTypedData,
sessionSignature,
cacheAuthorisation,
})

const sessionToken = await compileSessionTokenHelper(
compiledSession,
offchainSession,
this.sessionKey,
calls,
sessionSignature,
sessionAuthorizationSignature,
guardianSignature,
cacheAuthorisation,
)

return [SESSION_MAGIC, ...CallData.compile(sessionToken)]
}
}
Loading

0 comments on commit dc16f10

Please sign in to comment.