Skip to content

EIP-7702 Session Key Utils #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,19 +372,59 @@
// Console.WriteLine($"User Wallet address: {await smartEoa.GetAddress()}");

// // Upgrade EOA - This wallet explicitly uses EIP-7702 delegation to the thirdweb MinimalAccount (will delegate upon first tx)
// var signerAddress = await Utils.GetAddressFromENS(client, "vitalik.eth");

// // Transact, will upgrade EOA
// var receipt = await smartEoa.Transfer(chainId: chain, toAddress: await Utils.GetAddressFromENS(client, "vitalik.eth"), weiAmount: 0);
// var receipt = await smartEoa.Transfer(chainId: chain, toAddress: signerAddress, weiAmount: 0);
// Console.WriteLine($"Transfer Receipt: {receipt.TransactionHash}");

// // Double check that it was upgraded
// var isDelegated = await Utils.IsDelegatedAccount(client, chain, smartEoaAddress);
// Console.WriteLine($"Is delegated: {isDelegated}");

// // Create a session key
// var sessionKeyReceipt = await smartEoa.CreateSessionKey(chainId: chain, signerAddress: await Utils.GetAddressFromENS(client, "vitalik.eth"), durationInSeconds: 86400, grantFullPermissions: true);
// var sessionKeyReceipt = await smartEoa.CreateSessionKey(chainId: chain, signerAddress: signerAddress, durationInSeconds: 86400, grantFullPermissions: true);
// Console.WriteLine($"Session key receipt: {sessionKeyReceipt.TransactionHash}");

// // Validate session key config
// var hasFullPermissions = await smartEoa.SignerHasFullPermissions(chain, signerAddress);
// Console.WriteLine($"Signer has full permissions: {hasFullPermissions}");

// var sessionExpiration = await smartEoa.GetSessionExpirationForSigner(chain, signerAddress);
// Console.WriteLine($"Session expires in {sessionExpiration - Utils.GetUnixTimeStampNow()} seconds");

// // Create a session key with granular permissions
// var granularSessionKeyReceipt = await smartEoa.CreateSessionKey(
// chainId: chain,
// signerAddress: signerAddress,
// durationInSeconds: 86400,
// grantFullPermissions: false,
// transferPolicies: new List<TransferSpec>
// {
// new()
// {
// Target = signerAddress,
// MaxValuePerUse = BigInteger.Parse("0.001".ToWei()),
// ValueLimit = new UsageLimit
// {
// LimitType = 1, // Lifetime
// Limit = BigInteger.Parse("0.01".ToWei()),
// Period = 86400, // 1 day
// }
// }
// }
// );

// // Validate session key config
// var sessionState = await smartEoa.GetSessionStateForSigner(chain, signerAddress);
// Console.WriteLine($"Session state: {JsonConvert.SerializeObject(sessionState, Formatting.Indented)}");

// var transferPolcies = await smartEoa.GetTransferPoliciesForSigner(chain, signerAddress);
// Console.WriteLine($"Transfer policies: {JsonConvert.SerializeObject(transferPolcies, Formatting.Indented)}");

// var callPolicies = await smartEoa.GetCallPoliciesForSigner(chain, signerAddress);
// Console.WriteLine($"Call policies: {JsonConvert.SerializeObject(callPolicies, Formatting.Indented)}");

#endregion

#region Smart Ecosystem Wallet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,19 @@ public string GenerateExternalLoginLink(string redirectUrl)
return $"{redirectUrl}{queryString}";
}

