Skip to content

Commit 64405d5

Browse files
✨ Add SessionWallet (#142)
* Add SessionWallet via Solana.Unity.Gum * Add missing imports * Add SessionWallet via Solana.Unity.Gum * Add convenience methods * Add XML comments * Check if session token is initialized * ⚡ Enhance NFT loading (#138) * ✨ Enhance NFT loading (#141) * ✨ Enhance NFT loading * ✨ Reduce min delay --------- Co-authored-by: abishekk92 <[email protected]>
1 parent e65f2b6 commit 64405d5

File tree

7 files changed

+343
-45
lines changed

7 files changed

+343
-45
lines changed

Packages/Solana.Unity.Gum.dll

17 KB
Binary file not shown.

Packages/Solana.Unity.Gum.dll.meta

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/codebase/InGameWallet.cs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public class InGameWallet : WalletBase
1717
{
1818
private const string EncryptedKeystoreKey = "EncryptedKeystore";
1919

20-
public InGameWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
21-
string customRpcUri = null, string customStreamingRpcUri = null,
20+
public InGameWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
21+
string customRpcUri = null, string customStreamingRpcUri = null,
2222
bool autoConnectOnStartup = false) : base(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup)
2323
{
2424
}
@@ -71,7 +71,7 @@ protected override Task<Account> _CreateAccount(string secret = null, string pas
7171
secret = mnem.ToString();
7272
}
7373
if(account == null) return Task.FromResult<Account>(null);
74-
74+
7575
password ??= "";
7676

7777
var keystoreService = new KeyStorePbkdf2Service();
@@ -100,7 +100,7 @@ public override Task<byte[]> SignMessage(byte[] message)
100100
{
101101
return Task.FromResult(Account.Sign(message));
102102
}
103-
103+
104104
/// <summary>
105105
/// Returns an instance of Keypair from a mnemonic, byte array or secret key
106106
/// </summary>
@@ -124,7 +124,7 @@ public static Account FromSecret(string secret)
124124

125125
return account;
126126
}
127-
127+
128128
/// <summary>
129129
/// Returns an instance of Keypair from a mnemonic
130130
/// </summary>
@@ -135,7 +135,7 @@ private static Account FromMnemonic(string mnemonic)
135135
var wallet = new Wallet.Wallet(new Mnemonic(mnemonic));
136136
return wallet.Account;
137137
}
138-
138+
139139
/// <summary>
140140
/// Returns an instance of Keypair from a secret key
141141
/// </summary>
@@ -146,14 +146,14 @@ private static Account FromSecretKey(string secretKey)
146146
try
147147
{
148148
var wallet = new Wallet.Wallet(new PrivateKey(secretKey).KeyBytes, "", SeedMode.Bip39);
149-
return wallet.Account;
149+
return wallet.Account;
150150
}catch (ArgumentException)
151151
{
152152
return null;
153153
}
154154

155155
}
156-
156+
157157
/// <summary>
158158
/// Returns an instance of Keypair from a Byte Array
159159
/// </summary>
@@ -164,13 +164,13 @@ private static Account FromByteArray(byte[] secretByteArray)
164164
var wallet = new Wallet.Wallet(secretByteArray, "", SeedMode.Bip39);
165165
return wallet.Account;
166166
}
167-
167+
168168
/// <summary>
169-
/// Takes a string as input and checks if it is a valid mnemonic
169+
/// Takes a string as input and checks if it is a valid mnemonic
170170
/// </summary>
171171
/// <param name="secret"></param>
172172
/// <returns></returns>
173-
private static bool IsMnemonic(string secret)
173+
protected static bool IsMnemonic(string secret)
174174
{
175175
return secret.Split(' ').Length is 12 or 24;
176176
}
@@ -183,7 +183,7 @@ private static bool IsByteArray(string secret)
183183
{
184184
return secret.StartsWith('[') && secret.EndsWith(']');
185185
}
186-
186+
187187
/// <summary>
188188
/// Takes a string as input and tries to parse it into a Keypair
189189
/// </summary>
@@ -199,14 +199,14 @@ private static Account ParseByteArray(string secret)
199199

