Skip to content

Commit daf4ca2

Browse files
authored
EIP-7702 Session Key Utils (#151)
1 parent 4666962 commit daf4ca2

File tree

3 files changed

+223
-6
lines changed

3 files changed

+223
-6
lines changed

Thirdweb.Console/Program.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,19 +372,59 @@
372372
// Console.WriteLine($"User Wallet address: {await smartEoa.GetAddress()}");
373373

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

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

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

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

389+
// // Validate session key config
390+
// var hasFullPermissions = await smartEoa.SignerHasFullPermissions(chain, signerAddress);
391+
// Console.WriteLine($"Signer has full permissions: {hasFullPermissions}");
392+
393+
// var sessionExpiration = await smartEoa.GetSessionExpirationForSigner(chain, signerAddress);
394+
// Console.WriteLine($"Session expires in {sessionExpiration - Utils.GetUnixTimeStampNow()} seconds");
395+
396+
// // Create a session key with granular permissions
397+
// var granularSessionKeyReceipt = await smartEoa.CreateSessionKey(
398+
// chainId: chain,
399+
// signerAddress: signerAddress,
400+
// durationInSeconds: 86400,
401+
// grantFullPermissions: false,
402+
// transferPolicies: new List<TransferSpec>
403+
// {
404+
// new()
405+
// {
406+
// Target = signerAddress,
407+
// MaxValuePerUse = BigInteger.Parse("0.001".ToWei()),
408+
// ValueLimit = new UsageLimit
409+
// {
410+
// LimitType = 1, // Lifetime
411+
// Limit = BigInteger.Parse("0.01".ToWei()),
412+
// Period = 86400, // 1 day
413+
// }
414+
// }
415+
// }
416+
// );
417+
418+
// // Validate session key config
419+
// var sessionState = await smartEoa.GetSessionStateForSigner(chain, signerAddress);
420+
// Console.WriteLine($"Session state: {JsonConvert.SerializeObject(sessionState, Formatting.Indented)}");
421+
422+
// var transferPolcies = await smartEoa.GetTransferPoliciesForSigner(chain, signerAddress);
423+
// Console.WriteLine($"Transfer policies: {JsonConvert.SerializeObject(transferPolcies, Formatting.Indented)}");
424+
425+
// var callPolicies = await smartEoa.GetCallPoliciesForSigner(chain, signerAddress);
426+
// Console.WriteLine($"Call policies: {JsonConvert.SerializeObject(callPolicies, Formatting.Indented)}");
427+
388428
#endregion
389429

390430
#region Smart Ecosystem Wallet

Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,19 @@ public string GenerateExternalLoginLink(string redirectUrl)
450450
return $"{redirectUrl}{queryString}";
451451
}
452452

