Skip to content
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
Binary file added Packages/Solana.Unity.Gum.dll
Binary file not shown.
33 changes: 33 additions & 0 deletions Packages/Solana.Unity.Gum.dll.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 15 additions & 15 deletions Runtime/codebase/InGameWallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public class InGameWallet : WalletBase
{
private const string EncryptedKeystoreKey = "EncryptedKeystore";

public InGameWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
string customRpcUri = null, string customStreamingRpcUri = null,
public InGameWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
string customRpcUri = null, string customStreamingRpcUri = null,
bool autoConnectOnStartup = false) : base(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup)
{
}
Expand Down Expand Up @@ -71,7 +71,7 @@ protected override Task<Account> _CreateAccount(string secret = null, string pas
secret = mnem.ToString();
}
if(account == null) return Task.FromResult<Account>(null);

password ??= "";

var keystoreService = new KeyStorePbkdf2Service();
Expand Down Expand Up @@ -100,7 +100,7 @@ public override Task<byte[]> SignMessage(byte[] message)
{
return Task.FromResult(Account.Sign(message));
}

/// <summary>
/// Returns an instance of Keypair from a mnemonic, byte array or secret key
/// </summary>
Expand All @@ -124,7 +124,7 @@ public static Account FromSecret(string secret)

return account;
}

/// <summary>
/// Returns an instance of Keypair from a mnemonic
/// </summary>
Expand All @@ -135,7 +135,7 @@ private static Account FromMnemonic(string mnemonic)
var wallet = new Wallet.Wallet(new Mnemonic(mnemonic));
return wallet.Account;
}

/// <summary>
/// Returns an instance of Keypair from a secret key
/// </summary>
Expand All @@ -146,14 +146,14 @@ private static Account FromSecretKey(string secretKey)
try
{
var wallet = new Wallet.Wallet(new PrivateKey(secretKey).KeyBytes, "", SeedMode.Bip39);
return wallet.Account;
return wallet.Account;
}catch (ArgumentException)
{
return null;
}

}

/// <summary>
/// Returns an instance of Keypair from a Byte Array
/// </summary>
Expand All @@ -164,13 +164,13 @@ private static Account FromByteArray(byte[] secretByteArray)
var wallet = new Wallet.Wallet(secretByteArray, "", SeedMode.Bip39);
return wallet.Account;
}

/// <summary>
/// Takes a string as input and checks if it is a valid mnemonic
/// Takes a string as input and checks if it is a valid mnemonic
/// </summary>
/// <param name="secret"></param>
/// <returns></returns>
private static bool IsMnemonic(string secret)
protected static bool IsMnemonic(string secret)
{
return secret.Split(' ').Length is 12 or 24;
}
Expand All @@ -183,7 +183,7 @@ private static bool IsByteArray(string secret)
{
return secret.StartsWith('[') && secret.EndsWith(']');
}

/// <summary>
/// Takes a string as input and tries to parse it into a Keypair
/// </summary>
Expand All @@ -199,14 +199,14 @@ private static Account ParseByteArray(string secret)

return FromByteArray(parsed);
}


private static string LoadPlayerPrefs(string key)

protected static string LoadPlayerPrefs(string key)
{
return PlayerPrefs.GetString(key);
}