200200
return FromByteArray(parsed);
201201
}
202-
203202

204-
private static string LoadPlayerPrefs(string key)
203+
204+
protected static string LoadPlayerPrefs(string key)
205205
{
206206
return PlayerPrefs.GetString(key);
207207
}
208208

209-
private static void SavePlayerPrefs(string key, string value)
209+
protected static void SavePlayerPrefs(string key, string value)
210210
{
211211
PlayerPrefs.SetString(key, value);
212212
#if UNITY_WEBGL

Runtime/codebase/SessionWallet.cs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Solana.Unity.KeyStore.Exceptions;
7+
using Solana.Unity.KeyStore.Services;
8+
using Solana.Unity.Rpc.Models;
9+
using Solana.Unity.Wallet;
10+
using Solana.Unity.Programs;
11+
using Solana.Unity.Wallet.Bip39;
12+
using Solana.Unity.Gum.GplSession;
13+
using Solana.Unity.Gum.GplSession.Accounts;
14+
using Solana.Unity.Gum.GplSession.Program;
15+
using UnityEngine;
16+
17+
// ReSharper disable once CheckNamespace
18+
19+
namespace Solana.Unity.SDK
20+
{
21+
public class SessionWallet : InGameWallet
22+
{
23+
private const string EncryptedKeystoreKey = "SessionKeystore";
24+
25+
public PublicKey TargetProgram { get; protected set; }
26+
public PublicKey SessionTokenPDA { get; protected set; }
27+
28+
public SessionWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
29+
string customRpcUri = null, string customStreamingRpcUri = null,
30+
bool autoConnectOnStartup = false) : base(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup)
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Checks if a session wallet exists by checking if the encrypted keystore key is present in the player preferences.
36+
/// </summary>
37+
/// <returns>True if a session wallet exists, false otherwise.</returns>
38+
public static bool HasSessionWallet()
39+
{
40+
var prefs = LoadPlayerPrefs(EncryptedKeystoreKey);
41+
return !string.IsNullOrEmpty(prefs);
42+
}
43+
44+
/// <summary>
45+
/// Derives the public key of the session token account for the current session wallet.
46+
/// </summary>
47+
/// <returns>The public key of the session token account.</returns>
48+
private static PublicKey FindSessionToken(PublicKey TargetProgram, Account Account, Account Authority)
49+
{
50+
return SessionToken.DeriveSessionTokenAccount(
51+
authority: Authority.PublicKey,
52+
targetProgram: TargetProgram,
53+
sessionSigner: Account.PublicKey
54+
);
55+
}
56+
57+
/// <summary>
58+
/// Creates a new SessionWallet instance and logs in with the provided password if a session wallet exists, otherwise creates a new account and logs in.
59+
/// </summary>
60+
/// <param name="targetProgram">The target program to interact with.</param>
61+
/// <param name="password">The password to decrypt the session keystore.</param>
62+
/// <param name="rpcCluster">The Solana RPC cluster to connect to.</param>
63+
/// <param name="customRpcUri">A custom URI to connect to the Solana RPC cluster.</param>
64+
/// <param name="customStreamingRpcUri">A custom URI to connect to the Solana streaming RPC cluster.</param>
65+
/// <param name="autoConnectOnStartup">Whether to automatically connect to the Solana RPC cluster on startup.</param>
66+
/// <returns>A SessionWallet instance.</returns>
67+
public static async Task<SessionWallet> GetSessionWallet(PublicKey targetProgram, string password, RpcCluster rpcCluster = RpcCluster.DevNet,
68+
string customRpcUri = null, string customStreamingRpcUri = null,
69+
bool autoConnectOnStartup = false)
70+
{
71+
Debug.Log("Found Session Wallet");
72+
SessionWallet sessionWallet = new SessionWallet(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup);
73+
sessionWallet.TargetProgram = targetProgram;
74+
if (HasSessionWallet())
75+
{
76+
sessionWallet.Account = await sessionWallet.Login(password);
77+
sessionWallet.SessionTokenPDA = FindSessionToken(targetProgram, sessionWallet.Account, Web3.Account);
78+
79+
// If it is not uninitialized, return the session wallet
80+
if(!(await sessionWallet.IsSessionTokenInitialized())) {
81+
Debug.Log("Session Token is not initialized");
82+
return sessionWallet;
83+
}
84+
85+
// Otherwise check for a valid session token
86+
if ((await sessionWallet.IsSessionTokenValid())) {
87+
Debug.Log("Session Token is valid");
88+
return sessionWallet;
89+
}
90+
else
91+
{
92+
Debug.Log("Session Token is invalid");
93+
sessionWallet.Logout();
94+
return await GetSessionWallet(targetProgram, password, rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup);
95+
}
96+
}
97+
sessionWallet.Account = await sessionWallet.CreateAccount(password:password);
98+
sessionWallet.SessionTokenPDA = FindSessionToken(targetProgram, sessionWallet.Account, Web3.Account);
99+
return sessionWallet;
100+
}
101+
102+
/// <inheritdoc />
103+
protected override Task<Account> _Login(string password = "")
104+
{
105+
var keystoreService = new KeyStorePbkdf2Service();
106+
var encryptedKeystoreJson = LoadPlayerPrefs(EncryptedKeystoreKey);
107+
byte[] decryptedKeystore;
108+
try
109+
{
110+
if (string.IsNullOrEmpty(encryptedKeystoreJson))
111+
return Task.FromResult<Account>(null);
112+
decryptedKeystore = keystoreService.DecryptKeyStoreFromJson(password, encryptedKeystoreJson);
113+
}
114+
catch (DecryptionException e)
115+
{
116+
Debug.LogException(e);
117+
return Task.FromResult<Account>(null);
118+
}
119+
120+
var secret = Encoding.UTF8.GetString(decryptedKeystore);
121+
var account = FromSecret(secret);
122+
if (IsMnemonic(secret))
123+
{
124+
var restoredMnemonic = new Mnemonic(secret);
125+
Mnemonic = restoredMnemonic;
126+
}
127+
return Task.FromResult(account);
128+
}
129+
130+
/// <inheritdoc />
131+
public override async void Logout()
132+
{
133+
// Revoke Session
134+
var tx = new Transaction()
135+
{
136+
FeePayer = Account,
137+
Instructions = new List<TransactionInstruction>(),
138+
RecentBlockHash = await Web3.BlockHash()
139+
};
140+
141+
// Get balance and calculate refund
142+
var balance = await GetBalance(Account.PublicKey);
143+
var estimatedFees = await ActiveRpcClient.GetFeeCalculatorForBlockhashAsync(tx.RecentBlockHash);
144+
//var refund = balance - (estimatedFees.LamportsPerSignature * 2);
145+
var refund = balance - 1000000;
146+
147+
tx.Add(RevokeSessionIX());
148+
// Issue Refund
149+
tx.Add(SystemProgram.Transfer(Account.PublicKey, Web3.Account.PublicKey, (ulong)refund));
150+
await SignAndSendTransaction(tx);
151+
// Purge Keystore
152+
PlayerPrefs.DeleteKey(EncryptedKeystoreKey);
153+
base.Logout();
154+
}
155+
156+
/// <inheritdoc />
157+
protected override Task<Account> _CreateAccount(string secret = null, string password = null)
158+
{
159+
Account account;
160+
Mnemonic mnem = null;
161+
if (secret != null)
162+
{
163+
account = FromSecret(secret);
164+
if (IsMnemonic(secret))
165+
{
166+
mnem = new Mnemonic(secret);
167+
}
168+
}
169+
else
170+
{
171+
mnem = new Mnemonic(WordList.English, WordCount.Twelve);
172+
var wallet = new Wallet.Wallet(mnem);
173+
account = wallet.Account;
174+
secret = mnem.ToString();
175+
}
176+
if (account == null) return Task.FromResult<Account>(null);
177+
178+
password ??= "";
179+
180+
var keystoreService = new KeyStorePbkdf2Service();
181+
var stringByteArray = Encoding.UTF8.GetBytes(secret);
182+
var encryptedKeystoreJson = keystoreService.EncryptAndGenerateKeyStoreAsJson(
183+
password, stringByteArray, account.PublicKey.Key);
184+
185+
SavePlayerPrefs(EncryptedKeystoreKey, encryptedKeystoreJson);
186+
Mnemonic = mnem;
187+
return Task.FromResult(account);
188+
}
189+
190+
/// <summary>
191+
/// Creates a transaction instruction to create a new session token account and initialize it with the provided session signer and target program.
192+
/// </summary>
193+
/// <param name="topUp">Whether to top up the session token account with SOL.</param>
194+
/// <param name="sessionValidity">The validity period of the session token account, in seconds.</param>
195+
/// <returns>A transaction instruction to create a new session token account.</returns>
196+
public TransactionInstruction CreateSessionIX(bool topUp, long sessionValidity)
197+
{
198+
CreateSessionAccounts createSessionAccounts = new CreateSessionAccounts()
199+
{
200+
SessionToken = SessionTokenPDA,
201+
SessionSigner = Account.PublicKey,
202+
Authority = Web3.Account,
203+
TargetProgram = TargetProgram,
204+
SystemProgram = SystemProgram.ProgramIdKey,
205+
};
206+
207+
return GplSessionProgram.CreateSession(
208+
createSessionAccounts,
209+
topUp: topUp,
210+
validUntil: sessionValidity
211+
);
212+
}
213+
214+
/// <summary>
215+
/// Creates a transaction instruction to revoke the current session token account.
216+
/// </summary>
217+
/// <returns>A transaction instruction to revoke the current session token account.</returns>
218+
public TransactionInstruction RevokeSessionIX()
219+
{
220+
RevokeSessionAccounts revokeSessionAccounts = new RevokeSessionAccounts()
221+
{
222+
SessionToken = SessionTokenPDA,
223+
Authority = Account,
224+
SystemProgram = SystemProgram.ProgramIdKey,
225+
};
226+
227+
return GplSessionProgram.RevokeSession(
228+
revokeSessionAccounts
229+
);
230+
}
231+
232+
/// <summary>
233+
/// Checks if the session token account has been initialized by checking if the account data is present on the blockchain.
234+
/// </summary>
235+
/// <returns>True if the session token account has been initialized, false otherwise.</returns>
236+
public async Task<bool> IsSessionTokenInitialized()
237+
{
238+
var sessionTokenData = await ActiveRpcClient.GetAccountInfoAsync(SessionTokenPDA);
239+
return sessionTokenData.Result.Value != null;
240+
}
241+
242+
/// <summary>
243+
/// Checks if the session token is still valid by verifying if the session token account exists on the blockchain and if its validity period has not expired.
244+
/// </summary>
245+
/// <returns>True if the session token is still valid, false otherwise.</returns>
246+
public async Task<bool> IsSessionTokenValid()
247+
{
248+
var sessionTokenData = (await ActiveRpcClient.GetAccountInfoAsync(SessionTokenPDA)).Result.Value.Data[0];
249+
if (sessionTokenData == null) return false;
250+
return SessionToken.Deserialize(Convert.FromBase64String(sessionTokenData)).ValidUntil > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
251+
}
252+
253+
}
254+
}

0 commit comments

Comments
 (0)