453+
/// <summary>
454+
/// Creates a session key for the user wallet. This is only supported for EIP7702 and EIP7702Sponsored execution modes.
455+
/// </summary>
456+
/// <param name="chainId">The chain ID for the session key.</param>
457+
/// <param name="signerAddress">The address of the signer for the session key.</param>
458+
/// <param name="durationInSeconds">Duration in seconds for which the session key will be valid.</param>
459+
/// <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>
460+
/// <param name="callPolicies">List of call policies to apply to the session key. If null, no call policies will be applied.</param>
461+
/// <param name="transferPolicies">List of transfer policies to apply to the session key. If null, no transfer policies will be applied.</param>
462+
/// <param name="uid">A unique identifier for the session key. If null, a new GUID will be generated.</param>
463+
/// <returns>A task that represents the asynchronous operation. The task result contains the transaction receipt for the session key creation.</returns>
464+
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
465+
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty, or when the duration is less than or equal to zero.</exception>
453466
public async Task<ThirdwebTransactionReceipt> CreateSessionKey(
454467
BigInteger chainId,
455468
string signerAddress,
@@ -460,10 +473,7 @@ public async Task<ThirdwebTransactionReceipt> CreateSessionKey(
460473
byte[] uid = null
461474
)
462475
{
463-
if (this.ExecutionMode is not ExecutionMode.EIP7702 and not ExecutionMode.EIP7702Sponsored)
464-
{
465-
throw new InvalidOperationException("CreateSessionKey is only supported for EIP7702 and EIP7702Sponsored execution modes.");
466-
}
476+
await this.Ensure7702(chainId, false);
467477

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

505+
/// <summary>
506+
/// Checks if the signer has full permissions on the EIP7702 account.
507+
/// </summary>
508+
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
509+
/// <param name="signerAddress">The address of the signer to check permissions for.</param>
510+
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the signer has full permissions.</returns>
511+
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
512+
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
513+
public async Task<bool> SignerHasFullPermissions(BigInteger chainId, string signerAddress)
514+
{
515+
await this.Ensure7702(chainId, true);
516+
517+
if (string.IsNullOrEmpty(signerAddress))
518+
{
519+
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
520+
}
521+
522+
var userWalletAddress = await this.GetAddress();
523+
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
524+
var isWildcard = await userContract.Read<bool>("isWildcardSigner", signerAddress);
525+
return isWildcard;
526+
}
527+
528+
/// <summary>
529+
/// Gets the call policies for a specific signer on the EIP7702 account.
530+
/// </summary>
531+
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
532+
/// <param name="signerAddress">The address of the signer to get call policies for.</param>
533+
/// <returns>A task that represents the asynchronous operation. The task result contains a list of call policies for the signer.</returns>
534+
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
535+
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
536+
public async Task<List<CallSpec>> GetCallPoliciesForSigner(BigInteger chainId, string signerAddress)
537+
{
538+
await this.Ensure7702(chainId, true);
539+
540+
if (string.IsNullOrEmpty(signerAddress))
541+
{
542+
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
543+
}
544+
545+
var userWalletAddress = await this.GetAddress();
546+
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
547+
var callPolicies = await userContract.Read<List<CallSpec>>("getCallPoliciesForSigner", signerAddress);
548+
return callPolicies;
549+
}
550+
551+
/// <summary>
552+
/// Gets the transfer policies for a specific signer on the EIP7702 account.
553+
/// </summary>
554+
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
555+
/// <param name="signerAddress">The address of the signer to get transfer policies for.</param>
556+
/// <returns>A task that represents the asynchronous operation. The task result contains a list of transfer policies for the signer.</returns>
557+
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
558+
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
559+
public async Task<List<TransferSpec>> GetTransferPoliciesForSigner(BigInteger chainId, string signerAddress)
560+
{
561+
await this.Ensure7702(chainId, true);
562+
563+
if (string.IsNullOrEmpty(signerAddress))
564+
{
565+
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
566+
}
567+
568+
var userWalletAddress = await this.GetAddress();
569+
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
570+
var transferPolicies = await userContract.Read<List<TransferSpec>>("getTransferPoliciesForSigner", signerAddress);
571+
return transferPolicies;
572+
}
573+
574+
/// <summary>
575+
/// Gets the session expiration timestamp for a specific signer on the EIP7702 account.
576+
/// </summary>
577+
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
578+
/// <param name="signerAddress">The address of the signer to get session expiration for.</param>
579+
/// <returns>A task that represents the asynchronous operation. The task result contains the session expiration timestamp.</returns>
580+
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
581+
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
582+
public async Task<BigInteger> GetSessionExpirationForSigner(BigInteger chainId, string signerAddress)
583+
{
584+
await this.Ensure7702(chainId, true);
585+
586+
if (string.IsNullOrEmpty(signerAddress))
587+
{
588+
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
589+
}
590+
591+
var userWalletAddress = await this.GetAddress();
592+
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
593+
var expirationTimestamp = await userContract.Read<BigInteger>("getSessionExpirationForSigner", signerAddress);
594+
return expirationTimestamp;
595+
}
596+
597+
/// <summary>
598+
/// Gets the complete session state for a specific signer on the EIP7702 account, including remaining limits and usage information.
599+
/// </summary>
600+
/// <param name="chainId">The chain ID of the EIP7702 account.</param>
601+
/// <param name="signerAddress">The address of the signer to get session state for.</param>
602+
/// <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>
603+
/// <exception cref="InvalidOperationException">Thrown when the execution mode is not EIP7702 or EIP7702Sponsored.</exception>
604+
/// <exception cref="ArgumentException">Thrown when the signer address is null or empty.</exception>
605+
public async Task<SessionState> GetSessionStateForSigner(BigInteger chainId, string signerAddress)
606+
{
607+
await this.Ensure7702(chainId, true);
608+
609+
if (string.IsNullOrEmpty(signerAddress))
610+
{
611+
throw new ArgumentException("Signer address cannot be null or empty.", nameof(signerAddress));
612+
}
613+
614+
var userWalletAddress = await this.GetAddress();
615+
var userContract = await ThirdwebContract.Create(this.Client, userWalletAddress, chainId, Constants.MINIMAL_ACCOUNT_7702_ABI);
616+
var sessionState = await userContract.Read<SessionState>("getSessionStateForSigner", signerAddress);
617+
return sessionState;
618+
}
619+
495620
#endregion
496621

497622
#region Account Linking
@@ -1343,4 +1468,20 @@ public Task SwitchNetwork(BigInteger chainId)
13431468
}
13441469

13451470
#endregion
1471+
1472+
private async Task Ensure7702(BigInteger chainId, bool ensureDelegated)
1473+
{
1474+
if (this.ExecutionMode is not ExecutionMode.EIP7702 and not ExecutionMode.EIP7702Sponsored)
1475+
{
1476+
throw new InvalidOperationException("This operation is only supported for EIP7702 and EIP7702Sponsored execution modes.");
1477+
}
1478+
1479+
if (!await Utils.IsDelegatedAccount(this.Client, chainId, this.Address).ConfigureAwait(false))
1480+
{
1481+
if (ensureDelegated)
1482+
{
1483+
throw new InvalidOperationException("This operation requires a delegated account. Please ensure you have transacted at least once with the account to set up delegation.");
1484+
}
1485+
}
1486+
}
13461487
}

