xyz.dynamic.unitysdk
v2.0.1
Published
Dynamic authentication and embedded wallet SDK for Unity
Maintainers
Readme
Dynamic SDK for Unity
The Dynamic SDK for Unity provides authentication, wallet management, and blockchain transaction support for Unity games and applications. It supports EVM, Solana, and SUI chains through an embedded WebView-based architecture.
Platform Requirements
- Unity 6000.0+ (Unity 6) and newer
Supported platforms
- Since this SDK is built on top of the UniWebView, all the platforms that plugin supports are supported by Dynamic SDK, meaning macOS (including Editor), Android and iOS. You can develop your applicaiton in Linux and Windows Editors but you'll not be able to test any of the SDK features in them , but only on your mobile devices.
Passkey Limitation: If an account is secured with a Passkey, it cannot be used in the Unity Editor for testing. Passkey authentication requires a native device (iOS/Android). All other authentication methods work in the Editor on macOS.
Installation
1. Add Scoped Registry
Open your Unity project and go to Edit > Project Settings > Package Manager.
Add a new scoped registry:
| Field | Value |
|-------|-------|
| Name | Dynamic SDK |
| URL | https://packages.dynamic.xyz |
| Scope(s) | xyz.dynamic.unitysdk |
2. Install the SDK
Open Window > Package Manager, click the + button, and select Install package by name.
Enter: xyz.dynamic.unitysdk
Leave the version blank to install the latest version.
3. Install UniTask (Required Dependency)
Open Packages/manifest.json in a text editor and add to the dependencies section:
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"4. Install Solana Support (Optional)
If you need Solana transaction building (not just message signin), install the Solana extension package.
In Package Manager > Install package by name, enter: xyz.dynamic.unitysdk.solana
Then add the following to your Packages/manifest.json dependencies:
"com.solana.unity_sdk": "https://github.com/kantagara/Solana.Unity-SDK.git"Note: The Solana extension package only enables transaction building (RPC calls,
PublicKey,SystemProgram.Transfer, etc.). plain-text message signing via the Dynamic webview work without this package.
5. Android Setup (Required for Android Builds)
To build for Android, you need a custom Main Gradle Template. In Unity, go to Edit > Project Settings > Player > Android > Publishing Settings and enable:
- Custom Main Gradle Template
Assets/Plugins/Android/mainTemplate.gradle
Replace the contents with:
apply plugin: 'com.android.library'
apply from: '../shared/keepUnitySymbols.gradle'
**APPLY_PLUGINS**
dependencies {
// [Solana.Unity-SDK] Dependencies
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.versionedparcelable:versionedparcelable:1.2.1'
implementation 'com.google.guava:guava:33.5.0-android'
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
// [Solana.Unity-SDK] End Dependencies
implementation "androidx.credentials:credentials:1.3.0"
implementation "androidx.credentials:credentials-play-services-auth:1.3.0"
implementation "androidx.concurrent:concurrent-futures:1.2.0"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.datastore:datastore-preferences:1.1.1'
implementation 'com.google.crypto.tink:tink-android:1.11.0'
**DEPS**}
android {
namespace "com.unity3d.player"
ndkPath "**NDKPATH**"
ndkVersion "**NDKVERSION**"
compileSdk **APIVERSION**
buildToolsVersion = "**BUILDTOOLS**"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
defaultConfig {
minSdk **MINSDK**
targetSdk **TARGETSDK**
ndk {
abiFilters **ABIFILTERS**
debugSymbolLevel **DEBUGSYMBOLLEVEL**
}
versionCode **VERSIONCODE**
versionName '**VERSIONNAME**'
consumerProguardFiles 'proguard-unity.txt'**USER_PROGUARD**
**DEFAULT_CONFIG_SETUP**
}
lint {
abortOnError false
}
androidResources {
noCompress = **BUILTIN_NOCOMPRESS** + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~"
}**PACKAGING**
}
**IL_CPP_BUILD_SETUP**
**SOURCE_BUILD_SETUP**
**EXTERNAL_SOURCES**
// [Solana.Unity-SDK] Conflict Resolution
configurations.all {
exclude group: 'com.google.guava', module: 'listenablefuture'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
resolutionStrategy {
force 'androidx.core:core:1.13.1'
}
}
// [Solana.Unity-SDK] End Conflict ResolutionKey dependencies explained:
androidx.credentials+credentials-play-services-auth— required for Passkey (WebAuthn) supportandroidx.datastore+tink-android— required for secure storageandroidx.browser— required for OAuth social login flowscom.google.guava— required by Solana SDK
Setup
The SDK must be initialized manually in your scene.
1. Create Configuration Asset
In the Unity Editor: Assets > Create > DynamicSDK > Client Props
This creates a ClientPropsData ScriptableObject. Configure it in the Inspector:
| Field | Required | Description |
|-------|----------|-------------|
| Environment ID | Yes | Your environment ID from the Dynamic dashboard |
| App Name | No | Your application name |
| App Logo URL | No | URL to your app logo |
| App Origin | Yes | Mandatory for EOA connections and for passkey verification of your domain |
| Log Level | No | Error (default), Warning, Info, or Debug |
| Debug Webview | No | Enable WebView debugging |
2. Create Initializer Script
Create a MonoBehaviour that initializes the SDK on Awake:
using DynamicSDK.Core;
using UnityEngine;
public class DynamicSDKManager : MonoBehaviour
{
[SerializeField] private ClientPropsData props;
private void Awake()
{
DynamicSDK.DynamicSDK.Init(props);
}
}Attach this script to a GameObject in your scene and assign the ClientPropsData asset to the props field.
The SDK creates a DontDestroyOnLoad GameObject and persists across scene loads.
3. Wait for SDK Ready
The SDK loads asynchronously via WebView. You must wait for it to be ready before calling any SDK methods:
void Start()
{
if (DynamicSDK.DynamicSDK.Instance.IsWebViewReady)
{
OnSdkReady();
}
else
{
DynamicSDK.DynamicSDK.Instance.OnWebViewReady += OnSdkReady;
}
}
private void OnSdkReady()
{
Debug.Log("Dynamic SDK is ready!");
// Subscribe to auth state changes
DynamicSDK.DynamicSDK.Instance.Auth.OnTokenChanged += OnTokenChanged;
DynamicSDK.DynamicSDK.Instance.Auth.OnUserChanged += OnUserChanged;
}
private void OnTokenChanged(string token)
{
if (string.IsNullOrEmpty(token))
{
Debug.Log("User logged out");
// Navigate to login screen, clear UI, etc.
}
else
{
Debug.Log("User authenticated!");
// Navigate to main screen, load wallets, etc.
}
}
private void OnUserChanged(UserProfile user)
{
if (user != null)
Debug.Log($"User: {user.Email}");
}Important: Do not call any SDK methods (auth, wallets, signing, etc.) before
IsWebViewReadyistrueor theOnWebViewReadyevent fires.
Authentication
The SDK supports multiple authentication methods. After successful authentication, the SDK automatically populates the wallet list.
Email OTP
// Send OTP
await DynamicSDK.Instance.Auth.Email.SendOTP("[email protected]");
// Verify OTP (on a separate screen)
await DynamicSDK.Instance.Auth.Email.VerifyOTP("123456");
// Resend if needed
await DynamicSDK.Instance.Auth.Email.ResendOTP();SMS OTP
await DynamicSDK.Instance.Auth.Sms.SendOTP(new PhoneData
{
DialCode = "+1",
Iso2 = "US",
Phone = "5551234567",
});
await DynamicSDK.Instance.Auth.Sms.VerifyOTP("123456");Social Login
await DynamicSDK.Instance.Auth.Social.Connect(SocialProvider.Google);
await DynamicSDK.Instance.Auth.Social.Connect(SocialProvider.Apple);
await DynamicSDK.Instance.Auth.Social.Connect(SocialProvider.Farcaster);Passkey Sign-In
await DynamicSDK.Instance.Auth.Passkey.SignIn();Passkey sign-in only works on native devices (iOS/Android), not in the Unity Editor or macOS.
External JWT
await DynamicSDK.Instance.Auth.ExternalAuth.SignInWithExternalJwt(
new SignInWithExternalJwtParams
{
ExternalJwt = jwtToken,
ExternalUserId = userId,
});Dynamic Widget (Built-in Auth UI)
DynamicSDK.Instance.UI.ShowAuth();Session Management
// Get current auth token
string token = DynamicSDK.Instance.Auth.Token;
// Subscribe to auth state changes
DynamicSDK.Instance.Auth.OnTokenChanged += (token) =>
{
if (!string.IsNullOrEmpty(token))
Debug.Log("User logged in");
else
Debug.Log("User logged out");
};
// Get current user profile
DynamicSDK.Instance.Auth.OnUserChanged += (user) =>
{
if (user != null)
Debug.Log($"User: {user.Email}");
};
// Logout
await DynamicSDK.Instance.Auth.Logout();Wallet Management
Access Wallets
// All user wallets
List<BaseWallet> wallets = DynamicSDK.Instance.Wallets.UserWallets;
// Subscribe to wallet changes
DynamicSDK.Instance.Wallets.OnUserWalletsChanged += (wallets) =>
{
foreach (var w in wallets)
Debug.Log($"{w.Chain}: {w.Address}");
};Create Embedded Wallet
// Without password
await DynamicSDK.Instance.Wallets.Embedded.CreateWallet(EmbeddedWalletChain.Evm);
// With password protection
await DynamicSDK.Instance.Wallets.Embedded.CreateWallet(
EmbeddedWalletChain.Sol,
password: "user_password"
);Supported chains: EmbeddedWalletChain.Evm, .Sol, .Sui, .Ton, .Btc
Get Balance
string balance = await DynamicSDK.Instance.Wallets.GetBalance(wallet);Reveal Private Key
await DynamicSDK.Instance.UI.RevealEmbeddedWalletPrivateKey();EVM Operations
Send Transaction (Native Transfer)
var tx = new Dictionary<string, object>
{
{ "to", "0xRecipientAddress" },
{ "from", wallet.Address },
{ "value", "0x" + ((long)(0.01 * 1e18)).ToString("X") } // 0.01 ETH in wei
};
string txHash = await DynamicSDK.Instance.Networks.Evm.SendTransaction(
wallet.Address, tx);Write Contract
var abiJson = @"[{
""inputs"": [
{""name"": ""to"", ""type"": ""address""},
{""name"": ""amount"", ""type"": ""uint256""}
],
""name"": ""transfer"",
""outputs"": [{""name"": """", ""type"": ""bool""}],
""stateMutability"": ""nonpayable"",
""type"": ""function""
}]";
var input = new Dictionary<string, object>
{
{ "address", "0xContractAddress" },
{ "abi", JsonConvert.DeserializeObject<List<object>>(abiJson) },
{ "functionName", "transfer" },
{ "args", new List<object> { "0xRecipient", "1000000" } },
};
string txHash = await DynamicSDK.Instance.Networks.Evm.WriteContract(
wallet.Id, input);Sign Typed Data (EIP-712)
var typedData = JsonConvert.DeserializeObject<Dictionary<string, object>>(@"{
""types"": {
""EIP712Domain"": [
{""name"": ""name"", ""type"": ""string""},
{""name"": ""version"", ""type"": ""string""},
{""name"": ""chainId"", ""type"": ""uint256""}
],
""Mail"": [
{""name"": ""from"", ""type"": ""string""},
{""name"": ""to"", ""type"": ""string""},
{""name"": ""contents"", ""type"": ""string""}
]
},
""primaryType"": ""Mail"",
""domain"": { ""name"": ""Example"", ""version"": ""1"", ""chainId"": 1 },
""message"": { ""from"": ""Alice"", ""to"": ""Bob"", ""contents"": ""Hello!"" }
}");
string signature = await DynamicSDK.Instance.Networks.Evm.SignTypedData(
wallet.Id, typedData);Solana Operations
Requires
xyz.dynamic.unitysdk.solanapackage andcom.solana.unity_sdkfor transaction building. Message signing works without these packages.
Sign Message
string signature = await DynamicSDK.Instance.Networks.Solana.SignMessage(
wallet.Id, "Hello World");Send SOL (Versioned Transaction)
using Solana.Unity.Programs;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Wallet;
var solana = DynamicSDK.Instance.Networks.Solana;
var rpcClient = solana.CreateConnection();
// Get recent blockhash
var blockHashResult = await rpcClient.GetLatestBlockHashAsync();
var blockHash = blockHashResult.Result.Value.Blockhash;
var fromKey = new PublicKey(wallet.Address);
var toKey = new PublicKey("RecipientAddress");
var lamports = (ulong)(0.01 * 1_000_000_000); // 0.01 SOL
// Build versioned (v0) transaction
var tx = new VersionedTransaction
{
RecentBlockHash = blockHash,
FeePayer = fromKey,
Instructions = new List<TransactionInstruction>
{
SystemProgram.Transfer(fromKey, toKey, lamports)
},
AddressTableLookups = new List<MessageAddressTableLookup>()
};
var message = tx.CompileMessage();
// Prepend 1 empty signature slot for the SDK to sign
var txBytes = new byte[1 + 64 + message.Length];
txBytes[0] = 1; // 1 signature slot (compact-u16)
// bytes 1..64 stay zero (empty signature placeholder)
Buffer.BlockCopy(message, 0, txBytes, 65, message.Length);
string txHash = await solana.SignAndSendTransaction(
wallet.Id, txBytes, SolanaTransactionType.Versioned);Send SPL Token (Versioned Transaction)
#if DYNAMIC_SDK_SOLANA
using Solana.Unity.Programs;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Wallet;
var solana = DynamicSDK.Instance.Networks.Solana;
var rpcClient = solana.CreateConnection();
var blockHashResult = await rpcClient.GetLatestBlockHashAsync();
var blockHash = blockHashResult.Result.Value.Blockhash;
var ownerKey = new PublicKey(wallet.Address);
var recipientKey = new PublicKey("RecipientAddress");
var mintKey = new PublicKey("TokenMintAddress"); // e.g. USDC mint
// Derive Associated Token Accounts
var sourceAta = AssociatedTokenAccountProgram
.DeriveAssociatedTokenAccount(ownerKey, mintKey);
var destAta = AssociatedTokenAccountProgram
.DeriveAssociatedTokenAccount(recipientKey, mintKey);
int decimals = 6; // USDC has 6 decimals
var rawAmount = (ulong)(1.5 * Math.Pow(10, decimals)); // 1.5 USDC
var tx = new VersionedTransaction
{
RecentBlockHash = blockHash,
FeePayer = ownerKey,
Instructions = new List<TransactionInstruction>
{
// Create destination ATA if it doesn't exist (idempotent)
AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
ownerKey, recipientKey, mintKey),
// Transfer SPL tokens
TokenProgram.Transfer(sourceAta, destAta, rawAmount, ownerKey)
},
AddressTableLookups = new List<MessageAddressTableLookup>()
};
var message = tx.CompileMessage();
var txBytes = new byte[1 + 64 + message.Length];
txBytes[0] = 1;
Buffer.BlockCopy(message, 0, txBytes, 65, message.Length);
string txHash = await solana.SignAndSendTransaction(
wallet.Id, txBytes, SolanaTransactionType.Versioned);
#endifCommon Solana Token Mint Addresses
| Token | Network | Mint Address |
|-------|---------|--------------|
| USDC | Mainnet (101) | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| USDC | Devnet (103) | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
SUI Operations
Sign and Send SUI Transfer
// Sign only (does not broadcast)
string signature = await DynamicSDK.Instance.Networks.Sui.SignTransferTransaction(
wallet.Id, "0xRecipientAddress", "0.1");
// Sign and broadcast
string digest = await DynamicSDK.Instance.Networks.Sui.SignAndSendTransferTransaction(
wallet.Id, "0xRecipientAddress", "0.1");Sign Message
string signature = await DynamicSDK.Instance.Networks.Sui.SignMessage(
wallet.Id, "Hello from SUI");Sign / Send Raw Transaction
// Sign a base64-encoded transaction
string signature = await DynamicSDK.Instance.Networks.Sui.SignTransaction(
wallet.Id, base64TransactionBytes);
// Send a base64-encoded transaction
string digest = await DynamicSDK.Instance.Networks.Sui.SendTransaction(
wallet.Id, base64TransactionBytes);Token Balances
Query Multichain Balances
var request = new MultichainBalanceRequest
{
FilterSpamTokens = true,
BalanceRequests = new List<BalanceRequestItem>
{
new()
{
Address = wallet.Address,
Chain = "SOL", // or "EVM"
NetworkIds = new List<object> { 103 }, // Solana Devnet
}
}
};
var response = await DynamicSDK.Instance.Wallets.GetMultichainBalances(request);
foreach (var token in response.Balances)
{
Debug.Log($"{token.Symbol}: {token.Balance} (mint: {token.MintAddress})");
}Query Specific Token Balance
Use WhitelistedContracts to query a specific token by its contract/mint address:
var request = new MultichainBalanceRequest
{
FilterSpamTokens = false,
BalanceRequests = new List<BalanceRequestItem>
{
new()
{
Address = wallet.Address,
Chain = "SOL",
NetworkIds = new List<object> { 103 },
WhitelistedContracts = new List<string>
{
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" // USDC Devnet
},
}
}
};
var response = await DynamicSDK.Instance.Wallets.GetMultichainBalances(request);Passkey Management
Passkey registration and authentication only work on native devices (iOS/Android), not in the Unity Editor.
Register a Passkey
await DynamicSDK.Instance.Passkeys.RegisterPasskey();List Passkeys
List<UserPasskey> passkeys = await DynamicSDK.Instance.Passkeys.GetPasskeys();
foreach (var pk in passkeys)
Debug.Log($"Passkey: {pk.Id}, created: {pk.CreatedAt}");Authenticate with Passkey (MFA)
var result = await DynamicSDK.Instance.Passkeys.AuthenticatePasskeyMFA(
new MfaCreateToken { SingleUse = true });
string token = result?.MfaToken ?? result?.Jwt;Delete a Passkey
await DynamicSDK.Instance.Passkeys.DeletePasskey(new DeletePasskeyRequest
{
CredentialId = passkey.CredentialId,
PasskeyId = passkey.Id,
});Embedded Wallet (WaaS)
Password Protection
// Check wallet recovery state
var state = await DynamicSDK.Instance.Wallets.Waas.GetWalletRecoveryState(wallet);
bool isPasswordProtected = state.IsPasswordEncrypted;
// Set password on a wallet that doesn't have one
await DynamicSDK.Instance.Wallets.Waas.SetPassword(wallet, "new_password");
// Unlock a password-protected wallet
await DynamicSDK.Instance.Wallets.Waas.UnlockWallet(wallet, "password");
// Update password
await DynamicSDK.Instance.Wallets.Waas.UpdatePassword(wallet, "old_password", "new_password");Delegated Access
Delegated access allows key shares to be delegated for server-side transaction signing.
var delegation = DynamicSDK.Instance.Wallets.Waas.Delegation;
// Check if wallet is eligible
bool eligible = delegation.IsWalletEligibleForDelegation(wallet);
// Delegate key shares
await delegation.DelegateKeyShares(
new List<DelegationWalletIdentifier>
{
new()
{
ChainName = ChainEnum.Sol, // or ChainEnum.Evm
AccountAddress = wallet.Address,
}
});
// Check delegation status
var status = delegation.GetDelegationStatusForWallet(wallet.Id);
// Revoke delegation
await delegation.RevokeDelegation(
new List<DelegationWalletIdentifier>
{
new()
{
ChainName = ChainEnum.Sol,
AccountAddress = wallet.Address,
}
});MFA (Multi-Factor Authentication)
TOTP Setup
// Add a TOTP device
var result = await DynamicSDK.Instance.Mfa.AddDevice("totp");
// result.Secret — base32 secret for manual entry
// result.Uri — otpauth:// URI for QR code
// Verify with code from authenticator app
await DynamicSDK.Instance.Mfa.VerifyDevice(code, "totp");TOTP Authentication
string mfaToken = await DynamicSDK.Instance.Mfa.AuthenticateDevice(
new MfaAuthenticateDevice
{
Code = "123456",
Type = "totp",
DeviceId = deviceId,
CreateMfaToken = new MfaCreateToken { SingleUse = true },
});Recovery Codes
List<string> codes = await DynamicSDK.Instance.Mfa.GetNewRecoveryCodes();API Reference
Module Accessors
| Accessor | Description |
|----------|-------------|
| DynamicSDK.Instance.Auth | Authentication (email, SMS, social, passkey, JWT) |
| DynamicSDK.Instance.Wallets | Wallet management, balances, signing, delegation |
| DynamicSDK.Instance.Networks.Evm | EVM transactions and contract interaction |
| DynamicSDK.Instance.Networks.Solana | Solana signing, RPC connection, transactions |
| DynamicSDK.Instance.Networks.Sui | SUI signing and transactions |
| DynamicSDK.Instance.UI | Built-in UI (auth flow, profile, private key reveal) |
| DynamicSDK.Instance.Passkeys | Passkey registration and authentication |
| DynamicSDK.Instance.Mfa | Multi-factor authentication management |
Sample App
The SDK includes a complete sample app demonstrating all features. To import it:
- Open Window > Package Manager
- Select Dynamic SDK from the package list
- Expand Samples
- Click Import next to Sample App
Documentation
For full documentation, visit https://docs.dynamic.xyz.