private static void SavePlayerPrefs(string key, string value)
protected static void SavePlayerPrefs(string key, string value)
{
PlayerPrefs.SetString(key, value);
#if UNITY_WEBGL
Expand Down
254 changes: 254 additions & 0 deletions Runtime/codebase/SessionWallet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Solana.Unity.KeyStore.Exceptions;
using Solana.Unity.KeyStore.Services;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Wallet;
using Solana.Unity.Programs;
using Solana.Unity.Wallet.Bip39;
using Solana.Unity.Gum.GplSession;
using Solana.Unity.Gum.GplSession.Accounts;
using Solana.Unity.Gum.GplSession.Program;
using UnityEngine;

// ReSharper disable once CheckNamespace

namespace Solana.Unity.SDK
{
public class SessionWallet : InGameWallet
{
private const string EncryptedKeystoreKey = "SessionKeystore";

public PublicKey TargetProgram { get; protected set; }
public PublicKey SessionTokenPDA { get; protected set; }

public SessionWallet(RpcCluster rpcCluster = RpcCluster.DevNet,
string customRpcUri = null, string customStreamingRpcUri = null,
bool autoConnectOnStartup = false) : base(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup)
{
}

/// <summary>
/// Checks if a session wallet exists by checking if the encrypted keystore key is present in the player preferences.
/// </summary>
/// <returns>True if a session wallet exists, false otherwise.</returns>
public static bool HasSessionWallet()
{
var prefs = LoadPlayerPrefs(EncryptedKeystoreKey);
return !string.IsNullOrEmpty(prefs);
}

/// <summary>
/// Derives the public key of the session token account for the current session wallet.
/// </summary>
/// <returns>The public key of the session token account.</returns>
private static PublicKey FindSessionToken(PublicKey TargetProgram, Account Account, Account Authority)
{
return SessionToken.DeriveSessionTokenAccount(
authority: Authority.PublicKey,
targetProgram: TargetProgram,
sessionSigner: Account.PublicKey
);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="targetProgram">The target program to interact with.</param>
/// <param name="password">The password to decrypt the session keystore.</param>
/// <param name="rpcCluster">The Solana RPC cluster to connect to.</param>
/// <param name="customRpcUri">A custom URI to connect to the Solana RPC cluster.</param>
/// <param name="customStreamingRpcUri">A custom URI to connect to the Solana streaming RPC cluster.</param>
/// <param name="autoConnectOnStartup">Whether to automatically connect to the Solana RPC cluster on startup.</param>
/// <returns>A SessionWallet instance.</returns>
public static async Task<SessionWallet> GetSessionWallet(PublicKey targetProgram, string password, RpcCluster rpcCluster = RpcCluster.DevNet,
string customRpcUri = null, string customStreamingRpcUri = null,
bool autoConnectOnStartup = false)
{
Debug.Log("Found Session Wallet");
SessionWallet sessionWallet = new SessionWallet(rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup);
sessionWallet.TargetProgram = targetProgram;
if (HasSessionWallet())
{
sessionWallet.Account = await sessionWallet.Login(password);
sessionWallet.SessionTokenPDA = FindSessionToken(targetProgram, sessionWallet.Account, Web3.Account);

// If it is not uninitialized, return the session wallet
if(!(await sessionWallet.IsSessionTokenInitialized())) {
Debug.Log("Session Token is not initialized");
return sessionWallet;
}

// Otherwise check for a valid session token
if ((await sessionWallet.IsSessionTokenValid())) {
Debug.Log("Session Token is valid");
return sessionWallet;
}
else
{
Debug.Log("Session Token is invalid");
sessionWallet.Logout();
return await GetSessionWallet(targetProgram, password, rpcCluster, customRpcUri, customStreamingRpcUri, autoConnectOnStartup);
}
}
sessionWallet.Account = await sessionWallet.CreateAccount(password:password);
sessionWallet.SessionTokenPDA = FindSessionToken(targetProgram, sessionWallet.Account, Web3.Account);
return sessionWallet;
}

/// <inheritdoc />
protected override Task<Account> _Login(string password = "")
{
var keystoreService = new KeyStorePbkdf2Service();
var encryptedKeystoreJson = LoadPlayerPrefs(EncryptedKeystoreKey);
byte[] decryptedKeystore;
try
{
if (string.IsNullOrEmpty(encryptedKeystoreJson))
return Task.FromResult<Account>(null);
decryptedKeystore = keystoreService.DecryptKeyStoreFromJson(password, encryptedKeystoreJson);
}
catch (DecryptionException e)
{
Debug.LogException(e);
return Task.FromResult<Account>(null);
}

var secret = Encoding.UTF8.GetString(decryptedKeystore);
var account = FromSecret(secret);
if (IsMnemonic(secret))
{
var restoredMnemonic = new Mnemonic(secret);
Mnemonic = restoredMnemonic;
}
return Task.FromResult(account);
}

/// <inheritdoc />
public override async void Logout()
{
// Revoke Session
var tx = new Transaction()
{
FeePayer = Account,
Instructions = new List<TransactionInstruction>(),
RecentBlockHash = await Web3.BlockHash()
};

// Get balance and calculate refund
var balance = await GetBalance(Account.PublicKey);
var estimatedFees = await ActiveRpcClient.GetFeeCalculatorForBlockhashAsync(tx.RecentBlockHash);
//var refund = balance - (estimatedFees.LamportsPerSignature * 2);
var refund = balance - 1000000;

tx.Add(RevokeSessionIX());
// Issue Refund
tx.Add(SystemProgram.Transfer(Account.PublicKey, Web3.Account.PublicKey, (ulong)refund));
await SignAndSendTransaction(tx);
// Purge Keystore
PlayerPrefs.DeleteKey(EncryptedKeystoreKey);
base.Logout();
}

/// <inheritdoc />
protected override Task<Account> _CreateAccount(string secret = null, string password = null)
{
Account account;
Mnemonic mnem = null;
if (secret != null)
{
account = FromSecret(secret);
if (IsMnemonic(secret))
{
mnem = new Mnemonic(secret);
}
}
else
{
mnem = new Mnemonic(WordList.English, WordCount.Twelve);
var wallet = new Wallet.Wallet(mnem);
account = wallet.Account;
secret = mnem.ToString();
}
if (account == null) return Task.FromResult<Account>(null);

password ??= "";

var keystoreService = new KeyStorePbkdf2Service();
var stringByteArray = Encoding.UTF8.GetBytes(secret);
var encryptedKeystoreJson = keystoreService.EncryptAndGenerateKeyStoreAsJson(
password, stringByteArray, account.PublicKey.Key);

SavePlayerPrefs(EncryptedKeystoreKey, encryptedKeystoreJson);
Mnemonic = mnem;
return Task.FromResult(account);
}

/// <summary>
/// Creates a transaction instruction to create a new session token account and initialize it with the provided session signer and target program.
/// </summary>
/// <param name="topUp">Whether to top up the session token account with SOL.</param>
/// <param name="sessionValidity">The validity period of the session token account, in seconds.</param>
/// <returns>A transaction instruction to create a new session token account.</returns>
public TransactionInstruction CreateSessionIX(bool topUp, long sessionValidity)
{
CreateSessionAccounts createSessionAccounts = new CreateSessionAccounts()
{
SessionToken = SessionTokenPDA,
SessionSigner = Account.PublicKey,
Authority = Web3.Account,
TargetProgram = TargetProgram,
SystemProgram = SystemProgram.ProgramIdKey,
};

return GplSessionProgram.CreateSession(
createSessionAccounts,
topUp: topUp,
validUntil: sessionValidity
);
}

/// <summary>
/// Creates a transaction instruction to revoke the current session token account.
/// </summary>
/// <returns>A transaction instruction to revoke the current session token account.</returns>
public TransactionInstruction RevokeSessionIX()
{
RevokeSessionAccounts revokeSessionAccounts = new RevokeSessionAccounts()
{
SessionToken = SessionTokenPDA,
Authority = Account,
SystemProgram = SystemProgram.ProgramIdKey,
};

return GplSessionProgram.RevokeSession(
revokeSessionAccounts
);
}

/// <summary>
/// Checks if the session token account has been initialized by checking if the account data is present on the blockchain.
/// </summary>
/// <returns>True if the session token account has been initialized, false otherwise.</returns>
public async Task<bool> IsSessionTokenInitialized()
{
var sessionTokenData = await ActiveRpcClient.GetAccountInfoAsync(SessionTokenPDA);
return sessionTokenData.Result.Value != null;
}

/// <summary>
/// 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.
/// </summary>
/// <returns>True if the session token is still valid, false otherwise.</returns>
public async Task<bool> IsSessionTokenValid()
{
var sessionTokenData = (await ActiveRpcClient.GetAccountInfoAsync(SessionTokenPDA)).Result.Value.Data[0];
if (sessionTokenData == null) return false;
return SessionToken.Deserialize(Convert.FromBase64String(sessionTokenData)).ValidUntil > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

}
}
Loading