/// <summary>
/// Creates a session key for the user wallet. This is only supported for EIP7702 and EIP7702Sponsored execution modes.
/// </summary>
/// <param name="chainId">The chain ID for the session key.</param>
/// <param name="signerAddress">The address of the signer for the session key.</param>
/// <param name="durationInSeconds">Duration in seconds for which the session key will be valid.</param>
/// <param name="grantFullPermissions">Whether to grant full permissions to the session key. If false, only the specified call and transfer policies will be applied.</param>
/// <param name="callPolicies">List of call policies to apply to the session key. If null, no call policies will be applied.</param>
/// <param name="transferPolicies">List of transfer policies to apply to the session key. If null, no transfer policies will be applied.</param>
/// <param name="uid">A unique identifier for the session key. If null, a new GUID will be generated.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the transaction receipt for the session key creation.</returns>
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty, or when the duration is less than or equal to zero.</exception>
public async Task<ThirdwebTransactionReceipt> CreateSessionKey(
BigInteger chainId,
string signerAddress,
Expand All @@ -460,10 +473,7 @@ public async Task<ThirdwebTransactionReceipt> CreateSessionKey(
byte[] uid = null
)
{
if (this.ExecutionMode is not ExecutionMode.EIP7702 and not ExecutionMode.EIP7702Sponsored)
{
throw new InvalidOperationException("CreateSessionKey is only supported for EIP7702 and EIP7702Sponsored execution modes.");
}
await this.Ensure7702(chainId, false);

if (string.IsNullOrEmpty(signerAddress))
{
Expand Down Expand Up @@ -492,6 +502,121 @@ public async Task<ThirdwebTransactionReceipt> CreateSessionKey(
return await this.ExecuteTransaction(new ThirdwebTransactionInput(chainId: chainId, to: userWalletAddress, value: 0, data: sessionKeyCallData));
}

/// <summary>
/// Checks if the signer has full permissions on the EIP7702 account.
/// </summary>
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
/// <param name="signerAddress">The address of the signer to check permissions for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the signer has full permissions.</returns>
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
public async Task<bool> SignerHasFullPermissions(BigInteger chainId, string signerAddress)
{
await this.Ensure7702(chainId, true);

if (string.IsNullOrEmpty(signerAddress))
{
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
}

var userWalletAddress = await this.GetAddress();
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
var isWildcard = await userContract.Read<bool>("isWildcardSigner", signerAddress);
return isWildcard;
}

/// <summary>
/// Gets the call policies for a specific signer on the EIP7702 account.
/// </summary>
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
/// <param name="signerAddress">The address of the signer to get call policies for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of call policies for the signer.</returns>
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
public async Task<List<CallSpec>> GetCallPoliciesForSigner(BigInteger chainId, string signerAddress)
{
await this.Ensure7702(chainId, true);

if (string.IsNullOrEmpty(signerAddress))
{
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
}

var userWalletAddress = await this.GetAddress();
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
var callPolicies = await userContract.Read<List<CallSpec>>("getCallPoliciesForSigner", signerAddress);
return callPolicies;
}

/// <summary>
/// Gets the transfer policies for a specific signer on the EIP7702 account.
/// </summary>
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
/// <param name="signerAddress">The address of the signer to get transfer policies for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of transfer policies for the signer.</returns>
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
public async Task<List<TransferSpec>> GetTransferPoliciesForSigner(BigInteger chainId, string signerAddress)
{
await this.Ensure7702(chainId, true);

if (string.IsNullOrEmpty(signerAddress))
{
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
}

var userWalletAddress = await this.GetAddress();
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
var transferPolicies = await userContract.Read<List<TransferSpec>>("getTransferPoliciesForSigner", signerAddress);
return transferPolicies;
}

/// <summary>
/// Gets the session expiration timestamp for a specific signer on the EIP7702 account.
/// </summary>
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
/// <param name="signerAddress">The address of the signer to get session expiration for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the session expiration timestamp.</returns>
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
public async Task<BigInteger> GetSessionExpirationForSigner(BigInteger chainId, string signerAddress)
{
await this.Ensure7702(chainId, true);

if (string.IsNullOrEmpty(signerAddress))
{
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
}

var userWalletAddress = await this.GetAddress();
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
var expirationTimestamp = await userContract.Read<BigInteger>("getSessionExpirationForSigner", signerAddress);
return expirationTimestamp;
}

/// <summary>
/// Gets the complete session state for a specific signer on the EIP7702 account, including remaining limits and usage information.
/// </summary>
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
/// <param name="signerAddress">The address of the signer to get session state for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the session state with transfer value limits, call value limits, and call parameter limits.</returns>
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
public async Task<SessionState> GetSessionStateForSigner(BigInteger chainId, string signerAddress)
{
await this.Ensure7702(chainId, true);

if (string.IsNullOrEmpty(signerAddress))
{
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
}

var userWalletAddress = await this.GetAddress();
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
var sessionState = await userContract.Read<SessionState>("getSessionStateForSigner", signerAddress);
return sessionState;
}

#endregion

#region Account Linking
Expand Down Expand Up @@ -1343,4 +1468,20 @@ public Task SwitchNetwork(BigInteger chainId)
}

#endregion

private async Task Ensure7702(BigInteger chainId, bool ensureDelegated)
{
if (this.ExecutionMode is not ExecutionMode.EIP7702 and not ExecutionMode.EIP7702Sponsored)
{
throw new InvalidOperationException("This operation is only supported for EIP7702 and EIP7702Sponsored execution modes.");
}

if (!await Utils.IsDelegatedAccount(this.Client, chainId, this.Address).ConfigureAwait(false))
{
if (ensureDelegated)
{
throw new InvalidOperationException("This operation requires a delegated account. Please ensure you have transacted at least once with the account to set up delegation.");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -647,4 +647,40 @@ public object EncodeForHttp()
}
}

[Struct("LimitState")]
public class LimitState
{
[Parameter("uint256", "remaining", 1)]
[JsonProperty("remaining")]
public virtual BigInteger Remaining { get; set; }

[Parameter("address", "target", 2)]
[JsonProperty("target")]
public virtual string Target { get; set; }

[Parameter("bytes4", "selector", 3)]
[JsonProperty("selector")]
public virtual byte[] Selector { get; set; }

[Parameter("uint256", "index", 4)]
[JsonProperty("index")]
public virtual BigInteger Index { get; set; }
}

[Struct("SessionState")]
public class SessionState
{
[Parameter("tuple[]", "transferValue", 1, structTypeName: "LimitState[]")]
[JsonProperty("transferValue")]
public virtual List<LimitState> TransferValue { get; set; }

[Parameter("tuple[]", "callValue", 2, structTypeName: "LimitState[]")]
[JsonProperty("callValue")]
public virtual List<LimitState> CallValue { get; set; }

[Parameter("tuple[]", "callParams", 3, structTypeName: "LimitState[]")]
[JsonProperty("callParams")]
public virtual List<LimitState> CallParams { get; set; }
}

#endregion