Thirdweb/Thirdweb.Wallets/SmartWallet/Thirdweb.AccountAbstraction/AATypes.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,4 +647,40 @@ public object EncodeForHttp()
647647
}
648648
}
649649

650+
[Struct("LimitState")]
651+
public class LimitState
652+
{
653+
[Parameter("uint256", "remaining", 1)]
654+
[JsonProperty("remaining")]
655+
public virtual BigInteger Remaining { get; set; }
656+
657+
[Parameter("address", "target", 2)]
658+
[JsonProperty("target")]
659+
public virtual string Target { get; set; }
660+
661+
[Parameter("bytes4", "selector", 3)]
662+
[JsonProperty("selector")]
663+
public virtual byte[] Selector { get; set; }
664+
665+
[Parameter("uint256", "index", 4)]
666+
[JsonProperty("index")]
667+
public virtual BigInteger Index { get; set; }
668+
}
669+
670+
[Struct("SessionState")]
671+
public class SessionState
672+
{
673+
[Parameter("tuple[]", "transferValue", 1, structTypeName: "LimitState[]")]
674+
[JsonProperty("transferValue")]
675+
public virtual List<LimitState> TransferValue { get; set; }
676+
677+
[Parameter("tuple[]", "callValue", 2, structTypeName: "LimitState[]")]
678+
[JsonProperty("callValue")]
679+
public virtual List<LimitState> CallValue { get; set; }
680+
681+
[Parameter("tuple[]", "callParams", 3, structTypeName: "LimitState[]")]
682+
[JsonProperty("callParams")]
683+
public virtual List<LimitState> CallParams { get; set; }
684+
}
685+
650686
#endregion

0 commit comments

Comments
 (0)