From 2f8de680f55d0960f8317a86e37279fe59bacf31 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 16 Sep 2026 09:06:51 +0100 Subject: [PATCH 1/2] Make the Pera and email WebGL SDK the main Unity Package Manager release --- .github/workflows/webgl-package.yml | 6 +- Blockmaker.asmdef | 20 - .../blockmaker-txnlab-wallet.NOTICES.txt | 0 .../blockmaker-txnlab-wallet.d.mts | 0 .../blockmaker-txnlab-wallet.mjs | 0 .../blockmaker-unity-webgl-wallet-host.mjs | 0 .../Blockmaker => Browser~}/blockmaker.js | 0 CHANGELOG.md | 10 + Core/AlgoSignTypes.cs | 19 - Core/AlgoSignTypes.cs.meta | 2 - Core/BlockmakerAsyncExtensions.cs | 142 - Core/BlockmakerAsyncExtensions.cs.meta | 2 - Core/BlockmakerAuth.cs | 2450 ----------------- Core/BlockmakerAuth.cs.meta | 2 - Core/BlockmakerClient.cs | 1171 -------- Core/BlockmakerClient.cs.meta | 2 - Core/BlockmakerConfig.cs | 82 - Core/BlockmakerConfig.cs.meta | 2 - Core/BlockmakerErrors.cs | 42 - Core/BlockmakerErrors.cs.meta | 2 - Core/BlockmakerLog.cs | 89 - Core/BlockmakerLog.cs.meta | 2 - Core/BlockmakerProfileManager.cs | 702 ----- Core/BlockmakerProfileManager.cs.meta | 2 - Core/BlockmakerTypes.cs | 551 ---- Core/BlockmakerTypes.cs.meta | 2 - Core/BlockmakerUIUtils.cs | 16 - Core/BlockmakerUIUtils.cs.meta | 2 - Core/DefaultAvatarSet.cs | 22 - Core/DefaultAvatarSet.cs.meta | 2 - Core/IBlockmakerIdentity.cs | 96 - Core/IBlockmakerIdentity.cs.meta | 2 - Core/QRCodeEncoder.cs | 554 ---- Core/QRCodeEncoder.cs.meta | 2 - Core/QRTextureGenerator.cs | 77 - Core/QRTextureGenerator.cs.meta | 2 - Core/ReownWalletConnector.cs | 995 ------- Core/ReownWalletConnector.cs.meta | 2 - Core/SecurePrefs.cs | 257 -- Core/SecurePrefs.cs.meta | 2 - Core/TokenBalanceTracker.cs | 253 -- Core/TokenBalanceTracker.cs.meta | 2 - Core/WalletConnectV1Client.cs | 688 ----- Core/WalletConnectV1Client.cs.meta | 2 - Core/WalletDeepLink.cs | 108 - Core/WalletDeepLink.cs.meta | 2 - Core/XChainAddressDeriver.cs | 518 ---- Core/XChainAddressDeriver.cs.meta | 2 - .../email-setup.md | 2 +- Documentation~/integration.md | 107 + Documentation~/upgrading.md | 47 + Documentation~/validation.md | 56 + Editor/Blockmaker.Editor.asmdef | 25 +- Editor/BlockmakerBootSceneSetup.cs | 155 -- Editor/BlockmakerBootSceneSetup.cs.meta | 2 - Editor/BlockmakerBuildKeyStripper.cs | 87 - Editor/BlockmakerBuildKeyStripper.cs.meta | 11 - Editor/BlockmakerSetup.cs | 61 + Editor/BlockmakerSetup.cs.meta | 4 + Editor/BlockmakerWebGLBuild.cs | 78 + Editor/BlockmakerWebGLBuild.cs.meta | 4 + Identity/EmailIdentity.cs | 100 - Identity/EmailIdentity.cs.meta | 2 - Identity/EvmXChainIdentity.cs | 682 ----- Identity/EvmXChainIdentity.cs.meta | 2 - Identity/GuestIdentity.cs | 87 - Identity/GuestIdentity.cs.meta | 2 - Identity/MagicIdentity.cs | 281 -- Identity/MagicIdentity.cs.meta | 2 - Identity/ServerSignedIdentity.cs | 243 -- Identity/ServerSignedIdentity.cs.meta | 2 - Identity/WalletConnectIdentity.cs | 770 ------ Identity/WalletConnectIdentity.cs.meta | 2 - Installer~/BlockmakerInstaller.cs | 105 +- LEGACY_V1.md | 304 -- Plugins/WebGL/BlockmakerWalletBridge.cs | 344 --- Plugins/WebGL/BlockmakerWalletBridge.cs.meta | 2 - Plugins/WebGL/BlockmakerWalletBridge.jslib | 1522 ---------- .../WebGL/BlockmakerWalletBridge.jslib.meta | 2 - .../WebGL/BlockmakerWalletPackageWebGL.jslib | 0 .../BlockmakerWalletPackageWebGL.jslib.meta | 32 + README.md | 139 +- Runtime.meta | 5 + Runtime/Blockmaker.asmdef | 6 + .../Blockmaker.asmdef.meta | 0 .../Runtime => Runtime}/BlockmakerClient.cs | 0 Runtime/BlockmakerClient.cs.meta | 2 + .../BlockmakerUnityPresentation.cs | 0 Runtime/BlockmakerUnityPresentation.cs.meta | 2 + .../BlockmakerWalletDemo.cs | 26 +- Runtime/BlockmakerWalletDemo.cs.meta | 4 + .../BlockmakerWalletPackageWebGL.cs | 0 Runtime/BlockmakerWalletPackageWebGL.cs.meta | 2 + Runtime/WalletDemo.tss | 1 + Runtime/WalletDemo.tss.meta | 4 + Samples~/WalletDemo/README.md | 44 +- Samples~/WalletDemo/WalletDemo.cs | 312 --- Samples~/WalletDemo/WalletDemo.unity | 284 ++ .../WalletDemo/WalletDemo.unity.meta | 3 +- Samples~/WalletDemo/WalletPanel.asset | 52 + .../WalletDemo/WalletPanel.asset.meta | 6 +- Templates~/Blockmaker/index.html | 45 + Tools~/package.test.mjs | 79 + .../wallet-package-contract.mjs | 0 UI.meta | 8 - UI/AuthPrompt.uss | 1180 -------- UI/AuthPrompt.uss.meta | 12 - UI/AuthPrompt.uxml | 176 -- UI/AuthPrompt.uxml.meta | 10 - UI/AuthPromptController.cs | 1871 ------------- UI/AuthPromptController.cs.meta | 2 - UI/AuthScreen.uss | 316 --- UI/AuthScreen.uss.meta | 12 - UI/AuthScreen.uxml | 86 - UI/AuthScreen.uxml.meta | 10 - UI/AuthScreenController.cs | 161 -- UI/AuthScreenController.cs.meta | 2 - UI/BlockmakerAccount.uss | 378 --- UI/BlockmakerAccount.uss.meta | 12 - UI/BlockmakerAccount.uxml | 74 - UI/BlockmakerAccount.uxml.meta | 10 - UI/BlockmakerAccountController.cs | 201 -- UI/BlockmakerAccountController.cs.meta | 2 - UI/BlockmakerAuthUI.cs | 650 ----- UI/BlockmakerAuthUI.cs.meta | 2 - UI/BlockmakerConnectButton.cs | 227 -- UI/BlockmakerConnectButton.cs.meta | 10 - UI/BlockmakerTheme.uss | 116 - UI/BlockmakerTheme.uss.meta | 12 - UI/OTPScreen.uss | 93 - UI/OTPScreen.uss.meta | 12 - UI/OTPScreen.uxml | 56 - UI/OTPScreen.uxml.meta | 10 - UI/OTPScreenController.cs | 194 -- UI/OTPScreenController.cs.meta | 2 - UI/PeraConnectModal.uss | 490 ---- UI/PeraConnectModal.uss.meta | 12 - UI/PeraConnectModal.uxml | 90 - UI/PeraConnectModal.uxml.meta | 10 - UI/PeraConnectModalController.cs | 519 ---- UI/PeraConnectModalController.cs.meta | 2 - UI/ProfilePicPicker.uss | 349 --- UI/ProfilePicPicker.uss.meta | 12 - UI/ProfilePicPicker.uxml | 47 - UI/ProfilePicPicker.uxml.meta | 10 - UI/ProfilePicPickerController.cs | 423 --- UI/ProfilePicPickerController.cs.meta | 2 - UI/WalletBar.uss | 152 - UI/WalletBar.uss.meta | 12 - UI/WalletBar.uxml | 36 - UI/WalletBar.uxml.meta | 10 - UI/WalletBarController.cs | 164 -- UI/WalletBarController.cs.meta | 2 - UI/WalletUpgradeController.cs | 118 - UI/WalletUpgradeController.cs.meta | 2 - UI/WalletUpgradePrompt.uss | 297 -- UI/WalletUpgradePrompt.uss.meta | 12 - UI/WalletUpgradePrompt.uxml | 69 - UI/WalletUpgradePrompt.uxml.meta | 10 - UI/algorand-logo.png | Bin 7596 -> 0 bytes UI/algorand-logo.png.meta | 47 - UI/coinbase-logo.png | Bin 2881 -> 0 bytes UI/coinbase-logo.png.meta | 143 - UI/defly-logo.png | Bin 5928 -> 0 bytes UI/defly-logo.png.meta | 22 - UI/generic-wallet-icon.png | Bin 1192 -> 0 bytes UI/generic-wallet-icon.png.meta | 143 - UI/list-fade.png | Bin 293 -> 0 bytes UI/list-fade.png.meta | 143 - UI/metamask-logo.png | Bin 12045 -> 0 bytes UI/metamask-logo.png.meta | 143 - UI/pera-logo.png | Bin 9592 -> 0 bytes UI/pera-logo.png.meta | 34 - UI/rainbow-logo.png | Bin 13613 -> 0 bytes UI/rainbow-logo.png.meta | 143 - UI/trust-logo.png | Bin 9435 -> 0 bytes UI/trust-logo.png.meta | 143 - UI/xchain-logo.png | Bin 19753 -> 0 bytes UI/xchain-logo.png.meta | 22 - WebGL~/LICENSE | 21 - WebGL~/README.md | 122 - WebGL~/Samples~/EmailWallet/README.md | 46 - WebGL~/Tools/install.mjs | 161 -- WebGL~/Tools/install.test.mjs | 104 - WebGL~/VALIDATION.md | 64 - ...age-manifest.json => package-manifest.json | 0 package-manifest.json.meta | 4 + package.json | 64 +- 188 files changed, 1100 insertions(+), 24275 deletions(-) delete mode 100644 Blockmaker.asmdef rename {WebGL~/Assets/StreamingAssets/Blockmaker => Browser~}/blockmaker-txnlab-wallet.NOTICES.txt (100%) rename {WebGL~/Assets/StreamingAssets/Blockmaker => Browser~}/blockmaker-txnlab-wallet.d.mts (100%) rename {WebGL~/Assets/StreamingAssets/Blockmaker => Browser~}/blockmaker-txnlab-wallet.mjs (100%) rename {WebGL~/Assets/StreamingAssets/Blockmaker => Browser~}/blockmaker-unity-webgl-wallet-host.mjs (100%) rename {WebGL~/Assets/StreamingAssets/Blockmaker => Browser~}/blockmaker.js (100%) delete mode 100644 Core/AlgoSignTypes.cs delete mode 100644 Core/AlgoSignTypes.cs.meta delete mode 100644 Core/BlockmakerAsyncExtensions.cs delete mode 100644 Core/BlockmakerAsyncExtensions.cs.meta delete mode 100644 Core/BlockmakerAuth.cs delete mode 100644 Core/BlockmakerAuth.cs.meta delete mode 100644 Core/BlockmakerClient.cs delete mode 100644 Core/BlockmakerClient.cs.meta delete mode 100644 Core/BlockmakerConfig.cs delete mode 100644 Core/BlockmakerConfig.cs.meta delete mode 100644 Core/BlockmakerErrors.cs delete mode 100644 Core/BlockmakerErrors.cs.meta delete mode 100644 Core/BlockmakerLog.cs delete mode 100644 Core/BlockmakerLog.cs.meta delete mode 100644 Core/BlockmakerProfileManager.cs delete mode 100644 Core/BlockmakerProfileManager.cs.meta delete mode 100644 Core/BlockmakerTypes.cs delete mode 100644 Core/BlockmakerTypes.cs.meta delete mode 100644 Core/BlockmakerUIUtils.cs delete mode 100644 Core/BlockmakerUIUtils.cs.meta delete mode 100644 Core/DefaultAvatarSet.cs delete mode 100644 Core/DefaultAvatarSet.cs.meta delete mode 100644 Core/IBlockmakerIdentity.cs delete mode 100644 Core/IBlockmakerIdentity.cs.meta delete mode 100644 Core/QRCodeEncoder.cs delete mode 100644 Core/QRCodeEncoder.cs.meta delete mode 100644 Core/QRTextureGenerator.cs delete mode 100644 Core/QRTextureGenerator.cs.meta delete mode 100644 Core/ReownWalletConnector.cs delete mode 100644 Core/ReownWalletConnector.cs.meta delete mode 100644 Core/SecurePrefs.cs delete mode 100644 Core/SecurePrefs.cs.meta delete mode 100644 Core/TokenBalanceTracker.cs delete mode 100644 Core/TokenBalanceTracker.cs.meta delete mode 100644 Core/WalletConnectV1Client.cs delete mode 100644 Core/WalletConnectV1Client.cs.meta delete mode 100644 Core/WalletDeepLink.cs delete mode 100644 Core/WalletDeepLink.cs.meta delete mode 100644 Core/XChainAddressDeriver.cs delete mode 100644 Core/XChainAddressDeriver.cs.meta rename WebGL~/EMAIL_SETUP.md => Documentation~/email-setup.md (99%) create mode 100644 Documentation~/integration.md create mode 100644 Documentation~/upgrading.md create mode 100644 Documentation~/validation.md delete mode 100644 Editor/BlockmakerBootSceneSetup.cs delete mode 100644 Editor/BlockmakerBootSceneSetup.cs.meta delete mode 100644 Editor/BlockmakerBuildKeyStripper.cs delete mode 100644 Editor/BlockmakerBuildKeyStripper.cs.meta create mode 100644 Editor/BlockmakerSetup.cs create mode 100644 Editor/BlockmakerSetup.cs.meta create mode 100644 Editor/BlockmakerWebGLBuild.cs create mode 100644 Editor/BlockmakerWebGLBuild.cs.meta delete mode 100644 Identity/EmailIdentity.cs delete mode 100644 Identity/EmailIdentity.cs.meta delete mode 100644 Identity/EvmXChainIdentity.cs delete mode 100644 Identity/EvmXChainIdentity.cs.meta delete mode 100644 Identity/GuestIdentity.cs delete mode 100644 Identity/GuestIdentity.cs.meta delete mode 100644 Identity/MagicIdentity.cs delete mode 100644 Identity/MagicIdentity.cs.meta delete mode 100644 Identity/ServerSignedIdentity.cs delete mode 100644 Identity/ServerSignedIdentity.cs.meta delete mode 100644 Identity/WalletConnectIdentity.cs delete mode 100644 Identity/WalletConnectIdentity.cs.meta delete mode 100644 LEGACY_V1.md delete mode 100644 Plugins/WebGL/BlockmakerWalletBridge.cs delete mode 100644 Plugins/WebGL/BlockmakerWalletBridge.cs.meta delete mode 100644 Plugins/WebGL/BlockmakerWalletBridge.jslib delete mode 100644 Plugins/WebGL/BlockmakerWalletBridge.jslib.meta rename {WebGL~/Assets/Plugins => Plugins}/WebGL/BlockmakerWalletPackageWebGL.jslib (100%) create mode 100644 Plugins/WebGL/BlockmakerWalletPackageWebGL.jslib.meta create mode 100644 Runtime.meta create mode 100644 Runtime/Blockmaker.asmdef rename Blockmaker.asmdef.meta => Runtime/Blockmaker.asmdef.meta (100%) rename {WebGL~/Assets/Blockmaker/Runtime => Runtime}/BlockmakerClient.cs (100%) create mode 100644 Runtime/BlockmakerClient.cs.meta rename {WebGL~/Assets/Blockmaker/Runtime => Runtime}/BlockmakerUnityPresentation.cs (100%) create mode 100644 Runtime/BlockmakerUnityPresentation.cs.meta rename WebGL~/Samples~/EmailWallet/BlockmakerEmailSample.cs => Runtime/BlockmakerWalletDemo.cs (77%) create mode 100644 Runtime/BlockmakerWalletDemo.cs.meta rename {WebGL~/Assets/Blockmaker/Runtime => Runtime}/BlockmakerWalletPackageWebGL.cs (100%) create mode 100644 Runtime/BlockmakerWalletPackageWebGL.cs.meta create mode 100644 Runtime/WalletDemo.tss create mode 100644 Runtime/WalletDemo.tss.meta delete mode 100644 Samples~/WalletDemo/WalletDemo.cs create mode 100644 Samples~/WalletDemo/WalletDemo.unity rename Core.meta => Samples~/WalletDemo/WalletDemo.unity.meta (67%) create mode 100644 Samples~/WalletDemo/WalletPanel.asset rename Identity.meta => Samples~/WalletDemo/WalletPanel.asset.meta (52%) create mode 100644 Templates~/Blockmaker/index.html create mode 100644 Tools~/package.test.mjs rename {WebGL~/Tools => Tools~}/wallet-package-contract.mjs (100%) delete mode 100644 UI.meta delete mode 100644 UI/AuthPrompt.uss delete mode 100644 UI/AuthPrompt.uss.meta delete mode 100644 UI/AuthPrompt.uxml delete mode 100644 UI/AuthPrompt.uxml.meta delete mode 100644 UI/AuthPromptController.cs delete mode 100644 UI/AuthPromptController.cs.meta delete mode 100644 UI/AuthScreen.uss delete mode 100644 UI/AuthScreen.uss.meta delete mode 100644 UI/AuthScreen.uxml delete mode 100644 UI/AuthScreen.uxml.meta delete mode 100644 UI/AuthScreenController.cs delete mode 100644 UI/AuthScreenController.cs.meta delete mode 100644 UI/BlockmakerAccount.uss delete mode 100644 UI/BlockmakerAccount.uss.meta delete mode 100644 UI/BlockmakerAccount.uxml delete mode 100644 UI/BlockmakerAccount.uxml.meta delete mode 100644 UI/BlockmakerAccountController.cs delete mode 100644 UI/BlockmakerAccountController.cs.meta delete mode 100644 UI/BlockmakerAuthUI.cs delete mode 100644 UI/BlockmakerAuthUI.cs.meta delete mode 100644 UI/BlockmakerConnectButton.cs delete mode 100644 UI/BlockmakerConnectButton.cs.meta delete mode 100644 UI/BlockmakerTheme.uss delete mode 100644 UI/BlockmakerTheme.uss.meta delete mode 100644 UI/OTPScreen.uss delete mode 100644 UI/OTPScreen.uss.meta delete mode 100644 UI/OTPScreen.uxml delete mode 100644 UI/OTPScreen.uxml.meta delete mode 100644 UI/OTPScreenController.cs delete mode 100644 UI/OTPScreenController.cs.meta delete mode 100644 UI/PeraConnectModal.uss delete mode 100644 UI/PeraConnectModal.uss.meta delete mode 100644 UI/PeraConnectModal.uxml delete mode 100644 UI/PeraConnectModal.uxml.meta delete mode 100644 UI/PeraConnectModalController.cs delete mode 100644 UI/PeraConnectModalController.cs.meta delete mode 100644 UI/ProfilePicPicker.uss delete mode 100644 UI/ProfilePicPicker.uss.meta delete mode 100644 UI/ProfilePicPicker.uxml delete mode 100644 UI/ProfilePicPicker.uxml.meta delete mode 100644 UI/ProfilePicPickerController.cs delete mode 100644 UI/ProfilePicPickerController.cs.meta delete mode 100644 UI/WalletBar.uss delete mode 100644 UI/WalletBar.uss.meta delete mode 100644 UI/WalletBar.uxml delete mode 100644 UI/WalletBar.uxml.meta delete mode 100644 UI/WalletBarController.cs delete mode 100644 UI/WalletBarController.cs.meta delete mode 100644 UI/WalletUpgradeController.cs delete mode 100644 UI/WalletUpgradeController.cs.meta delete mode 100644 UI/WalletUpgradePrompt.uss delete mode 100644 UI/WalletUpgradePrompt.uss.meta delete mode 100644 UI/WalletUpgradePrompt.uxml delete mode 100644 UI/WalletUpgradePrompt.uxml.meta delete mode 100644 UI/algorand-logo.png delete mode 100644 UI/algorand-logo.png.meta delete mode 100644 UI/coinbase-logo.png delete mode 100644 UI/coinbase-logo.png.meta delete mode 100644 UI/defly-logo.png delete mode 100644 UI/defly-logo.png.meta delete mode 100644 UI/generic-wallet-icon.png delete mode 100644 UI/generic-wallet-icon.png.meta delete mode 100644 UI/list-fade.png delete mode 100644 UI/list-fade.png.meta delete mode 100644 UI/metamask-logo.png delete mode 100644 UI/metamask-logo.png.meta delete mode 100644 UI/pera-logo.png delete mode 100644 UI/pera-logo.png.meta delete mode 100644 UI/rainbow-logo.png delete mode 100644 UI/rainbow-logo.png.meta delete mode 100644 UI/trust-logo.png delete mode 100644 UI/trust-logo.png.meta delete mode 100644 UI/xchain-logo.png delete mode 100644 UI/xchain-logo.png.meta delete mode 100644 WebGL~/LICENSE delete mode 100644 WebGL~/README.md delete mode 100644 WebGL~/Samples~/EmailWallet/README.md delete mode 100644 WebGL~/Tools/install.mjs delete mode 100644 WebGL~/Tools/install.test.mjs delete mode 100644 WebGL~/VALIDATION.md rename WebGL~/package-manifest.json => package-manifest.json (100%) create mode 100644 package-manifest.json.meta diff --git a/.github/workflows/webgl-package.yml b/.github/workflows/webgl-package.yml index 670217c..5bae39e 100644 --- a/.github/workflows/webgl-package.yml +++ b/.github/workflows/webgl-package.yml @@ -1,10 +1,8 @@ -name: WebGL package +name: Unity package on: pull_request: - paths: ['WebGL~/**', '.github/workflows/webgl-package.yml'] push: branches: [main] - paths: ['WebGL~/**', '.github/workflows/webgl-package.yml'] permissions: contents: read jobs: @@ -15,4 +13,4 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '22' - - run: node --test 'WebGL~/Tools/install.test.mjs' + - run: node --test 'Tools~/package.test.mjs' diff --git a/Blockmaker.asmdef b/Blockmaker.asmdef deleted file mode 100644 index 4425dc8..0000000 --- a/Blockmaker.asmdef +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Blockmaker", - "rootNamespace": "Blockmaker", - "references": [ - "Reown.Sign", - "Reown.Core", - "Reown.Core.Common", - "Reown.Core.Network", - "Reown.Sign.Unity" - ], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} diff --git a/WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-txnlab-wallet.NOTICES.txt b/Browser~/blockmaker-txnlab-wallet.NOTICES.txt similarity index 100% rename from WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-txnlab-wallet.NOTICES.txt rename to Browser~/blockmaker-txnlab-wallet.NOTICES.txt diff --git a/WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-txnlab-wallet.d.mts b/Browser~/blockmaker-txnlab-wallet.d.mts similarity index 100% rename from WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-txnlab-wallet.d.mts rename to Browser~/blockmaker-txnlab-wallet.d.mts diff --git a/WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-txnlab-wallet.mjs b/Browser~/blockmaker-txnlab-wallet.mjs similarity index 100% rename from WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-txnlab-wallet.mjs rename to Browser~/blockmaker-txnlab-wallet.mjs diff --git a/WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-unity-webgl-wallet-host.mjs b/Browser~/blockmaker-unity-webgl-wallet-host.mjs similarity index 100% rename from WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker-unity-webgl-wallet-host.mjs rename to Browser~/blockmaker-unity-webgl-wallet-host.mjs diff --git a/WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker.js b/Browser~/blockmaker.js similarity index 100% rename from WebGL~/Assets/StreamingAssets/Blockmaker/blockmaker.js rename to Browser~/blockmaker.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2949120..ebd0823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.0.0] - 2026-09-16 + +- Make the current WebGL Pera/optional email SDK the root Unity Package Manager + package; remove the obsolete v1 runtime and its external dependencies. +- Include verified browser assets automatically through Unity’s build API. +- Restore a one-file installer using `PackageManager.Client.Add`, plus demo + scene setup and a fullscreen WebGL template. No Node.js install step. +- Preserve the nine canonical wallet runtime files and their script/plugin + GUIDs. Document the major-version migration and pending attended wallet checks. + ## WebGL preview — 2026-09-16 - Publish the current complete Pera/Lute and optional TxnLab/MetaMask Embedded diff --git a/Core/AlgoSignTypes.cs b/Core/AlgoSignTypes.cs deleted file mode 100644 index b9da531..0000000 --- a/Core/AlgoSignTypes.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using UnityEngine.Scripting; - -namespace Blockmaker -{ - - /// - /// WalletConnect v2 JSON-RPC types for the Algorand algo_signTxn method. - /// Used as List<List<AlgoSignTxnParam>> — the SDK serializes the list - /// directly as the params field of the JSON-RPC request. - /// - [Preserve] - public class AlgoSignTxnParam - { - [Preserve] public string txn { get; set; } - [Preserve] public string message { get; set; } - } - -} \ No newline at end of file diff --git a/Core/AlgoSignTypes.cs.meta b/Core/AlgoSignTypes.cs.meta deleted file mode 100644 index 7a693e9..0000000 --- a/Core/AlgoSignTypes.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 69cb456d4b2fa40d68a5e46b5e0d6769 \ No newline at end of file diff --git a/Core/BlockmakerAsyncExtensions.cs b/Core/BlockmakerAsyncExtensions.cs deleted file mode 100644 index 20b30bf..0000000 --- a/Core/BlockmakerAsyncExtensions.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Blockmaker -{ - - /// - /// Async/await overloads for BlockmakerAuth and BlockmakerClient. - /// These wrap the callback-based APIs so game code can use modern C# patterns: - /// - /// var identity = await BlockmakerAuth.Instance.ConnectWalletAsync("Pera"); - /// var result = await BlockmakerClient.Instance.RunFlowAsync("myFlow"); - /// - /// All methods marshal back to the Unity main thread via TaskCompletionSource. - /// - public static class BlockmakerAsyncExtensions - { - // ── BlockmakerAuth ──────────────────────────────────────────────────────── - - /// Connect a wallet and return the identity, or throw on error. - public static Task ConnectWalletAsync(this BlockmakerAuth auth, string provider) - { - var tcs = new TaskCompletionSource(); - auth.ConnectWallet(provider, - identity => tcs.TrySetResult(identity), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// Connect via Magic email and return the identity, or throw on error. - public static Task ConnectMagicEmailAsync(this BlockmakerAuth auth, string email) - { - var tcs = new TaskCompletionSource(); - auth.ConnectMagicEmail(email, - identity => tcs.TrySetResult(identity), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// Connect an EVM wallet and return the identity, or throw on error. - public static Task ConnectEvmAsync(this BlockmakerAuth auth) - { - var tcs = new TaskCompletionSource(); - auth.ConnectEvm( - identity => tcs.TrySetResult(identity), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// Verify the current session. Returns true if valid. - public static Task VerifySessionAsync(this BlockmakerAuth auth) - { - var tcs = new TaskCompletionSource(); - auth.VerifySession(ok => tcs.TrySetResult(ok)); - return tcs.Task; - } - - /// Request an email OTP code, or throw on error. - public static Task RequestEmailOTPAsync(this BlockmakerAuth auth, string email) - { - var tcs = new TaskCompletionSource(); - auth.RequestEmailOTP(email, - () => tcs.TrySetResult(true), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// Verify an email OTP and return the identity, or throw on error. - public static Task VerifyEmailOTPAsync(this BlockmakerAuth auth, string email, string otp) - { - var tcs = new TaskCompletionSource(); - auth.VerifyEmailOTP(email, otp, - identity => tcs.TrySetResult(identity), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - // ── BlockmakerClient ────────────────────────────────────────────────────── - - /// POST to a server path and return the typed response, or throw on error. - public static Task PostAsync(this BlockmakerClient client, string path, TReq body) where TRes : class - { - var tcs = new TaskCompletionSource(); - client.Post(path, body, - result => tcs.TrySetResult(result), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// GET from a server path and return the typed response, or throw on error. - public static Task GetAsync(this BlockmakerClient client, string path) where TRes : class - { - var tcs = new TaskCompletionSource(); - client.Get(path, - result => tcs.TrySetResult(result), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - // ── BlockmakerProfileManager ────────────────────────────────────────────── - - /// Claim a username and return the updated profile, or throw on error. - public static Task ClaimUsernameAsync(this BlockmakerProfileManager mgr, string username) - { - var tcs = new TaskCompletionSource(); - mgr.ClaimUsername(username, - profile => tcs.TrySetResult(profile), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// Change username and return the updated profile, or throw on error. - public static Task ChangeUsernameAsync(this BlockmakerProfileManager mgr, string newUsername) - { - var tcs = new TaskCompletionSource(); - mgr.ChangeUsername(newUsername, - profile => tcs.TrySetResult(profile), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - - /// Set profile picture to an NFT and return the updated profile, or throw on error. - public static Task SetProfilePicNftAsync(this BlockmakerProfileManager mgr, long assetId) - { - var tcs = new TaskCompletionSource(); - mgr.SetProfilePicNft(assetId, - profile => tcs.TrySetResult(profile), - error => tcs.TrySetException(new BlockmakerException(error))); - return tcs.Task; - } - } - - /// - /// Exception thrown by async SDK methods when the underlying operation fails. - /// The message is always a user-friendly string safe to display in UI. - /// - public class BlockmakerException : Exception - { - public BlockmakerException(string message) : base(message) { } - } - -} \ No newline at end of file diff --git a/Core/BlockmakerAsyncExtensions.cs.meta b/Core/BlockmakerAsyncExtensions.cs.meta deleted file mode 100644 index 255cd3d..0000000 --- a/Core/BlockmakerAsyncExtensions.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e98ea94ce3f7842d194efe21146ee002 \ No newline at end of file diff --git a/Core/BlockmakerAuth.cs b/Core/BlockmakerAuth.cs deleted file mode 100644 index 60f2dda..0000000 --- a/Core/BlockmakerAuth.cs +++ /dev/null @@ -1,2450 +0,0 @@ -using System; -using System.Collections; -using System.ComponentModel; -using UnityEngine; -using UnityEngine.Scripting; - -namespace Blockmaker -{ - - /// - /// Central auth manager. Holds the current IBlockmakerIdentity and manages - /// the full identity lifecycle: guest start, login, session restore, upgrade, - /// and logout. - /// - /// All game code accesses identity through here: - /// BlockmakerAuth.Instance.Identity - /// BlockmakerAuth.Instance.Address - /// BlockmakerAuth.Instance.CanSign - /// - /// Subscribe to OnIdentityChanged to react to login/logout/upgrade events. - /// - /// Wallet connection: - /// Pera — uses a native WalletConnect v1 client (WalletConnectV1Client) - /// that connects directly to Pera's bridge servers, no external SDK needed. - /// Defly — uses the Reown SDK (WalletConnect v2). - /// Both paths generate a QR code via OnWalletQRReady for display in the UI. - /// After the user scans and approves, OnIdentityChanged fires with the address. - /// - /// Requires a WalletConnect Project ID for Defly/EVM — get one free at: - /// https://cloud.walletconnect.com - /// - public class BlockmakerAuth : MonoBehaviour - { - // ── Singleton ────────────────────────────────────────────────────────────── - public static BlockmakerAuth Instance { get; private set; } - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() - { - Instance = null; - OnIdentityChanged = null; - OnIdentityUpgraded = null; - OnAuthError = null; - OnWalletQRReady = null; - OnWalletAddressChanged = null; - SessionRestoreSettled = false; - OnSessionRestoreSettled = null; - BlockmakerPrefs.InvalidatePrefix(); - } - - // ── Provider constants ───────────────────────────────────────────────────── - public const string ProviderPera = "Pera"; - public const string ProviderDefly = "Defly"; - - // ── Events ───────────────────────────────────────────────────────────────── - - /// - /// Fired when the current identity changes (login, logout, upgrade). - /// Warning: This is a static event. Subscribers must unsubscribe in - /// OnDestroy() to avoid leaks across scene loads. - /// - public static event Action OnIdentityChanged; - - /// - /// Fired when the player upgrades from a lower tier to a higher one. - /// Warning: This is a static event. Subscribers must unsubscribe in - /// OnDestroy() to avoid leaks across scene loads. - /// - public static event Action OnIdentityUpgraded; - - /// - /// Fired when an auth error occurs (wallet connection failed, session expired, etc.). - /// Warning: This is a static event. Subscribers must unsubscribe in - /// OnDestroy() to avoid leaks across scene loads. - /// - public static event Action OnAuthError; - - /// Non-error progress messages for the auth UI (e.g. "approve the sign-in - /// request in your wallet") — fired at moments where the user must act - /// somewhere OTHER than the game and would otherwise see nothing happen. - public static event Action OnAuthStatus; - - /// - /// Fired when the QR code is ready to display. - /// Warning: This is a static event. Subscribers must unsubscribe in - /// OnDestroy() to avoid leaks across scene loads. - /// - public static event Action OnWalletQRReady; - - /// - /// Fired when signing in causes a wallet address change. - /// Subscribe to show a warning that assets at the old address - /// won't be accessible through the new sign-in method. - /// Warning: This is a static event. Subscribers must unsubscribe in - /// OnDestroy() to avoid leaks across scene loads. - /// - public static event Action OnWalletAddressChanged; - - // ── Boot-time session-restore resolution (additive; nothing in the SDK gates on it) ── - - /// - /// True once the boot-time session-restore attempt has RESOLVED: the stored session's - /// token was refreshed/verified (signed in), or a fresh user sign-in is required, or - /// there was no stored session at all. Until this flips, the restored identity is a - /// GUESS — UI should render a neutral "resolving" state rather than a stale identity - /// (which may be logged out a moment later) or a premature SIGN IN button. - /// A late-arriving success after this settles still fires OnIdentityChanged as usual. - /// - public static bool SessionRestoreSettled { get; private set; } - - /// - /// Fired ONCE, when flips true. Subscribers that - /// attach late should read the flag first — it may already be settled. - /// Warning: This is a static event. Subscribers must unsubscribe in - /// OnDestroy() to avoid leaks across scene loads. - /// - public static event Action OnSessionRestoreSettled; - - private static void SettleSessionRestore() - { - if (SessionRestoreSettled) return; - SessionRestoreSettled = true; - var handler = OnSessionRestoreSettled; - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - /// - /// True when Magic email login is available on this platform and configured. - /// - public static bool IsMagicAvailable - { - get - { - #if UNITY_WEBGL && !UNITY_EDITOR - var cfg = Instance?.blockmakerConfig; - return cfg != null && cfg.enableMagicEmail && !string.IsNullOrEmpty(cfg.magicPublishableKey); - #else - return false; - #endif - } - } - - /// - /// Wallet sign timeout sourced from config, with a safe fallback. - /// Used by identity classes instead of hardcoded constants. - /// - internal static float WalletSignTimeout => - Instance?.blockmakerConfig?.walletSignTimeoutSeconds ?? 120f; - - // ── Public identity accessors ────────────────────────────────────────────── - public bool IsAuthenticating { get; private set; } - public IBlockmakerIdentity Identity { get; private set; } - public string Address => Identity?.Address ?? string.Empty; - public string DisplayName => Identity?.DisplayName ?? "Guest"; - public bool HasWallet => Identity?.HasWallet ?? false; - public bool CanSign => Identity?.CanSign ?? false; - public IdentityTier Tier => Identity?.Tier ?? IdentityTier.Guest; - - /// True when the player has an active session (Email or SelfCustody tier). - public bool IsLoggedIn => Identity != null && Identity.Tier >= IdentityTier.Email; - - /// - /// Returns the player's email address if signed in via email or Magic, null otherwise. - /// - public string GetUserEmail() - { - if (Identity is EmailIdentity e) return e.Email; - if (Identity is MagicIdentity m) return m.Email; - return null; - } - - /// - /// Check whether the current session token is still valid. - /// Useful before expensive operations or after app resume. - /// Returns false for Guest identities. - /// - public void VerifySession(Action onResult) - { - if (Identity == null || Identity.Tier == IdentityTier.Guest) - { - onResult?.Invoke(false); - return; - } - - string token = null; - if (Identity is EmailIdentity e) token = e.SessionToken; - else if (Identity is MagicIdentity m) token = m.SessionToken; - - if (string.IsNullOrEmpty(token)) - { - if (Identity is WalletConnectIdentity wc) - { - var wcv1 = wc.OwnWCv1Client; - bool wcv1Connected = wcv1 != null && !wcv1.IsDisposed && wcv1.IsConnected; - var connector = ReownWalletConnector.Instance; - bool reownConnected = connector != null && connector.IsConnected; - onResult?.Invoke(wcv1Connected || reownConnected); - } - else if (Identity is EvmXChainIdentity) - { - var connector = ReownWalletConnector.Instance; - onResult?.Invoke(connector != null && connector.IsConnected); - } - else - { - onResult?.Invoke(false); - } - return; - } - - if (BlockmakerClient.Instance == null) - { - onResult?.Invoke(false); - return; - } - - BlockmakerClient.Instance.VerifySessionToken(token, onResult); - } - - // ── Pending JS sign callbacks ────────────────────────────────────────────── - internal string PendingSignedTxn { get; private set; } - internal string[] PendingSignedTxns { get; private set; } - internal string PendingSignError { get; private set; } - private int _signGeneration; - private int _pendingSignGeneration; - - internal string ConsumePendingSignedTxn() { var v = PendingSignedTxn; PendingSignedTxn = null; _signAwaiting = false; return v; } - internal string[] ConsumePendingSignedTxns() { var v = PendingSignedTxns; PendingSignedTxns = null; _signAwaiting = false; return v; } - internal string ConsumePendingSignError() { var v = PendingSignError; PendingSignError = null; _signAwaiting = false; return v; } - - /// - /// Clear pending sign state and return a generation token. - /// If the generation changes before your result arrives, another sign - /// request has started and yours should abort. - /// - internal int BeginPendingSign() - { - PendingSignedTxn = null; - PendingSignedTxns = null; - PendingSignError = null; - _pendingSignGeneration = ++_signGeneration; - _signAwaiting = true; - return _signGeneration; - } - - internal bool IsSignGenerationCurrent(int gen) => _signGeneration == gen; - - // True from BeginPendingSign() until the result is consumed (ConsumePending*). Covers - // both "sign awaiting in JS" and "result landed but not yet read", so deferring on it - // can't clobber an unconsumed result. Used to defer an auto wallet-login sign so it - // doesn't bump the sign generation out from under a user-initiated sign (which would - // otherwise make the loser die "interrupted"). - internal bool IsWebGLSignInFlight => _signAwaiting; - - private bool _signAwaiting; - - // ── Inspector ────────────────────────────────────────────────────────────── - [Header("Blockmaker")] - [Tooltip("Drag your BlockmakerConfig asset here. Required if no BlockmakerClient exists in the scene.")] - public BlockmakerConfig blockmakerConfig; - - [Header("WalletConnect (legacy — use BlockmakerConfig instead)")] - [Tooltip("Deprecated: set WalletConnect Project ID on BlockmakerConfig instead. This field is used as a fallback if the config field is empty.")] - [System.Obsolete("Use BlockmakerConfig.walletConnectProjectId instead")] - [HideInInspector] - public string walletConnectProjectId = ""; - - private string ResolvedWalletConnectProjectId - { - get - { - if (!string.IsNullOrEmpty(blockmakerConfig?.walletConnectProjectId)) - return blockmakerConfig.walletConnectProjectId; - if (!string.IsNullOrEmpty(walletConnectProjectId)) - return walletConnectProjectId; - return BlockmakerConfig.DefaultWalletConnectProjectId; - } - } - - // ── Proactive token refresh ───────────────────────────────────────────── - private Coroutine _tokenRefreshCoroutine; - private bool _isRefreshing; - private const float TOKEN_REFRESH_INTERVAL = 50f * 60f; // refresh ~10 min before 1h expiry - - // ── In-flight coroutine tracking ────────────────────────────────────────── - private Coroutine _magicLoginCoroutine; - private Coroutine _emailOtpCoroutine; - private Coroutine _peraConnectCoroutine; - private Coroutine _webglTimeoutCoroutine; - private bool _isWalletConnecting; - - // Guards against concurrent wallet-signature Login() coroutines. The existing - // SessionToken check is a no-op during the async login window because the token - // isn't set until the coroutine's last line, so connect+restore+reconnect events - // could each start a Login() (duplicate signature prompts / torn SaveSession). - private bool _walletLoginInFlight; - - // Monotonically-increasing id of the CURRENT wallet-login attempt. Every attempt - // captures the value at start; RetryWalletLogin / CancelWalletLogin / Logout bump it, - // which invalidates the old attempt's callbacks (a late success/error whose captured - // seq no longer matches is logged and ignored — it must not clear the new attempt's - // in-flight flag or double-fire OnIdentityChanged). - private int _walletLoginSeq; - // Live RunWalletLogin coroutine, so retry/cancel can hard-stop the old attempt - // (kills the nested Login() wait too — StopCoroutine disposes the iterator chain, - // running RunWalletLogin's finally, which is seq-guarded). - private Coroutine _walletLoginCoroutine; - - // ── Native WC v1 client for Pera ────────────────────────────────────────── - private Texture2D _peraQRTexture; - private WalletConnectV1Client _wcv1Client; - internal WalletConnectV1Client WCv1Client => _wcv1Client; - private volatile string _wcv1Address; - private volatile string _wcv1Error; - private volatile bool _wcv1Done; - - private ReownWalletConnector _connector; - - // ── Pending connection callbacks ────────────────────────────────────────── - private Action _pendingConnectSuccess; - private Action _pendingConnectError; - private Action _pendingMagicSuccess; - private Action _pendingMagicError; - private Action _pendingEvmSuccess; - private Action _pendingEvmError; - private IBlockmakerIdentity _evmRestoreCapturedIdentity; - - // ── Lifecycle ────────────────────────────────────────────────────────────── - - private void Awake() - { - if (Instance != null && Instance != this) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); - BlockmakerPrefs.InvalidatePrefix(); - EnsureBlockmakerClient(); - } - - private void EnsureBlockmakerClient() - { - if (BlockmakerClient.Instance != null) return; - - var existing = GetComponent(); - if (existing != null) - { - if (existing.config == null && blockmakerConfig != null) - { - existing.config = blockmakerConfig; - existing.InitFromAuth(); - } - return; - } - - BlockmakerConfig cfg = blockmakerConfig; - if (cfg == null) - { - var configs = Resources.FindObjectsOfTypeAll(); - if (configs.Length > 0) cfg = configs[0]; - } - - if (cfg == null) - { - BlockmakerLog.Warning("[BlockmakerAuth] No BlockmakerConfig found. Create one via Assets > Create > Blockmaker > Config."); - return; - } - - var client = gameObject.AddComponent(); - client.config = cfg; - client.InitFromAuth(); - - if (BlockmakerProfileManager.Instance == null && GetComponent() == null) - gameObject.AddComponent(); - } - - private void OnDestroy() - { - if (_connector != null) _connector.OnInitialized -= OnReownInitialized; - CleanupWCv1(); - StopTokenRefreshTimer(); - CancelWebGLTimeout(); - - if (Instance == this) - Instance = null; - } - - private float _lastRefreshTime; - - private void OnApplicationPause(bool paused) - { - if (paused) return; - float elapsed = Time.realtimeSinceStartup - _lastRefreshTime; - if (elapsed < 300f) return; - TryImmediateRefresh(); - } - - private void OnApplicationFocus(bool hasFocus) - { - if (!hasFocus) return; - float elapsed = Time.realtimeSinceStartup - _lastRefreshTime; - if (elapsed < 300f) return; - TryImmediateRefresh(); - } - - private void TryImmediateRefresh() - { - string refreshToken = null; - if (Identity is ServerSignedIdentity e) refreshToken = e.RefreshToken; - else if (Identity is MagicIdentity m) refreshToken = m.RefreshToken; - else if (Identity is WalletConnectIdentity wc) refreshToken = wc.RefreshToken; - else if (Identity is EvmXChainIdentity evm) refreshToken = evm.RefreshToken; - - if (string.IsNullOrEmpty(refreshToken) || BlockmakerClient.Instance == null || _isRefreshing) return; - - _isRefreshing = true; - _lastRefreshTime = Time.realtimeSinceStartup; - var capturedIdentity = Identity; - BlockmakerClient.Instance.RefreshToken(refreshToken, result => - { - _isRefreshing = false; - if (this == null || Identity != capturedIdentity) return; - if (result == null || string.IsNullOrEmpty(result.sessionToken)) return; - - if (capturedIdentity is ServerSignedIdentity ce) ce.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is MagicIdentity cm) cm.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is WalletConnectIdentity cwc) cwc.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is EvmXChainIdentity cevm) cevm.UpdateTokens(result.sessionToken, result.refreshToken); - capturedIdentity.SaveSession(); - BlockmakerLog.Info("[BlockmakerAuth] Token refreshed after app resume."); - }, err => - { - _isRefreshing = false; - BlockmakerLog.Warning($"[BlockmakerAuth] Resume token refresh failed: {err}"); - SafeInvoke(OnAuthError, "Your session may have expired. Please sign in again if you experience issues."); - }); - } - - private void Start() - { - if (!TryRestoreSession()) - { - SetIdentity(new GuestIdentity()); - SettleSessionRestore(); // no stored session — the auth state is KNOWN immediately - } - else - { - VerifyRestoredSession(); - } - - _connector = GetComponent(); - if (_connector == null) _connector = gameObject.AddComponent(); - var wcProjectId = ResolvedWalletConnectProjectId; - if (!string.IsNullOrEmpty(wcProjectId)) - { - BlockmakerLog.Info($"[BlockmakerAuth] Initializing Reown with project ID: {wcProjectId[..8]}..."); - _connector.OnInitialized += OnReownInitialized; - _connector.Initialize(wcProjectId); - } - else - { - BlockmakerLog.Warning("[BlockmakerAuth] No WalletConnect Project ID — Defly and X-Chain will not be available."); - } - -#if UNITY_WEBGL && !UNITY_EDITOR - // Restore browser-held wallet sessions (Pera JS etc.) NOW, independent of - // Reown init — a Reown failure/hang must not strand a returning Pera player's - // signing (their JWT restores but the Pera JS session never re-attached). - TryReconnectBrowserWalletSessions(); -#endif - } - - private void OnReownInitialized() - { - if (_connector != null) _connector.OnInitialized -= OnReownInitialized; - - TryReconnectWalletSessions(); - } - - // ── Session restore ──────────────────────────────────────────────────────── - - private bool TryRestoreSession() - { - var evmData = EvmXChainIdentity.TryLoadSessionData(); - if (evmData != null) - { - var evmIdentity = new EvmXChainIdentity(evmData.algorandAddress, evmData.evmAddress); - evmIdentity.UpdateTokens(evmData.sessionToken, evmData.refreshToken); - SetIdentity(evmIdentity); - BlockmakerLog.Info($"[BlockmakerAuth] EVM xChain session restored: {evmData.evmAddress}"); - // Do NOT TriggerWalletLogin inline here — the relay/connection isn't live yet. - // VerifyRestoredSession() refreshes/verifies any existing token; a fresh sign-in - // login (no-token case) is triggered later from TryReconnectWalletSessions once - // the connection is ready. - return true; - } - - foreach (var provider in new[] { "Pera", "Defly" }) - { - var data = WalletConnectIdentity.TryLoadSessionData(provider); - if (data != null) - { - var restoredIdentity = CreateWalletIdentity(provider, data.address); - if (restoredIdentity is WalletConnectIdentity restoredWc) - restoredWc.UpdateTokens(data.sessionToken, data.refreshToken); - SetIdentity(restoredIdentity); - BlockmakerLog.Info($"[BlockmakerAuth] {provider} session restored: {data.address}"); - - var wcv1Session = WalletConnectIdentity.TryLoadWCv1Session(provider); - if (wcv1Session != null) - { - CleanupWCv1(); - _wcv1Client = WalletConnectV1Client.FromSession(wcv1Session); - if (restoredIdentity is WalletConnectIdentity wcIdentity) - wcIdentity.OwnWCv1Client = _wcv1Client; - _wcv1Client.Reconnect().ContinueWith(t => - { - if (t.IsFaulted) BlockmakerLog.Warning($"[BlockmakerAuth] WCv1 reconnect failed: {t.Exception?.InnerException?.Message}"); - }, System.Threading.Tasks.TaskContinuationOptions.OnlyOnFaulted); - BlockmakerLog.Info($"[BlockmakerAuth] WCv1 client restored and reconnecting for {provider}"); - } - - // Do NOT TriggerWalletLogin inline here — the WCv1 relay is still reconnecting - // and Reown may not be initialized. VerifyRestoredSession() handles an existing - // token; a fresh sign-in login (no-token case) is triggered later from - // TryReconnectWalletSessions once the connection is confirmed ready. - return true; - } - } - - var magicIdentity = MagicIdentity.TryLoadSession(); - if (magicIdentity != null) - { - SetIdentity(magicIdentity); - BlockmakerLog.Info($"[BlockmakerAuth] Magic session restored: {magicIdentity.Email}"); - return true; - } - - var emailIdentity = EmailIdentity.TryLoadSession(); - if (emailIdentity != null) - { - SetIdentity(emailIdentity); - BlockmakerLog.Info($"[BlockmakerAuth] Email session restored: {emailIdentity.Email}"); - return true; - } - - return false; - } - - private void VerifyRestoredSession() - { - string token = null; - string refreshToken = null; - bool isWallet = false; - - if (Identity is EmailIdentity email) - { token = email.SessionToken; refreshToken = email.RefreshToken; } - else if (Identity is MagicIdentity magic) - { token = magic.SessionToken; refreshToken = magic.RefreshToken; } - else if (Identity is WalletConnectIdentity wc) - { token = wc.SessionToken; refreshToken = wc.RefreshToken; isWallet = true; } - else if (Identity is EvmXChainIdentity evm) - { token = evm.SessionToken; refreshToken = evm.RefreshToken; isWallet = true; } - - if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(refreshToken)) - { - // For wallet identities, "no tokens" is a normal restore state: the JWT is - // acquired by a fresh wallet-signature Login once the connection is confirmed - // ready (TryReconnectWalletSessions / OnWalletReconnectedFromJS). Leave the - // identity in place rather than downgrading to Guest. - if (isWallet) - { - BlockmakerLog.Info("[BlockmakerAuth] Restored wallet session has no JWT yet — will sign in once the connection is ready."); - // Settled as "no valid token right now" — a later relay-gated wallet login - // that lands a JWT announces itself via OnIdentityChanged (allowed upgrade). - SettleSessionRestore(); - return; - } - BlockmakerLog.Info("[BlockmakerAuth] Restored session has no tokens — clearing."); - SetIdentity(new GuestIdentity()); - SettleSessionRestore(); - return; - } - - var capturedIdentity = Identity; - - // If we have a refresh token, use it to get a fresh JWT immediately - if (!string.IsNullOrEmpty(refreshToken) && BlockmakerClient.Instance != null) - { - BlockmakerClient.Instance.RefreshToken(refreshToken, result => - { - // Every path below settles the restore. On the success path the settle must - // come AFTER UpdateTokens/SaveSession: settle subscribers may immediately fire - // token-authed requests (profile fetch), which would 401 on the stale token. - if (this == null) { SettleSessionRestore(); return; } - if (Identity != capturedIdentity) { SettleSessionRestore(); return; } - if (result != null && !string.IsNullOrEmpty(result.sessionToken)) - { - if (capturedIdentity is ServerSignedIdentity ss) ss.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is MagicIdentity m) m.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is WalletConnectIdentity cwc) cwc.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is EvmXChainIdentity cevm) cevm.UpdateTokens(result.sessionToken, result.refreshToken); - capturedIdentity.SaveSession(); - BlockmakerLog.Info("[BlockmakerAuth] Session token refreshed on restore."); - // Settle FIRST (fresh token is stored now), so OnIdentityChanged - // subscribers already read SessionRestoreSettled == true. - SettleSessionRestore(); - // The restore-time OnIdentityChanged fired BEFORE this fresh JWT existed, - // so session-gated consumers (balance tracker, profile manager) skipped - // their loads and are waiting for a re-fire that would otherwise never - // come on this path. Announce the now-valid session the same way a - // completed wallet-signature login does (see RunWalletLogin). - SafeInvoke(OnIdentityChanged, capturedIdentity); - } - else - { - // Refresh "succeeded" but returned no usable token — resolved either way. - SettleSessionRestore(); - } - }, err => - { - // Restore attempt resolved: the stored token could not be refreshed. Either a - // wallet re-sign (below) or a fresh user sign-in is required from here. - // Settled at the END of each path (after the identity is in its final state) - // so settle subscribers never act on the not-yet-downgraded identity. - if (this == null) { SettleSessionRestore(); return; } - if (Identity != capturedIdentity) { SettleSessionRestore(); return; } - // For wallet identities (WalletConnect/EVM xChain) a refresh failure does NOT - // mean the player must drop to Guest: the live wallet relay can re-sign for a - // fresh JWT. Clear only the stale tokens and leave the identity in place so the - // gated TriggerWalletLogin path (TryReconnectWalletSessions / reconnect / restore - // callbacks) re-acquires a JWT once the connection is ready. - // Email/Magic keep the original behavior: a failed refresh means re-login. - if (capturedIdentity is WalletConnectIdentity cwc) - { - BlockmakerLog.Info($"[BlockmakerAuth] Wallet token refresh failed — keeping identity, will re-sign when relay is ready: {err}"); - cwc.ClearTokens(); - cwc.SaveSession(); - // Re-sign immediately now that the stale token is cleared, rather than - // waiting on a later connection event (an adverse async ordering could - // otherwise leave the wallet connected-but-tokenless until reconnect). - // The F1 in-flight guard + the empty-token check inside TriggerWalletLogin - // prevent a duplicate prompt; tokens are now empty so it won't early-return. - TriggerWalletLogin(cwc); - } - else if (capturedIdentity is EvmXChainIdentity cevm) - { - BlockmakerLog.Info($"[BlockmakerAuth] Wallet token refresh failed — keeping identity, will re-sign when relay is ready: {err}"); - cevm.ClearTokens(); - cevm.SaveSession(); - // Re-sign immediately now that the stale token is cleared (see WC branch). - TriggerWalletLogin(cevm); - } - else - { - BlockmakerLog.Info($"[BlockmakerAuth] Refresh failed — clearing session: {err}"); - SetIdentity(new GuestIdentity()); - } - SettleSessionRestore(); - }); - return; - } - - // No refresh token — just verify the JWT - if (BlockmakerClient.Instance == null) - { - BlockmakerLog.Warning("[BlockmakerAuth] Cannot verify session — no server connection. Clearing session."); - SetIdentity(new GuestIdentity()); - SettleSessionRestore(); - return; - } - BlockmakerClient.Instance.VerifySessionToken(token, ok => - { - // Verified either way — the restore attempt is resolved. Settle AFTER the identity - // reaches its final state (the !ok downgrade), so settle subscribers never read a - // signed-in identity that is about to drop to Guest. - if (this == null) { SettleSessionRestore(); return; } - if (Identity != capturedIdentity) { SettleSessionRestore(); return; } - if (!ok) - { - BlockmakerLog.Info($"[BlockmakerAuth] {capturedIdentity.ProviderName} session expired — please sign in again."); - SetIdentity(new GuestIdentity()); - } - SettleSessionRestore(); - }); - } - - // ── Proactive token refresh ────────────────────────────────────────────── - - private void StartTokenRefreshTimer() - { - StopTokenRefreshTimer(); - _tokenRefreshCoroutine = StartCoroutine(TokenRefreshLoop()); - } - - private void StopTokenRefreshTimer() - { - if (_tokenRefreshCoroutine != null) - { - StopCoroutine(_tokenRefreshCoroutine); - _tokenRefreshCoroutine = null; - } - } - - private IEnumerator TokenRefreshLoop() - { - while (true) - { - yield return new WaitForSecondsRealtime(TOKEN_REFRESH_INTERVAL); - - string refreshToken = null; - if (Identity is ServerSignedIdentity e) refreshToken = e.RefreshToken; - else if (Identity is MagicIdentity m) refreshToken = m.RefreshToken; - else if (Identity is WalletConnectIdentity wc) refreshToken = wc.RefreshToken; - else if (Identity is EvmXChainIdentity evm) refreshToken = evm.RefreshToken; - - if (_isRefreshing && BlockmakerClient.Instance == null) - _isRefreshing = false; - - if (string.IsNullOrEmpty(refreshToken) || BlockmakerClient.Instance == null || _isRefreshing) - continue; - - _isRefreshing = true; - _lastRefreshTime = Time.realtimeSinceStartup; - var capturedIdentity = Identity; - BlockmakerClient.Instance.RefreshToken(refreshToken, result => - { - _isRefreshing = false; - if (this == null || Identity != capturedIdentity) return; - if (result == null || string.IsNullOrEmpty(result.sessionToken)) return; - - if (capturedIdentity is ServerSignedIdentity ce) ce.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is MagicIdentity cm) cm.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is WalletConnectIdentity cwc) cwc.UpdateTokens(result.sessionToken, result.refreshToken); - else if (capturedIdentity is EvmXChainIdentity cevm) cevm.UpdateTokens(result.sessionToken, result.refreshToken); - capturedIdentity.SaveSession(); - BlockmakerLog.Info("[BlockmakerAuth] Token proactively refreshed."); - }, err => - { - _isRefreshing = false; - BlockmakerLog.Warning($"[BlockmakerAuth] Proactive token refresh failed: {err}"); - SafeInvoke(OnAuthError, "Your session could not be refreshed. You may need to sign in again."); - }); - } - } - - private void TryReconnectWalletSessions() - { - if (_connector != null && _connector.IsInitialized) - { - var address = _connector.TryRestoreSession(); - if (address != null) - { - BlockmakerLog.Info($"[BlockmakerAuth] Native WC v2 session restored: {address}"); - // Connection is now ready. If the restored wallet identity has no JWT yet, - // run a fresh wallet-signature login now (gated behind connection-ready, not - // fired inline during TryRestoreSession). VerifyRestoredSession already - // refreshed/verified any existing token, so this no-ops when one is present. - TriggerWalletLogin(Identity); - return; - } - } - - #if UNITY_WEBGL && !UNITY_EDITOR - // Browser-held sessions (Pera JS lib / Magic / EVM) restore independently of - // Reown — see TryReconnectBrowserWalletSessions (idempotent; normally already - // fired from Start, this covers late callers). - TryReconnectBrowserWalletSessions(); - #else - // Native (non-WebGL): the WCv1 Pera path doesn't go through _connector.TryRestoreSession - // above. If a wallet identity restored without a JWT, run a fresh wallet-signature login - // now that we're past Reown init. Login() itself waits for the WCv1 relay to reconnect, - // and RunWalletLogin defers while any sign is in flight. No-ops if a token already exists. - if (Identity is WalletConnectIdentity || Identity is EvmXChainIdentity) - TriggerWalletLogin(Identity); - #endif - } - -#if UNITY_WEBGL && !UNITY_EDITOR - // Guard so the browser reconnect runs exactly once whether it fires from Start - // (Reown-independent) or from OnReownInitialized (the legacy trigger). - private bool _browserWalletReconnectAttempted; - - /// - /// Restore wallet sessions that live in the BROWSER, not in Reown: Pera's JS lib - /// (localStorage), Magic, and the EVM bridge. Historically this only ran from - /// OnReownInitialized — if Reown's init threw or hung (its failure path swallows - /// and never fires OnInitialized), a returning Pera player's JWT restored fine but - /// the Pera JS session was never re-attached, so their FIRST signature failed with - /// "Pera wallet not connected" until a manual reconnect. Pera needs nothing from - /// Reown, so this is also called directly from Start. - /// - private void TryReconnectBrowserWalletSessions() - { - if (_browserWalletReconnectAttempted) return; - _browserWalletReconnectAttempted = true; - - foreach (var provider in new[] { "Pera", "Defly" }) - { - BlockmakerWalletBridge.TryReconnect( - provider, - gameObject.name, - nameof(OnWalletReconnectedFromJS), - nameof(OnWalletReconnectFailed) - ); - } - - // Pera on WebGL sessions live in Pera's own JS library (localStorage). The jslib - // emits "Pera:
" — the same payload the generic reconnect receivers expect. - if (Identity is WalletConnectIdentity peraId && peraId.ProviderName == ProviderPera) - { - BlockmakerWalletBridge.PeraJsReconnect( - gameObject.name, - nameof(OnWalletReconnectedFromJS), - nameof(OnWalletReconnectFailed) - ); - } - - var cfg = BlockmakerClient.Instance?.config; - if (Identity is MagicIdentity && cfg != null && cfg.enableMagicEmail && !string.IsNullOrEmpty(cfg.magicPublishableKey)) - { - BlockmakerWalletBridge.MagicTryRestore( - cfg.magicPublishableKey, - gameObject.name, - nameof(OnMagicRestoreSuccess), - nameof(OnMagicRestoreError) - ); - } - - if (Identity is EvmXChainIdentity evm) - { - _evmRestoreCapturedIdentity = Identity; - BlockmakerWalletBridge.EvmTryRestore( - evm.EvmAddress, - gameObject.name, - nameof(OnEvmRestoreSuccess), - nameof(OnEvmRestoreError) - ); - } - } -#endif - - // ── Connect wallet (QR flow) ─────────────────────────────────────────────── - - /// - /// Begin a wallet connection. - /// - /// Pera: uses native WalletConnect v1 on all platforms. - /// Defly: uses Reown SDK (WalletConnect v2); falls back to JS bridge on WebGL. - /// - /// OnWalletQRReady fires with the QR code for display. - /// onSuccess / OnIdentityChanged fire when the user approves. - /// - public void ConnectWallet( - string provider, - Action onSuccess = null, - Action onError = null) - { - if (_isWalletConnecting || IsAuthenticating) - { - onError?.Invoke(_isWalletConnecting - ? "A wallet connection is already in progress. Please wait." - : "Another sign-in is already in progress. Please wait."); - return; - } - - if (!provider.Equals(ProviderPera, StringComparison.OrdinalIgnoreCase) && - !provider.Equals(ProviderDefly, StringComparison.OrdinalIgnoreCase)) - { - BlockmakerLog.Error($"[BlockmakerAuth] Unknown wallet provider '{provider}'. Supported: \"{ProviderPera}\", \"{ProviderDefly}\"."); - onError?.Invoke($"Unknown wallet provider \"{provider}\". Please use Pera or Defly."); - return; - } - - IsAuthenticating = true; - _isWalletConnecting = true; - _pendingConnectSuccess = onSuccess; - _pendingConnectError = onError; - - if (provider.Equals(ProviderPera, StringComparison.OrdinalIgnoreCase)) - { - #if UNITY_WEBGL && !UNITY_EDITOR - // Pera speaks WalletConnect v1 ONLY (Pera-founder-confirmed) — never route - // it through the WC v2 paths (Reown / the in-house jslib client): the app - // cannot pair their QR codes. On WebGL we use Pera's official JS library - // HEADLESS: its DOM modal is suppressed (browser fullscreen would hide it — - // sign-in must never leave fullscreen) and the v1 URI is sent to Unity for - // the usual in-canvas QR. - BlockmakerWalletBridge.PeraJsConnect( - gameObject.name, - nameof(OnPeraJsConnected), - nameof(OnPeraJsError), - nameof(OnWalletQRFromJS) // v1 URI → the usual in-canvas QR pipeline - ); - StartWebGLTimeout(WalletSignTimeout, () => - { - if (_isWalletConnecting) FailWalletConnection("Connection timed out. Please try again."); - }); - #else - _peraConnectCoroutine = StartCoroutine(PeraNativeWCv1Flow()); - #endif - return; - } - - if (_connector != null && _connector.IsInitialized) - { - _connector.ConnectAlgorand( - provider, - onConnected: (prov, address) => CompleteWalletConnection(prov, address), - onError: err => FailWalletConnection(err) - ); - return; - } - - #if UNITY_WEBGL && !UNITY_EDITOR - if (string.IsNullOrEmpty(ResolvedWalletConnectProjectId)) - { - BlockmakerLog.Error("[BlockmakerAuth] WalletConnect Project ID is not set. Set it on BlockmakerConfig (https://cloud.walletconnect.com)."); - _isWalletConnecting = false; - IsAuthenticating = false; - onError?.Invoke("Wallet connection is not available right now. Please try again later."); - return; - } - - BlockmakerWalletBridge.ConnectWalletQR( - ResolvedWalletConnectProjectId, - provider, - gameObject.name, - nameof(OnWalletQRFromJS), - nameof(OnWalletConnectedFromJS), - nameof(OnWalletErrorFromJS) - ); - StartWebGLTimeout(WalletSignTimeout, () => - { - if (_isWalletConnecting) FailWalletConnection("Connection timed out. Please try again."); - }); - #else - _isWalletConnecting = false; - IsAuthenticating = false; - onError?.Invoke("Wallet connection is not available. Please restart the game and try again."); - #endif - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnWalletQRFromJS(string payload) - { - int firstPipe = payload.IndexOf('|'); - if (firstPipe < 0) return; - var provider = payload.Substring(0, firstPipe); - var rest = payload.Substring(firstPipe + 1); - - int secondPipe = rest.IndexOf('|'); - if (secondPipe < 0) return; - var wcUri = rest.Substring(0, secondPipe); - var qrB64 = rest.Substring(secondPipe + 1); - - BlockmakerLog.Info($"[BlockmakerAuth] QR ready for {provider}"); - SafeInvoke(OnWalletQRReady, new WalletQREventArgs(provider, wcUri, qrB64)); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnWalletConnectedFromJS(string payload) - { - CancelWebGLTimeout(); - if (!_isWalletConnecting) - { - BlockmakerLog.Warning("[BlockmakerAuth] Ignoring unexpected wallet connection callback."); - return; - } - - var parts = payload.Split(new[] { ':' }, 2); - var provider = parts.Length > 1 ? parts[0] : "Unknown"; - var address = parts.Length > 1 ? parts[1] : payload; - - CompleteWalletConnection(provider, address); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnWalletReconnectedFromJS(string payload) - { - var parts = payload.Split(new[] { ':' }, 2); - var provider = parts.Length > 1 ? parts[0] : "Unknown"; - var address = parts.Length > 1 ? parts[1] : payload; - - if (Identity is WalletConnectIdentity existing && existing.Address == address) - { - BlockmakerLog.Info($"[BlockmakerAuth] WebGL wallet reconnected: {provider} {address}"); - // The relay is now confirmed live. If this restored identity still has no JWT, - // this is the correct gated moment to run a fresh wallet-signature login. - // TriggerWalletLogin no-ops when a token already exists. - if (string.IsNullOrEmpty(existing.SessionToken)) - TriggerWalletLogin(existing); - return; - } - - if (Identity != null && !(Identity is WalletConnectIdentity) && !(Identity is GuestIdentity)) - { - BlockmakerLog.Info($"[BlockmakerAuth] Ignoring wallet reconnect — current identity is {Identity.ProviderName}"); - return; - } - - var identity = CreateWalletIdentity(provider, address); - SetIdentity(identity); - identity.SaveSession(); - TriggerWalletLogin(identity); - BlockmakerLog.Info($"[BlockmakerAuth] WebGL wallet session restored via reconnect: {provider} {address}"); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnWalletReconnectFailed(string error) - { - BlockmakerLog.Warning($"[BlockmakerAuth] WebGL wallet reconnect failed: {error}"); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnWalletErrorFromJS(string error) - { - CancelWebGLTimeout(); - FailWalletConnection(error); - } - - // ── Pera official JS SDK callbacks (WebGL Pera path) ───────────────────── - - /// - /// Success callback for BlockmakerWalletBridge.PeraJsConnect (WebGL only). - /// Receives the bare Algorand address — Pera's own modal handled the - /// QR / deep-link UX, so this feeds straight into the same post-connect - /// funnel as the other wallets (identity → SaveSession → TriggerWalletLogin). - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnPeraJsConnected(string address) - { - CancelWebGLTimeout(); - if (!_isWalletConnecting) - { - BlockmakerLog.Warning("[BlockmakerAuth] Ignoring unexpected Pera JS connection callback."); - return; - } - - CompleteWalletConnection(ProviderPera, address); - } - - /// Error callback for BlockmakerWalletBridge.PeraJsConnect (WebGL only). - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnPeraJsError(string error) - { - CancelWebGLTimeout(); - if (!_isWalletConnecting) - { - BlockmakerLog.Warning($"[BlockmakerAuth] Ignoring Pera JS error after connect ended: {error}"); - return; - } - - // Recognizable code sent by the jslib when the user simply closed Pera's modal. - if (error == "PERA_CONNECT_CANCELLED") - error = "The connection was cancelled. Please try again."; - - FailWalletConnection(error); - } - - // ── Pera native WalletConnect v1 ────────────────────────────────────────── - - private IEnumerator PeraNativeWCv1Flow() - { - CleanupWCv1(); - var dAppUrl = blockmakerConfig?.dAppUrl; - if (string.IsNullOrEmpty(dAppUrl)) dAppUrl = blockmakerConfig?.serverUrl; - if (string.IsNullOrEmpty(dAppUrl)) dAppUrl = BlockmakerConfig.DefaultServerUrl; - - _wcv1Client = new WalletConnectV1Client( - chainId: 4160, - appName: Application.productName, - appDescription: Application.productName, - appUrl: dAppUrl - ); - - string wcUri = _wcv1Client.Uri; - _wcv1Address = null; - _wcv1Error = null; - _wcv1Done = false; - - _wcv1Client.OnSessionApproved += addr => { _wcv1Address = addr; _wcv1Done = true; }; - _wcv1Client.OnSessionRejected += err => { _wcv1Error = err; _wcv1Done = true; }; - _wcv1Client.OnError += err => { _wcv1Error = err; _wcv1Done = true; }; - - // Show QR immediately — the URI is known before connecting - var qrTexture = QRTextureGenerator.Generate(wcUri, 512); - _peraQRTexture = qrTexture; - ReownWalletConnector.FireQRReadyForBridge("Pera", wcUri, qrTexture); - - BlockmakerLog.Info($"[BlockmakerAuth] Pera WCv1 connecting to bridge..."); - - var connectTask = _wcv1Client.Connect(); - while (!connectTask.IsCompleted) - yield return null; - - if (connectTask.IsFaulted) - { - _peraConnectCoroutine = null; - if (qrTexture != null) { Destroy(qrTexture); _peraQRTexture = null; } - string msg = connectTask.Exception?.InnerException?.Message ?? "Could not connect to the Pera wallet service. Please check your internet connection and try again."; - FailWalletConnection(msg); - CleanupWCv1(); - yield break; - } - - float elapsed = 0f; - float TIMEOUT = WalletSignTimeout; - - float lastCheck = Time.realtimeSinceStartup; - while (!_wcv1Done && elapsed < TIMEOUT && _isWalletConnecting) - { - yield return new WaitForSecondsRealtime(0.25f); - float now = Time.realtimeSinceStartup; - elapsed += now - lastCheck; - lastCheck = now; - } - - _peraConnectCoroutine = null; - if (qrTexture != null) { Destroy(qrTexture); _peraQRTexture = null; } - - if (!string.IsNullOrEmpty(_wcv1Address)) - { - BlockmakerLog.Info($"[BlockmakerAuth] Pera WCv1 connected: {_wcv1Address}"); - - if (_wcv1Client != null) - { - var sd = _wcv1Client.GetSessionData(); - if (sd != null) - { - SecurePrefs.SetString(BlockmakerPrefs.Key("wc_session_pera_wcv1"), JsonUtility.ToJson(sd)); - SecurePrefs.Save(); - } - } - - CompleteWalletConnection("Pera", _wcv1Address); - } - else if (!string.IsNullOrEmpty(_wcv1Error)) - { - FailWalletConnection(_wcv1Error); - CleanupWCv1(); - } - else - { - FailWalletConnection("Connection timed out. Please try again."); - CleanupWCv1(); - } - } - - private void CleanupWCv1() - { - if (_peraQRTexture != null) { Destroy(_peraQRTexture); _peraQRTexture = null; } - if (_wcv1Client != null) - { - if (Identity is WalletConnectIdentity wcIdentity && wcIdentity.OwnWCv1Client == _wcv1Client) - wcIdentity.OwnWCv1Client = null; - _wcv1Client.Dispose(); - _wcv1Client = null; - } - } - - private void CompleteWalletConnection(string provider, string address) - { - try - { - var prevTier = Tier; - var identity = CreateWalletIdentity(provider, address); - if (identity is WalletConnectIdentity wcIdentity && _wcv1Client != null) - wcIdentity.OwnWCv1Client = _wcv1Client; - - var successCb = _pendingConnectSuccess; - _pendingConnectSuccess = null; - _pendingConnectError = null; - _isWalletConnecting = false; - IsAuthenticating = false; - - SetIdentity(identity); - identity.SaveSession(); - TriggerWalletLogin(identity); - - if (prevTier < IdentityTier.SelfCustody) - SafeInvoke(OnIdentityUpgraded, identity, prevTier); - - successCb?.Invoke(identity); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[BlockmakerAuth] Wallet connection error: {ex.Message}"); - FailWalletConnection("Something went wrong while connecting. Please try again."); - } - } - - private void FailWalletConnection(string error) - { - BlockmakerLog.Error($"[BlockmakerAuth] Wallet error: {error}"); - SafeInvoke(OnAuthError, error); - _pendingConnectError?.Invoke(error); - _pendingConnectError = null; - _pendingConnectSuccess = null; - _isWalletConnecting = false; - IsAuthenticating = false; - } - - public void CancelWalletConnect() - { - if (!_isWalletConnecting) return; - CancelWebGLTimeout(); - if (_peraQRTexture != null) { Destroy(_peraQRTexture); _peraQRTexture = null; } - if (_peraConnectCoroutine != null) - { - StopCoroutine(_peraConnectCoroutine); - _peraConnectCoroutine = null; - } - CleanupWCv1(); - _connector?.CancelConnection(); - IsAuthenticating = false; - _pendingConnectSuccess = null; - _pendingConnectError = null; - _isWalletConnecting = false; - #if UNITY_WEBGL && !UNITY_EDITOR - BlockmakerWalletBridge.CancelWalletQR(); - #endif - } - - private void StartWebGLTimeout(float seconds, Action onTimeout) - { - CancelWebGLTimeout(); - _webglTimeoutCoroutine = StartCoroutine(WebGLTimeoutRoutine(seconds, onTimeout)); - } - - private void CancelWebGLTimeout() - { - if (_webglTimeoutCoroutine != null) - { - StopCoroutine(_webglTimeoutCoroutine); - _webglTimeoutCoroutine = null; - } - } - - private IEnumerator WebGLTimeoutRoutine(float seconds, Action onTimeout) - { - yield return new WaitForSecondsRealtime(seconds); - _webglTimeoutCoroutine = null; - onTimeout?.Invoke(); - } - - // ── Magic email login ──────────────────────────────────────────────────── - - /// - /// Start a Magic SDK email login. Magic handles its own OTP verification UI. - /// On success the DID token is sent to our server for JWT issuance. - /// - public void ConnectMagicEmail( - string email, - Action onSuccess = null, - Action onError = null) - { - if (IsAuthenticating || _pendingMagicSuccess != null || _pendingMagicError != null) - { - onError?.Invoke("Another sign-in is already in progress. Please wait."); - return; - } - - IsAuthenticating = true; - - var cfg = BlockmakerClient.Instance?.config; - if (cfg == null || !cfg.enableMagicEmail || string.IsNullOrEmpty(cfg.magicPublishableKey)) - { - IsAuthenticating = false; - BlockmakerLog.Error("[BlockmakerAuth] Magic publishable key not configured. Set it on the BlockmakerConfig asset."); - onError?.Invoke("Email sign-in is not available right now. Please try again later."); - return; - } - - #if UNITY_WEBGL && !UNITY_EDITOR - _pendingMagicSuccess = onSuccess; - _pendingMagicError = onError; - - BlockmakerWalletBridge.MagicLoginWithEmail( - cfg.magicPublishableKey, - email, - gameObject.name, - nameof(OnMagicLoginSuccess), - nameof(OnMagicLoginError) - ); - StartWebGLTimeout(WalletSignTimeout, () => - { - if (_pendingMagicSuccess != null || _pendingMagicError != null) - OnMagicLoginError("Sign-in timed out. Please try again."); - }); - #else - IsAuthenticating = false; - onError?.Invoke("Email sign-in is only available when playing in a web browser."); - #endif - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnMagicLoginSuccess(string payload) - { - CancelWebGLTimeout(); - if (_pendingMagicSuccess == null && _pendingMagicError == null) - return; - - var parts = payload.Split(new[] { '|' }, 4); - if (parts.Length < 4) - { - OnMagicLoginError("Sign-in could not be completed. Please try again."); - return; - } - var address = parts[1]; - var email = parts[2]; - var didToken = parts[3]; - - _magicLoginCoroutine = StartCoroutine(FinishMagicLogin(address, email, didToken)); - } - - private IEnumerator FinishMagicLogin(string address, string email, string didToken) - { - if (BlockmakerClient.Instance == null) - { - OnMagicLoginError("Unable to reach the server. Please check your connection and try again."); - yield break; - } - - string jwt = null; - string refreshTok = null; - string error = null; - - try - { - yield return BlockmakerClient.Instance.VerifyMagicToken( - didToken, email, - result => { jwt = result.sessionToken; refreshTok = result.refreshToken; }, - err => { error = err; } - ); - } - finally - { - _magicLoginCoroutine = null; - } - - if (error != null) - { - BlockmakerLog.Error($"[BlockmakerAuth] Magic server verify failed: {error}"); - SafeInvoke(OnAuthError, "Sign-in could not be completed. Please try again."); - _pendingMagicError?.Invoke("Sign-in could not be completed. Please try again."); - _pendingMagicError = null; - _pendingMagicSuccess = null; - IsAuthenticating = false; - yield break; - } - - if (_pendingMagicSuccess == null && _pendingMagicError == null) - yield break; - - try - { - var prevTier = Tier; - var identity = new MagicIdentity(email, address, jwt, refreshTok); - SetIdentity(identity); - identity.SaveSession(); - - if (prevTier < IdentityTier.Email) - SafeInvoke(OnIdentityUpgraded, identity, prevTier); - - IsAuthenticating = false; - _pendingMagicSuccess?.Invoke(identity); - _pendingMagicSuccess = null; - _pendingMagicError = null; - } - catch (Exception ex) - { - BlockmakerLog.Error($"[BlockmakerAuth] Magic login error: {ex.Message}"); - OnMagicLoginError("Something went wrong while signing in. Please try again."); - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnMagicLoginError(string error) - { - CancelWebGLTimeout(); - if (_pendingMagicSuccess == null && _pendingMagicError == null) - return; - - BlockmakerLog.Error($"[BlockmakerAuth] Magic login error: {error}"); - SafeInvoke(OnAuthError, error); - _pendingMagicError?.Invoke(error); - _pendingMagicError = null; - _pendingMagicSuccess = null; - IsAuthenticating = false; - } - - public void CancelPendingMagic() - { - if (_magicLoginCoroutine != null) - { - StopCoroutine(_magicLoginCoroutine); - _magicLoginCoroutine = null; - } - _pendingMagicSuccess = null; - _pendingMagicError = null; - IsAuthenticating = false; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnMagicRestoreSuccess(string payload) - { - var parts = payload.Split('|'); - if (parts.Length < 3) return; - BlockmakerLog.Info($"[BlockmakerAuth] Magic JS session confirmed active for {parts[2]}"); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnMagicRestoreError(string error) - { - if (Identity is MagicIdentity magic) - { - BlockmakerLog.Info("[BlockmakerAuth] Magic JS session expired — verifying server JWT…"); - if (string.IsNullOrEmpty(magic.SessionToken)) - { - Identity.ClearSession(); - SetIdentity(new GuestIdentity()); - return; - } - if (BlockmakerClient.Instance == null) - { - BlockmakerLog.Warning("[BlockmakerAuth] Cannot verify Magic JWT — no server connection. Clearing session."); - Identity.ClearSession(); - SetIdentity(new GuestIdentity()); - return; - } - var capturedIdentity = Identity; - BlockmakerClient.Instance.VerifySessionToken(magic.SessionToken, ok => - { - if (this == null) return; - if (Identity != capturedIdentity) return; - if (!ok) - { - BlockmakerLog.Info("[BlockmakerAuth] Server JWT also invalid — clearing session."); - Identity.ClearSession(); - SetIdentity(new GuestIdentity()); - } - else - { - BlockmakerLog.Info("[BlockmakerAuth] Server JWT still valid — keeping Magic identity."); - } - }); - } - } - - // ── EVM session restore callbacks ────────────────────────────────────────── - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnEvmRestoreSuccess(string payload) - { - _evmRestoreCapturedIdentity = null; - // payload is the raw EVM address ("0x…") — the jslib already verified it - // matches the expected address passed to EvmTryRestore. - if (string.IsNullOrEmpty(payload)) return; - BlockmakerLog.Info($"[BlockmakerAuth] EVM wallet reconnected: {payload}"); - // Provider confirmed live. If the restored EVM identity still has no JWT, run a - // fresh wallet-signature login now (the gated, connection-ready moment). No-ops if - // a token already exists (VerifyRestoredSession refreshes/verifies any existing one). - if (Identity is EvmXChainIdentity evm && string.IsNullOrEmpty(evm.SessionToken)) - TriggerWalletLogin(evm); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnEvmRestoreError(string error) - { - if (Identity != _evmRestoreCapturedIdentity) { _evmRestoreCapturedIdentity = null; return; } - _evmRestoreCapturedIdentity = null; - if (Identity is EvmXChainIdentity) - { - BlockmakerLog.Warning($"[BlockmakerAuth] EVM restore failed: {error} — clearing session."); - Identity.ClearSession(); - SetIdentity(new GuestIdentity()); - } - } - - // ── EVM xChain ───────────────────────────────────────────────────────────── - - /// - /// Connect an EVM wallet via xChain Accounts. Derives an Algorand LogicSig address. - /// This is the NO-PICKER path (auto-pick: last-used wallet → first announced → - /// window.ethereum) — the wallet-picker UI uses + - /// instead. onError receives a bare message here - /// (unchanged legacy shape). - /// - public void ConnectEvm( - Action onSuccess = null, - Action onError = null) - { - if (IsAuthenticating || _pendingEvmSuccess != null || _pendingEvmError != null) - { - onError?.Invoke("Another sign-in is already in progress. Please wait."); - return; - } - - IsAuthenticating = true; - _pendingEvmSuccess = onSuccess; - _pendingEvmError = onError; - _evmConnectErrorsIncludeCode = false; // legacy path: bare-message errors - _evmExpectedRdns = null; // auto-pick: accept whichever wallet connects - - #if UNITY_WEBGL && !UNITY_EDITOR - // Zero-bundle browser path: discover the installed EVM wallets first - // (EIP-6963), then connect. OnEvmWalletsDiscovered picks the wallet and - // calls EvmConnect; the timeout below covers the whole chain. - // _evmConnectDispatched guards the discover→connect hop: a cancel + - // immediate reconnect can leave a STALE discovery callback in flight, - // and without the guard both callbacks would call EvmConnect → duplicate - // eth_requestAccounts popups. - _evmConnectDispatched = false; - BlockmakerWalletBridge.EvmDiscoverWallets( - gameObject.name, - nameof(OnEvmWalletsDiscovered) - ); - StartWebGLTimeout(WalletSignTimeout, () => - { - if (_pendingEvmSuccess != null || _pendingEvmError != null) - OnEvmError("Connection timed out. Please try again."); - }); - #else - if (_connector == null || !_connector.IsInitialized) - { - IsAuthenticating = false; - _pendingEvmSuccess = null; - _pendingEvmError = null; - onError?.Invoke("Wallet connection is not ready yet. Please try again in a moment."); - return; - } - - // OnEvmConnected receives the raw EVM address and derives the Algorand - // LogicSig address in C# — the same funnel the WebGL bridge feeds. - _connector.ConnectEvm( - evmAddr => OnEvmConnected(evmAddr), - err => OnEvmError(err) - ); - #endif - } - - /// - /// Callback for BlockmakerWalletBridge.EvmDiscoverWallets on the ConnectEvm - /// AUTO-CONNECT path (WebGL only). Payload: JSON - /// {"wallets":[{"rdns","name","icon","lastUsed"},…],"legacy":bool} — or the - /// sentinel "!none" when no EVM provider is installed at all. This path - /// ignores the wallet list and auto-picks via EvmConnect(""); the picker UI - /// uses the separate DiscoverEvmWallets / OnEvmWalletsDiscoveredForUi / - /// ConnectEvmWallet API below instead. - /// - // True once the current connect attempt has dispatched EvmConnect — stale - // discovery callbacks (from a cancelled attempt) must not dispatch a second. - private bool _evmConnectDispatched; - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnEvmWalletsDiscovered(string payload) - { - if (_pendingEvmSuccess == null && _pendingEvmError == null) - return; // the connect was cancelled while discovery ran - - if (_evmConnectDispatched) - return; // a discovery callback already advanced this attempt to connect - - if (payload == "!none") - { - OnEvmError("No EVM wallet found. Please install one to continue."); - return; - } - - // Do NOT log the payload itself — it now carries base64 icon bytes. - BlockmakerLog.Info($"[BlockmakerAuth] EVM wallets discovered ({(payload?.Length ?? 0)} chars) — auto-connecting."); - - _evmConnectDispatched = true; - BlockmakerWalletBridge.EvmConnect( - "", // auto-pick: last-used rdns (localStorage) → first announced → window.ethereum - gameObject.name, - nameof(OnEvmConnected), - nameof(OnEvmError) - ); - } - - // ── EVM wallet picker (discover / connect split) ─────────────────────────── - // The picker UI drives these two calls: DiscoverEvmWallets → show the list → - // ConnectEvmWallet(chosen rdns). ConnectEvm above stays the no-picker - // auto-pick path for API compatibility. - - // Single pending slot for the UI discovery callback (last-wins): a newer - // DiscoverEvmWallets call replaces the callback; the first jslib response - // consumes the slot and any later stale response finds it empty and is dropped. - private Action _pendingEvmDiscoverForUi; - - // True while the CURRENT EVM connect attempt came from ConnectEvmWallet (the - // picker path): OnEvmError then forwards "code|message" to the pending error - // callback so the UI can branch on EIP-1193 codes. ConnectEvm (the legacy - // auto-pick path) resets it so its callers keep receiving the bare message. - private bool _evmConnectErrorsIncludeCode; - - /// - /// Discover installed EVM wallets for a picker UI. onResult receives the RAW - /// jslib payload: - /// JSON — {"wallets":[{"rdns","name","icon","lastUsed"},…],"legacy":bool} - /// (icon = base64 96x96 PNG, no data: prefix, "" if unavailable; lastUsed - /// marks the last-used wallet; legacy = a window.ethereum provider exists) - /// — or the sentinel "!none" when no EVM provider is available at all. - /// On non-WebGL platforms onResult fires immediately with - /// {"wallets":[],"legacy":false,"native":true} — the UI should skip the - /// picker and connect via the native (Reown) flow. - /// Discovery is passive (no wallet popup) and does not touch IsAuthenticating. - /// - public void DiscoverEvmWallets(Action onResult) - { - #if UNITY_WEBGL && !UNITY_EDITOR - _pendingEvmDiscoverForUi = onResult; // single slot, last-wins - BlockmakerWalletBridge.EvmDiscoverWallets( - gameObject.name, - nameof(OnEvmWalletsDiscoveredForUi) - ); - #else - onResult?.Invoke("{\"wallets\":[],\"legacy\":false,\"native\":true}"); - #endif - } - - /// - /// Receiver for DiscoverEvmWallets (picker path) — deliberately separate from - /// OnEvmWalletsDiscovered, which belongs to the ConnectEvm auto-connect flow - /// and dispatches a connect on arrival. This one only relays the payload. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnEvmWalletsDiscoveredForUi(string payload) - { - var cb = _pendingEvmDiscoverForUi; - _pendingEvmDiscoverForUi = null; - cb?.Invoke(payload); - } - - /// - /// Connect the SPECIFIC EVM wallet chosen in the picker — like ConnectEvm but - /// skips discovery and passes rdns straight to the bridge ("" = auto-pick). - /// A picked rdns that is no longer available FAILS (no silent fallback). - /// onError always receives "code|message": code is the wallet's numeric - /// EIP-1193 / JSON-RPC error code when it supplied one (e.g. - /// "4001|User rejected the request.") and "" otherwise — including timeouts, - /// busy-guard rejections and native-path errors, e.g. "|Connection timed out. - /// Please try again.". On non-WebGL platforms this falls back to the - /// ConnectEvm (Reown) behavior and rdns is ignored. - /// - public void ConnectEvmWallet( - string rdns, - Action onSuccess = null, - Action onError = null) - { - if (IsAuthenticating || _pendingEvmSuccess != null || _pendingEvmError != null) - { - onError?.Invoke("|Another sign-in is already in progress. Please wait."); - return; - } - - IsAuthenticating = true; - _pendingEvmSuccess = onSuccess; - _pendingEvmError = onError; - _evmConnectErrorsIncludeCode = true; // picker path: "code|message" errors - // The user picked THIS wallet — a stale success echoing any other rdns - // (an earlier cancelled attempt's still-open popup getting approved) - // must be dropped, not logged in. Null = accept any (auto-pick paths). - _evmExpectedRdns = string.IsNullOrEmpty(rdns) ? null : rdns; - - #if UNITY_WEBGL && !UNITY_EDITOR - // Straight to connect — discovery already ran for the picker. Mark the - // discover→connect hop as already done so a STALE discovery callback - // (from an earlier cancelled ConnectEvm) can't dispatch a second - // EvmConnect and race this one with a duplicate eth_requestAccounts popup. - _evmConnectDispatched = true; - BlockmakerWalletBridge.EvmConnect( - rdns ?? "", - gameObject.name, - nameof(OnEvmConnected), - nameof(OnEvmError) - ); - StartWebGLTimeout(WalletSignTimeout, () => - { - if (_pendingEvmSuccess != null || _pendingEvmError != null) - OnEvmError("Connection timed out. Please try again."); - }); - #else - if (_connector == null || !_connector.IsInitialized) - { - IsAuthenticating = false; - _pendingEvmSuccess = null; - _pendingEvmError = null; - onError?.Invoke("|Wallet connection is not ready yet. Please try again in a moment."); - return; - } - - // Native fallback = ConnectEvm behavior: Reown has no rdns concept, so the - // chosen rdns is ignored and the same OnEvmConnected/OnEvmError funnel runs. - _connector.ConnectEvm( - evmAddr => OnEvmConnected(evmAddr), - err => OnEvmError(err) - ); - #endif - } - - public void CancelEvmConnect() - { - if (_pendingEvmSuccess == null && _pendingEvmError == null) return; - _connector?.CancelConnection(); - _pendingEvmSuccess = null; - _pendingEvmError = null; - IsAuthenticating = false; - #if UNITY_WEBGL && !UNITY_EDITOR - BlockmakerWalletBridge.CancelWalletQR(); - #endif - } - - // rdns the CURRENT connect attempt expects the bridge to echo back; null = - // accept any (auto-pick / native). Guards the cancel-then-repick race: wallet - // A's still-open popup approved during attempt B must not become identity A. - private string _evmExpectedRdns; - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnEvmConnected(string payload) - { - if (_pendingEvmSuccess == null && _pendingEvmError == null) - return; - - // WebGL bridge payload is "rdns|0xEvmAddress" (rdns '' for the legacy - // window.ethereum fallback); the native Reown connector sends a bare - // "0x…" address. Split on the first '|' when present. - string echoedRdns = null; - var evmAddress = payload; - int sep = payload?.IndexOf('|') ?? -1; - if (sep >= 0) - { - echoedRdns = payload.Substring(0, sep); - evmAddress = payload.Substring(sep + 1); - } - - // Stale-success guard: this success is for a DIFFERENT wallet than the - // current attempt picked — an earlier cancelled attempt's popup was - // approved late. Ignore it (the current attempt keeps waiting on its own - // callback); consuming it would log the player into the wrong wallet. - if (_evmExpectedRdns != null && echoedRdns != null && echoedRdns != _evmExpectedRdns) - { - BlockmakerLog.Warning( - $"[BlockmakerAuth] Ignoring stale EVM connect success from '{echoedRdns}' — the current attempt expects '{_evmExpectedRdns}'."); - return; - } - - CancelWebGLTimeout(); - - // The Algorand LogicSig address is derived here in C# (byte-proven - // against the on-chain LogicSig derivation). - if (string.IsNullOrEmpty(evmAddress) || - !evmAddress.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - { - OnEvmError("Something went wrong during wallet connection. Please try again."); - return; - } - - try - { - var algoAddr = XChainAddressDeriver.DeriveAlgorandAddress(evmAddress); - var prevTier = Tier; - var identity = new EvmXChainIdentity(algoAddr, evmAddress); - SetIdentity(identity); - identity.SaveSession(); - TriggerWalletLogin(identity); - - if (prevTier < IdentityTier.SelfCustody) - SafeInvoke(OnIdentityUpgraded, identity, prevTier); - - IsAuthenticating = false; - _pendingEvmSuccess?.Invoke(identity); - _pendingEvmSuccess = null; - _pendingEvmError = null; - } - catch (Exception ex) - { - BlockmakerLog.Error($"[BlockmakerAuth] EVM connection error: {ex.Message}"); - OnEvmError("Something went wrong while connecting. Please try again."); - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnEvmError(string error) - { - CancelWebGLTimeout(); - if (_pendingEvmSuccess == null && _pendingEvmError == null) - return; - - // The jslib's EvmConnect errors arrive as "code|message" (numeric EIP-1193 - // code, or empty). Internal, timeout and native-connector errors are bare - // messages. Normalize: humans (log + OnAuthError) always get the message - // alone; the pending callback gets "code|message" on the picker path - // (ConnectEvmWallet) and the bare message on the legacy ConnectEvm path. - ParseEvmConnectError(error, out var code, out var message); - BlockmakerLog.Error($"[BlockmakerAuth] EVM connect error: {(string.IsNullOrEmpty(code) ? message : code + " " + message)}"); - SafeInvoke(OnAuthError, message); - _pendingEvmError?.Invoke(_evmConnectErrorsIncludeCode ? code + "|" + message : message); - _pendingEvmError = null; - _pendingEvmSuccess = null; - IsAuthenticating = false; - } - - /// - /// Split a "code|message" EVM connect-error payload (the jslib EvmConnect - /// error shape). The prefix before the FIRST '|' is accepted as the code only - /// when it is empty or an integer (optionally negative — JSON-RPC codes like - /// -32002); otherwise the '|' belonged to a bare message, which is returned - /// whole with code "". Payloads without any '|' are bare messages too. - /// - private static void ParseEvmConnectError(string payload, out string code, out string message) - { - code = ""; - message = payload ?? ""; - if (string.IsNullOrEmpty(payload)) return; - - int pipe = payload.IndexOf('|'); - if (pipe < 0) return; - - var prefix = payload.Substring(0, pipe); - if (prefix == "-") return; // a lone minus is not a code - for (int i = 0; i < prefix.Length; i++) - { - char c = prefix[i]; - if (!(char.IsDigit(c) || (c == '-' && i == 0))) return; - } - - code = prefix; - message = payload.Substring(pipe + 1); - } - - // ── Legacy email login (server-managed wallet) ──────────────────────────── - - private Coroutine _otpRequestCoroutine; - - public void RequestEmailOTP(string email, Action onSent, Action onError) - { - if (IsAuthenticating) - { - onError?.Invoke("Another sign-in is already in progress. Please wait."); - return; - } - if (_otpRequestCoroutine != null) - { - onError?.Invoke("A code request is already in progress. Please wait."); - return; - } - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Unable to reach the server. Please check your connection and try again."); - return; - } - _otpRequestCoroutine = StartCoroutine(RequestEmailOTPGuarded(email, onSent, onError)); - } - - private System.Collections.IEnumerator RequestEmailOTPGuarded(string email, Action onSent, Action onError) - { - try - { - yield return BlockmakerClient.Instance.RequestEmailOTP(email, onSent, onError); - } - finally - { - _otpRequestCoroutine = null; - } - } - - public void VerifyEmailOTP( - string email, - string otp, - Action onSuccess, - Action onError) - { - if (IsAuthenticating || _emailOtpCoroutine != null) - { - onError?.Invoke("Another sign-in is already in progress. Please wait."); - return; - } - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Unable to reach the server. Please check your connection and try again."); - return; - } - - IsAuthenticating = true; - _emailOtpCoroutine = StartCoroutine(VerifyEmailOTPRoutine(email, otp, onSuccess, onError)); - } - - private IEnumerator VerifyEmailOTPRoutine( - string email, - string otp, - Action onSuccess, - Action onError) - { - if (BlockmakerClient.Instance == null) - { - _emailOtpCoroutine = null; - IsAuthenticating = false; - onError?.Invoke("Unable to reach the server. Please check your connection and try again."); - yield break; - } - - EmailIdentity identity = null; - string error = null; - - try - { - yield return BlockmakerClient.Instance.VerifyEmailOTP( - email, otp, - result => { identity = new EmailIdentity(email, result.walletAddress, result.sessionToken, result.refreshToken); }, - err => { error = err; } - ); - } - finally - { - _emailOtpCoroutine = null; - IsAuthenticating = false; - } - - if (error != null) - { - // Inline-only: the caller's onError renders the message ON the code-entry - // page. Broadcasting OnAuthError here too made AuthPromptController close - // the OTP page (HandleAuthError → ShowOptionsPage), so ONE typo ejected - // the player from code entry — and re-requesting a code burns the - // 3-per-10-min budget. A wrong code is a field-level error, not an - // auth-flow failure. - onError?.Invoke(error); - yield break; - } - - var prevTier = Tier; - SetIdentity(identity); - identity.SaveSession(); - - if (prevTier < IdentityTier.Email) - SafeInvoke(OnIdentityUpgraded, identity, prevTier); - - onSuccess?.Invoke(identity); - } - - // ── Transaction signing (JS callbacks) ──────────────────────────────────── - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnTxnSignedFromJS(string signedTxnBase64) - { - if (_pendingSignGeneration == _signGeneration) - PendingSignedTxn = signedTxnBase64; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnGroupTxnSignedFromJS(string signedTxnsJson) - { - if (_pendingSignGeneration != _signGeneration) return; - try - { - var wrapper = JsonUtility.FromJson("{\"items\":" + signedTxnsJson + "}"); - if (wrapper?.items == null) - { - PendingSignError = "Your wallet did not return a signed transaction. Please try again."; - return; - } - PendingSignedTxns = wrapper.items; - } - catch (Exception ex) - { - BlockmakerLog.Warning($"[BlockmakerAuth] Failed to parse group sign result: {ex.Message}"); - PendingSignError = "Something went wrong while processing the signed transactions. Please try again."; - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Preserve] - public void OnTxnErrorFromJS(string error) - { - if (_pendingSignGeneration == _signGeneration) - PendingSignError = error; - } - - // ── Logout ───────────────────────────────────────────────────────────────── - - public void Logout() - { - StopTokenRefreshTimer(); - _isRefreshing = false; - CancelWebGLTimeout(); - BeginPendingSign(); - _signAwaiting = false; // logout invalidates any in-flight sign rather than awaiting it - - IsAuthenticating = false; - _pendingConnectSuccess = null; - _pendingConnectError = null; - _pendingEvmSuccess = null; - _pendingEvmError = null; - _pendingEvmDiscoverForUi = null; - _pendingMagicSuccess = null; - _pendingMagicError = null; - _isWalletConnecting = false; - _walletLoginInFlight = false; - // Invalidate + hard-stop any in-flight wallet-signature login: without this a - // still-polling Login() could complete AFTER logout and write a fresh session - // (UpdateTokens/SaveSession) for an identity the user just discarded. - _walletLoginSeq++; - if (_walletLoginCoroutine != null) - { - StopCoroutine(_walletLoginCoroutine); - _walletLoginCoroutine = null; - } - - if (_magicLoginCoroutine != null) - { - StopCoroutine(_magicLoginCoroutine); - _magicLoginCoroutine = null; - } - if (_otpRequestCoroutine != null) - { - StopCoroutine(_otpRequestCoroutine); - _otpRequestCoroutine = null; - } - if (_emailOtpCoroutine != null) - { - StopCoroutine(_emailOtpCoroutine); - _emailOtpCoroutine = null; - } - if (_peraConnectCoroutine != null) - { - StopCoroutine(_peraConnectCoroutine); - _peraConnectCoroutine = null; - } - CleanupWCv1(); - - _connector?.CancelConnection(); - - #if UNITY_WEBGL && !UNITY_EDITOR - BlockmakerWalletBridge.CancelWalletQR(); - #endif - - string refreshToken = null; - if (Identity is ServerSignedIdentity e) refreshToken = e.RefreshToken; - else if (Identity is MagicIdentity m) refreshToken = m.RefreshToken; - else if (Identity is WalletConnectIdentity wc) refreshToken = wc.RefreshToken; - else if (Identity is EvmXChainIdentity evm) refreshToken = evm.RefreshToken; - BlockmakerClient.Instance?.ServerLogout(refreshToken); - - Identity?.ClearSession(); - SetIdentity(new GuestIdentity()); - // Any in-flight boot restore is moot now — the auth state is KNOWN (guest). - // Idempotent; prevents UI from waiting on a settle that will never come. - SettleSessionRestore(); - BlockmakerLog.Info("[BlockmakerAuth] Logged out — reverted to guest."); - } - - // ── Safe event helpers ───────────────────────────────────────────────────── - - private static void SafeInvoke(Action handler, T arg) - { - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(arg); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - private static void SafeInvoke(Action handler, T1 a, T2 b) - { - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(a, b); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - // ── Helpers ──────────────────────────────────────────────────────────────── - - private void SetIdentity(IBlockmakerIdentity identity) - { - var oldProvider = Identity?.ProviderName; - var oldAddress = Identity?.Address; - var oldTier = Identity?.Tier ?? IdentityTier.Guest; - - if (Identity != null && Identity != identity && oldTier > IdentityTier.Guest) - { - if (oldProvider != identity.ProviderName || - oldAddress != identity.Address) - Identity.ClearSession(); - } - Identity = identity; - IsAuthenticating = false; - BlockmakerLog.Info($"[BlockmakerAuth] Identity set: {identity.ProviderName} | {identity.Address} | Tier: {identity.Tier}"); - - if (identity is EmailIdentity || identity is MagicIdentity || - identity is WalletConnectIdentity || identity is EvmXChainIdentity) - StartTokenRefreshTimer(); - else - StopTokenRefreshTimer(); - - SafeInvoke(OnIdentityChanged, identity); - - if (oldTier > IdentityTier.Guest && - identity.Tier > IdentityTier.Guest && - !string.IsNullOrEmpty(oldAddress) && - !string.IsNullOrEmpty(identity.Address) && - oldAddress != identity.Address) - { - SafeInvoke(OnWalletAddressChanged, new WalletAddressChangedEventArgs(oldProvider, oldAddress, identity.ProviderName, identity.Address)); - } - } - - internal static void NotifyIdentityChanged(IBlockmakerIdentity identity) - => SafeInvoke(OnIdentityChanged, identity); - - /// - /// Acquire a player session token (JWT) for a self-custody wallet identity by - /// running its wallet-signature login (challenge → sign → verify → store). - /// No-op if the identity already holds a non-empty SessionToken. The login is - /// fire-and-forget — game code can act on the wallet immediately; the token - /// lands asynchronously and is then sent on player-authed requests. - /// - private void TriggerWalletLogin(IBlockmakerIdentity identity) - { - if (identity == null) return; - - // In-flight guard: the SessionToken check below is a no-op during the async login - // window (token isn't set until Login()'s last line), so concurrent connect + - // restore + reconnect events could each start a Login() — duplicate signature - // prompts and torn SaveSession. _walletLoginInFlight closes that window. - // - // CRITICAL: set the flag SYNCHRONOUSLY here, before StartCoroutine. The flag used to - // be set deep inside RunWalletLogin (after a possible frame of deferral), so two - // TriggerWalletLogin calls in the SAME frame both passed this guard and both started - // a full Login() → the wallet was prompted to sign twice. Setting it here makes the - // second same-frame call early-return. RunWalletLogin now only OWNS/CLEARS the flag - // (cleared on every exit), never sets it. Logout() also resets it. - if (_walletLoginInFlight) return; - - if (identity is WalletConnectIdentity wc) - { - if (!string.IsNullOrEmpty(wc.SessionToken)) return; - _walletLoginInFlight = true; - - // The connect approval is done; a SECOND approval (the login signature) - // is about to arrive in the wallet app. Without this message users think - // sign-in stalled — the request is easy to miss on a phone. - SafeInvoke(OnAuthStatus, - $"Connected! Now approve the sign-in request in your {wc.ProviderName} app…"); - _walletLoginCoroutine = StartCoroutine(RunWalletLogin(wc, ++_walletLoginSeq)); - } - else if (identity is EvmXChainIdentity evm) - { - if (!string.IsNullOrEmpty(evm.SessionToken)) return; - _walletLoginInFlight = true; - _walletLoginCoroutine = StartCoroutine(RunWalletLogin(evm, ++_walletLoginSeq)); - } - } - - /// - /// Runs a wallet-signature login, deferring it while a user-initiated WebGL sign is - /// in flight so the two signs (which share OnTxnSignedFromJS / the pending-sign slots) - /// don't race — the generation guard would otherwise make one die "interrupted". - /// The auto-login is the deferrer, so the user's sign always wins and the login simply - /// runs afterwards (and is retryable on the next trigger if it never gets a clear slot). - /// - private IEnumerator RunWalletLogin(IBlockmakerIdentity identity, int seq) - { - // _walletLoginInFlight was set SYNCHRONOUSLY by TriggerWalletLogin before this - // coroutine started. This coroutine OWNS the flag from here on: it never sets it, - // and it must CLEAR it on EVERY exit path (early yield breaks below, plus the - // success/error callbacks). Otherwise a future TriggerWalletLogin would be stuck. - // - // `seq` is this attempt's id (captured from _walletLoginSeq at start). If it stops - // matching, RetryWalletLogin/CancelWalletLogin/Logout superseded this attempt: the - // flag now belongs to a NEWER attempt (or was deliberately cleared), so a stale - // attempt must exit without touching the flag and its late callbacks are ignored. - - // Defer briefly if a sign is already in flight (connect or a user txn sign). - // Bounded so a stuck sign can't pin the auto-login forever; if it never clears, - // we abort and rely on the next TriggerWalletLogin / proactive refresh. - float waited = 0f; - while ((_isWalletConnecting || IsWebGLSignInFlight) && waited < WalletSignTimeout) - { - if (_walletLoginSeq != seq) yield break; // superseded by retry/cancel/logout - waited += Time.unscaledDeltaTime; - yield return null; - } - if (_walletLoginSeq != seq) yield break; // superseded by retry/cancel/logout - if (_isWalletConnecting || IsWebGLSignInFlight) - { - BlockmakerLog.Info("[BlockmakerAuth] Auto wallet sign-in deferred — a sign is still in flight; will retry on next trigger."); - _walletLoginInFlight = false; - yield break; - } - - // Re-check guards after the wait (identity may have changed or the token may have - // arrived while we yielded). We still own the flag, so clear it before bailing. - if (Identity != identity) { _walletLoginInFlight = false; yield break; } - - string existingToken = null; - if (identity is WalletConnectIdentity wcCheck) existingToken = wcCheck.SessionToken; - else if (identity is EvmXChainIdentity evmCheck) existingToken = evmCheck.SessionToken; - if (!string.IsNullOrEmpty(existingToken)) { _walletLoginInFlight = false; yield break; } - - // try/finally (no catch — legal around `yield` in an iterator) so the flag is - // ALWAYS cleared, even if Login() throws mid-yield. The per-callback clears stay as - // the fast path; the finally is the backstop. Idempotent bool write, so a double - // clear is harmless. Without this, an unhandled exception in the yield region would - // skip the defensive clear and pin _walletLoginInFlight until Logout(). - try - { - if (identity is WalletConnectIdentity wc) - { - yield return wc.Login( - onSuccess: () => - { - if (_walletLoginSeq != seq) { BlockmakerLog.Info($"[BlockmakerAuth] Ignoring stale wallet sign-in success for {wc.ProviderName} (superseded by retry/cancel)."); return; } - _walletLoginInFlight = false; - if (Identity == wc) SafeInvoke(OnIdentityChanged, wc); - }, - onError: err => - { - if (_walletLoginSeq != seq) { BlockmakerLog.Info($"[BlockmakerAuth] Ignoring stale wallet sign-in error for {wc.ProviderName} (superseded by retry/cancel): {err}"); return; } - _walletLoginInFlight = false; - BlockmakerLog.Warning($"[BlockmakerAuth] Wallet sign-in failed for {wc.ProviderName}: {err}"); - // Tell the step-2 panel — a declined/failed sign-in signature - // previously only logged, leaving the panel silently pulsing - // "approve the request" with no hint anything went wrong. - SafeInvoke(OnAuthStatus, - "Sign-in request was declined or failed — use RESEND to try again, or CANCEL."); - }); - } - else if (identity is EvmXChainIdentity evm) - { - yield return evm.Login( - onSuccess: () => - { - if (_walletLoginSeq != seq) { BlockmakerLog.Info("[BlockmakerAuth] Ignoring stale wallet sign-in success for EVM xChain (superseded by retry/cancel)."); return; } - _walletLoginInFlight = false; - if (Identity == evm) SafeInvoke(OnIdentityChanged, evm); - }, - onError: err => - { - if (_walletLoginSeq != seq) { BlockmakerLog.Info($"[BlockmakerAuth] Ignoring stale wallet sign-in error for EVM xChain (superseded by retry/cancel): {err}"); return; } - _walletLoginInFlight = false; - BlockmakerLog.Warning($"[BlockmakerAuth] Wallet sign-in failed for EVM xChain: {err}"); - SafeInvoke(OnAuthStatus, - "Sign-in request was declined or failed — use RESEND to try again, or CANCEL."); - }); - } - else - { - _walletLoginInFlight = false; - } - } - finally - { - // Defensive backstop: every Login() exit path invokes a callback that clears the - // flag, but guarantee it's cleared even on an exception or a future Login refactor - // that returns without one. Seq-guarded: this finally also runs when retry/cancel - // StopCoroutine()s this attempt (Unity disposes the iterator), and a superseded - // attempt must NOT clear the flag the newer attempt now owns. - if (_walletLoginSeq == seq) - { - _walletLoginInFlight = false; - _walletLoginCoroutine = null; - } - } - } - - // ── Wallet-login retry / cancel (the "stuck on approval 2 of 2" escape hatch) ── - - /// - /// True when the current identity is a self-custody wallet that has connected - /// (approval 1) but not yet completed the login signature (approval 2) — i.e. it - /// holds no session token. This is the state where the step-2 panel is shown and - /// / are meaningful. - /// Intentionally true even while a login attempt is in flight: the whole point of - /// retry is to replace an attempt whose wallet request expired or was missed. - /// - public static bool CanRetryWalletLogin - { - get - { - var id = Instance?.Identity; - if (id is WalletConnectIdentity wc) return string.IsNullOrEmpty(wc.SessionToken); - if (id is EvmXChainIdentity evm) return string.IsNullOrEmpty(evm.SessionToken); - return false; - } - } - - /// - /// Abandon any in-flight wallet-signature login attempt and start a fresh one for - /// the current identity: a new challenge is requested and a NEW sign request is - /// pushed to the wallet app (the old one may have expired or been dismissed). - /// Re-fires so the UI can show "approve the request…" - /// feedback again. Safe no-op (log only) when is false. - /// - public void RetryWalletLogin() - { - if (!CanRetryWalletLogin) - { - BlockmakerLog.Info("[BlockmakerAuth] RetryWalletLogin ignored — no tokenless wallet identity to retry."); - return; - } - - BlockmakerLog.Info("[BlockmakerAuth] Retrying wallet sign-in — abandoning the previous attempt and sending a fresh request."); - AbandonWalletLoginAttempt(); - - // Fresh attempt. For WalletConnect identities TriggerWalletLogin itself fires the - // "Connected! Now approve the sign-in request…" OnAuthStatus message; EVM xChain - // has no message in the trigger path, so give the UI equivalent feedback here. - if (Identity is EvmXChainIdentity) - SafeInvoke(OnAuthStatus, "Approve the sign-in request in your wallet…"); - TriggerWalletLogin(Identity); - } - - /// - /// Abort the wallet login-signature phase entirely: invalidate any in-flight attempt's - /// callbacks, clear the guards, and back to a clean guest state. - /// A tokenless wallet identity can't call player-authed endpoints, so keeping it - /// half-connected only causes confusion — OnIdentityChanged fires via Logout as usual. - /// - public void CancelWalletLogin() - { - BlockmakerLog.Info("[BlockmakerAuth] Wallet sign-in cancelled — aborting the login attempt and logging out."); - AbandonWalletLoginAttempt(); - Logout(); - } - - /// - /// Invalidate the in-flight wallet-login attempt (if any) so it can neither complete - /// nor clear the guards out from under a successor: bumps the attempt seq (late - /// callbacks become stale no-ops), hard-stops the RunWalletLogin coroutine (which also - /// tears down the nested Login() wait), clears the in-flight flag, and frees the shared - /// WebGL pending-sign slots (same idiom as Logout) — bumping the sign generation makes - /// a zombie JS sign wait exit, and clearing the awaiting bit stops the next attempt - /// from deferring behind the dead sign for a full WalletSignTimeout. - /// - private void AbandonWalletLoginAttempt() - { - _walletLoginSeq++; - if (_walletLoginCoroutine != null) - { - StopCoroutine(_walletLoginCoroutine); - _walletLoginCoroutine = null; - } - _walletLoginInFlight = false; - - BeginPendingSign(); - _signAwaiting = false; // invalidate, don't await, the abandoned sign - } - - private static IBlockmakerIdentity CreateWalletIdentity(string provider, string address) - { - if (provider.Equals(ProviderDefly, StringComparison.OrdinalIgnoreCase)) - return new DeflyIdentity(address); - if (provider.Equals(ProviderPera, StringComparison.OrdinalIgnoreCase)) - return new PeraIdentity(address); - - BlockmakerLog.Warning($"[BlockmakerAuth] Unknown provider '{provider}', defaulting to Pera."); - return new PeraIdentity(address); - } - } - - public static class BlockmakerPrefs - { - private static string _prefix; - - public static string Prefix - { - get - { - if (_prefix == null) - { - var config = BlockmakerAuth.Instance?.blockmakerConfig; - string id = config != null ? config.gameId : ""; - if (string.IsNullOrEmpty(id)) - id = Application.identifier ?? "default"; - _prefix = $"bm_{id}_"; - } - return _prefix; - } - } - - public static string Key(string baseName) => Prefix + baseName; - - public static void InvalidatePrefix() => _prefix = null; - } - - [Serializable] - internal class StringArrayWrapper - { - public string[] items; - } - - public readonly struct WalletQREventArgs - { - public string Provider { get; } - public string WalletConnectUri { get; } - public string QRCodeBase64Png { get; } - - public WalletQREventArgs(string provider, string walletConnectUri, string qrCodeBase64Png) - { - Provider = provider; - WalletConnectUri = walletConnectUri; - QRCodeBase64Png = qrCodeBase64Png; - } - } - - public readonly struct WalletAddressChangedEventArgs - { - public string OldProvider { get; } - public string OldAddress { get; } - public string NewProvider { get; } - public string NewAddress { get; } - - public WalletAddressChangedEventArgs(string oldProvider, string oldAddress, string newProvider, string newAddress) - { - OldProvider = oldProvider; - OldAddress = oldAddress; - NewProvider = newProvider; - NewAddress = newAddress; - } - } - -} \ No newline at end of file diff --git a/Core/BlockmakerAuth.cs.meta b/Core/BlockmakerAuth.cs.meta deleted file mode 100644 index b0d3474..0000000 --- a/Core/BlockmakerAuth.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 1e6cbb4e8034b4081aa006f2461b9047 \ No newline at end of file diff --git a/Core/BlockmakerClient.cs b/Core/BlockmakerClient.cs deleted file mode 100644 index 7152fb3..0000000 --- a/Core/BlockmakerClient.cs +++ /dev/null @@ -1,1171 +0,0 @@ -using System; -using System.Collections; -using System.Text; -using UnityEngine; -using UnityEngine.Networking; - -namespace Blockmaker -{ - - /// - /// HTTP client for the Blockmaker server. - /// All game systems call this — it knows nothing about which wallet or - /// auth provider is active. - /// - /// Auth identity is read from BlockmakerAuth.Instance.Identity automatically - /// for every request that needs a wallet address. - /// - public partial class BlockmakerClient : MonoBehaviour - { - public static BlockmakerClient Instance { get; private set; } - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() - { - Instance = null; - } - - [Header("Config")] - public BlockmakerConfig config; - - private string _baseUrl; - - private void Awake() - { - if (Instance != null && Instance != this) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); - - if (config == null) - { - // Config may be assigned post-Awake by BlockmakerAuth.EnsureBlockmakerClient() - // via InitFromAuth(). Don't destroy — just disable until config arrives. - BlockmakerLog.Verbose("[BlockmakerClient] No BlockmakerConfig assigned yet — waiting for BlockmakerAuth.InitFromAuth()."); - Instance = null; - enabled = false; - return; - } - var url = config.serverUrl; - if (string.IsNullOrEmpty(url)) - url = BlockmakerConfig.DefaultServerUrl; - _baseUrl = url.TrimEnd('/'); - - } - - private void OnDestroy() - { - if (Instance == this) - Instance = null; - } - - /// - /// Re-initializes after config was set post-Awake (e.g. when added via AddComponent at runtime). - /// Called by BlockmakerAuth.EnsureBlockmakerClient(). - /// - public void InitFromAuth() - { - if (config == null) return; - var url = config.serverUrl; - if (string.IsNullOrEmpty(url)) - url = BlockmakerConfig.DefaultServerUrl; - enabled = true; - if (Instance == null) Instance = this; - _baseUrl = url.TrimEnd('/'); - BlockmakerLog.Info($"[BlockmakerClient] Initialized — server: {_baseUrl}"); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // FLOW RUNNER - // ═══════════════════════════════════════════════════════════════════════════ - - /// Run a flow against the current identity's address. - public void RunFlow( - string flowId, - Action onSuccess, - Action onError = null, - string contextJson = null) - { - var auth = BlockmakerAuth.Instance; - if (auth == null || !auth.HasWallet) - { - onError?.Invoke("No wallet connected. Please connect a wallet or sign in first."); - return; - } - StartCoroutine(RunFlowRoutine(flowId, auth.Address, contextJson, onSuccess, onError)); - } - - /// Run a flow with a custom result type (for game-specific flow data). - public void RunFlow( - string flowId, - Action onSuccess, - Action onError = null, - string contextJson = null) where T : class - { - var auth = BlockmakerAuth.Instance; - if (auth == null || !auth.HasWallet) - { - onError?.Invoke("No wallet connected. Please connect a wallet or sign in first."); - return; - } - StartCoroutine(RunFlowRoutine(flowId, auth.Address, contextJson, onSuccess, onError)); - } - - /// Run a flow against an explicit wallet address. - public void RunFlowForWallet( - string flowId, - string walletAddress, - Action onSuccess, - Action onError = null, - string contextJson = null) - { - if (_baseUrl == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); return; } - StartCoroutine(RunFlowRoutine(flowId, walletAddress, contextJson, onSuccess, onError)); - } - - private IEnumerator RunFlowRoutine( - string flowId, string walletAddress, string contextJson, - Action onSuccess, Action onError) where T : class - { - string url = $"{_baseUrl}/v1/flows/{flowId}/run"; - string body = JsonUtility.ToJson(new FlowRunRequest - { wallet = walletAddress, context = contextJson }); - - using var req = BuildPost(url, body, config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // EMAIL AUTH - // ═══════════════════════════════════════════════════════════════════════════ - - /// Ask the server to send an OTP to the given email. - public IEnumerator RequestEmailOTP(string email, Action onSent, Action onError) - { - string url = $"{_baseUrl}/v1/auth/email/request"; - string body = JsonUtility.ToJson(new EmailOTPRequest { email = email }); - - using var req = BuildPost(url, body, config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - - if (req.result != UnityWebRequest.Result.Success) - { - string err = "Something went wrong. Please check your connection and try again."; - try - { - var respBody = req.downloadHandler?.text; - if (!string.IsNullOrEmpty(respBody)) - { - var parsed = JsonUtility.FromJson(respBody); - if (!string.IsNullOrEmpty(parsed.error)) - { - BlockmakerLog.Verbose($"[BlockmakerClient] Server error: {parsed.error}"); - err = parsed.error; - } - } - } - catch (Exception parseEx) { BlockmakerLog.Warning($"[BlockmakerClient] Error response parse failed: {parseEx.Message}"); } - BlockmakerLog.Error($"[BlockmakerClient] Email OTP HTTP {req.responseCode}: {req.error}"); - onError?.Invoke(err); - } - else - onSent?.Invoke(); - } - - /// Verify an OTP and receive a session token + managed wallet address. - public IEnumerator VerifyEmailOTP( - string email, string otp, - Action onSuccess, - Action onError) - { - string url = $"{_baseUrl}/v1/auth/email/verify"; - string body = JsonUtility.ToJson(new EmailVerifyRequest { email = email, otp = otp }); - - using var req = BuildPost(url, body, config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // MAGIC AUTH - // ═══════════════════════════════════════════════════════════════════════════ - - /// - /// Send Magic's DID token to the server for verification. - /// Server verifies the token with Magic's admin SDK, creates or finds - /// the player account, and returns a JWT + Algorand address. - /// - public IEnumerator VerifyMagicToken( - string didToken, - string email, - Action onSuccess, - Action onError) - { - string url = $"{_baseUrl}/v1/auth/magic/verify"; - string body = JsonUtility.ToJson(new MagicVerifyRequest { didToken = didToken, email = email }); - - using var req = BuildPost(url, body, config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // WALLET-SIGNATURE AUTH (self-custody tier) - // ═══════════════════════════════════════════════════════════════════════════ - - /// - /// Ask the server for a single-use challenge to sign with a self-custody wallet. - /// chain is "algorand" (Pera/Defly) or "evm" (xChain); pass the EVM signer - /// address for the "evm" path (null for "algorand"). - /// - public IEnumerator RequestWalletChallenge( - string walletAddress, string chain, string evmAddress, - Action onSuccess, - Action onError) - { - string url = $"{_baseUrl}/v1/auth/wallet/challenge"; - string body = JsonUtility.ToJson(new WalletChallengeRequest - { walletAddress = walletAddress, chain = chain, evmAddress = evmAddress }); - - using var req = BuildPost(url, body, config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - /// - /// Submit a wallet proof-of-ownership and receive a player session token + - /// refresh token (same shape as email/magic verify). - /// algorand (Pera/Defly): pass (base64 of the - /// signed 0-amount self-payment whose note == nonce) and null for - /// . evm (xChain): pass the personal_sign - /// hex and null for . - /// - public IEnumerator VerifyWalletSignature( - string walletAddress, string chain, string signature, string signedTxn, string nonce, string evmAddress, - Action onSuccess, - Action onError) - { - string url = $"{_baseUrl}/v1/auth/wallet/verify"; - string body = JsonUtility.ToJson(new WalletVerifyRequest - { walletAddress = walletAddress, chain = chain, signature = signature, signedTxn = signedTxn, nonce = nonce, evmAddress = evmAddress }); - - using var req = BuildPost(url, body, config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // SERVER-SIDE SIGNING (Email tier) - // ═══════════════════════════════════════════════════════════════════════════ - - /// - /// Ask the Blockmaker server to sign a transaction using the player's - /// managed wallet. Only valid for Email tier identities. - /// - public IEnumerator SignTransactionServerSide( - string unsignedTxnBase64, - string sessionToken, - Action onSigned, - Action onError, - Action onBlockmakerError = null) - { - string url = $"{_baseUrl}/v1/auth/sign"; - string body = JsonUtility.ToJson(new ServerSignRequest - { unsignedTxnBase64 = unsignedTxnBase64 }); - - using var req = new UnityWebRequest(url, "POST") - { - uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)), - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(config.longRequestTimeoutSeconds) - }; - req.SetRequestHeader("Content-Type", "application/json"); - req.SetRequestHeader("Authorization", $"Bearer {sessionToken}"); - - yield return req.SendWebRequest(); - - if (req.result != UnityWebRequest.Result.Success) - { - string err = "Something went wrong. Please try again."; - string code = ""; - try - { - var respBody = req.downloadHandler?.text; - if (!string.IsNullOrEmpty(respBody)) - { - var parsed = JsonUtility.FromJson(respBody); - if (!string.IsNullOrEmpty(parsed.error)) - { - BlockmakerLog.Verbose($"[BlockmakerClient] Server error: {parsed.error}"); - err = parsed.error; - } - code = parsed.code ?? ""; - } - } - catch (Exception parseEx) { BlockmakerLog.Warning($"[BlockmakerClient] Error response parse failed: {parseEx.Message}"); } - BlockmakerLog.Error($"[BlockmakerClient] Sign HTTP {req.responseCode}: {req.error}"); - onBlockmakerError?.Invoke(new BlockmakerError(code, err, (int)req.responseCode)); - onError?.Invoke(err); - yield break; - } - - try - { - var result = JsonUtility.FromJson(req.downloadHandler.text); - onSigned?.Invoke(result.signedTxnBase64); - } - catch (Exception e) - { - BlockmakerLog.Error($"[BlockmakerClient] Sign parse error: {e.Message}"); - onError?.Invoke("Something went wrong. Please try again."); - } - } - - public IEnumerator SignTransactionsServerSide( - string[] unsignedTxnsBase64, - string sessionToken, - Action onSigned, - Action onError, - Action onBlockmakerError = null) - { - string url = $"{_baseUrl}/v1/auth/sign"; - string body = JsonUtility.ToJson(new ServerSignRequest - { unsignedTxnsBase64 = unsignedTxnsBase64 }); - - using var req = new UnityWebRequest(url, "POST") - { - uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)), - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(config.longRequestTimeoutSeconds) - }; - req.SetRequestHeader("Content-Type", "application/json"); - req.SetRequestHeader("Authorization", $"Bearer {sessionToken}"); - - yield return req.SendWebRequest(); - - if (req.result != UnityWebRequest.Result.Success) - { - string err = "Something went wrong. Please try again."; - string code = ""; - try - { - var respBody = req.downloadHandler?.text; - if (!string.IsNullOrEmpty(respBody)) - { - var parsed = JsonUtility.FromJson(respBody); - if (!string.IsNullOrEmpty(parsed.error)) - { - BlockmakerLog.Verbose($"[BlockmakerClient] Server error: {parsed.error}"); - err = parsed.error; - } - code = parsed.code ?? ""; - } - } - catch (Exception parseEx) { BlockmakerLog.Warning($"[BlockmakerClient] Error response parse failed: {parseEx.Message}"); } - BlockmakerLog.Error($"[BlockmakerClient] Sign HTTP {req.responseCode}: {req.error}"); - onBlockmakerError?.Invoke(new BlockmakerError(code, err, (int)req.responseCode)); - onError?.Invoke(err); - yield break; - } - - try - { - var result = JsonUtility.FromJson(req.downloadHandler.text); - onSigned?.Invoke(result.signedTxnsBase64); - } - catch (Exception e) - { - BlockmakerLog.Error($"[BlockmakerClient] Sign parse error: {e.Message}"); - onError?.Invoke("Something went wrong. Please try again."); - } - } - - // ═══════════════════════════════════════════════════════════════════════════ - // REWARDS - // ═══════════════════════════════════════════════════════════════════════════ - - /// - /// Send a reward from the game treasury wallet. - /// Only available in the Unity Editor — never in player builds. - /// A player who extracts the API key from a build can call the rewards - /// endpoint directly and drain your treasury. - /// For production, call the rewards endpoint from your own trusted server. - /// - /// - /// Idempotency key. Pass a STABLE id you own for this logical reward (e.g. - /// "{raceId}:{wallet}:{reason}") and reuse the SAME value on any retry — the - /// server then dedups a retried send and never double-pays. Leave null and the - /// SDK mints a fresh key per call (protects only this call, not a caller-level retry). - /// - public void SendReward( - string recipientWallet, - long amountMicroAlgo, - string reason = "reward", - long assetId = 0, - string contextId = null, - Action onSuccess = null, - Action onError = null) - { - #if !UNITY_EDITOR - BlockmakerLog.Error("[Blockmaker] SendReward is disabled in player builds — use a trusted server to send rewards."); - onError?.Invoke("This action is not available right now."); - return; - #else - StartCoroutine(PostJson( - $"{_baseUrl}/v1/rewards/send", - new RewardRequest - { - recipientWallet = recipientWallet, - assetId = assetId, - amountMicroAlgo = amountMicroAlgo, - reason = reason, - // Stable key dedups retries; mint one if the caller didn't supply it. - contextId = string.IsNullOrEmpty(contextId) - ? System.Guid.NewGuid().ToString("N") - : contextId - }, - config.longRequestTimeoutSeconds, - onSuccess, onError - )); - #endif - } - - // ═══════════════════════════════════════════════════════════════════════════ - // GAME-SPECIFIC ENDPOINTS - // ═══════════════════════════════════════════════════════════════════════════ - - /// POST JSON to a server path. Use for game-specific endpoints. - public void Post(string path, TReq body, Action onSuccess = null, Action onError = null) where TRes : class - { - Post(path, body, config.defaultTimeoutSeconds, onSuccess, onError); - } - - /// - /// POST JSON to a server path with an explicit timeout. Use a longer timeout (e.g. - /// config.walletTimeoutSeconds) for endpoints that scan a whole wallet — large - /// wallets can take well over the 10s default to enumerate on-chain. - /// - public void Post(string path, TReq body, float timeoutSeconds, Action onSuccess = null, Action onError = null) where TRes : class - { - StartCoroutine(PostJsonAuth( - $"{_baseUrl}{path}", body, - timeoutSeconds, - onSuccess, onError - )); - } - - /// GET JSON from a server path (auto-appends wallet). Use for game-specific endpoints. - public void Get(string path, Action onSuccess, Action onError = null) where TRes : class - { - string wallet = UnityWebRequest.EscapeURL(BlockmakerAuth.Instance?.Address ?? ""); - string sep = path.Contains("?") ? "&" : "?"; - StartCoroutine(GetJson( - $"{_baseUrl}{path}{sep}wallet={wallet}", - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TRANSACTION BUILDER - // ═══════════════════════════════════════════════════════════════════════════ - - /// Build an unsigned payment transaction via the server. - public void BuildPayment( - string recipient, long amountMicroAlgo, string note = null, - Action onSuccess = null, Action onError = null) - { - StartCoroutine(PostJsonAuth( - $"{_baseUrl}/v1/transactions/build", - new BuildTransactionRequest { type = "payment", recipient = recipient, amount = amountMicroAlgo, note = note ?? "", walletAddress = BlockmakerAuth.Instance?.Address ?? "" }, - config.defaultTimeoutSeconds, onSuccess, onError)); - } - - /// Build an unsigned ASA transfer transaction via the server. - public void BuildAssetTransfer( - string recipient, long assetId, long amount, string note = null, - Action onSuccess = null, Action onError = null) - { - StartCoroutine(PostJsonAuth( - $"{_baseUrl}/v1/transactions/build", - new BuildTransactionRequest { type = "asset_transfer", recipient = recipient, amount = amount, assetId = assetId, note = note ?? "", walletAddress = BlockmakerAuth.Instance?.Address ?? "" }, - config.defaultTimeoutSeconds, onSuccess, onError)); - } - - /// Build an unsigned ASA opt-in transaction (0-amount self-transfer). - public void BuildAssetOptIn( - long assetId, - Action onSuccess = null, Action onError = null) - { - StartCoroutine(PostJsonAuth( - $"{_baseUrl}/v1/transactions/build", - new BuildTransactionRequest { type = "asset_optin", assetId = assetId, walletAddress = BlockmakerAuth.Instance?.Address ?? "" }, - config.defaultTimeoutSeconds, onSuccess, onError)); - } - - /// Submit a signed transaction to the Algorand network via the server. - public void SubmitTransaction( - string signedTxnBase64, - Action onSuccess = null, Action onError = null) - { - StartCoroutine(PostJsonAuth( - $"{_baseUrl}/v1/transactions/submit", - new SubmitTransactionRequest { signedTxnBase64 = signedTxnBase64 }, - config.longRequestTimeoutSeconds, onSuccess, onError)); - } - - /// Submit signed group transactions to the Algorand network via the server. - public void SubmitTransactions( - string[] signedTxnsBase64, - Action onSuccess = null, Action onError = null) - { - StartCoroutine(PostJsonAuth( - $"{_baseUrl}/v1/transactions/submit", - new SubmitTransactionRequest { signedTxnsBase64 = signedTxnsBase64 }, - config.longRequestTimeoutSeconds, onSuccess, onError)); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // SERVER HEALTH - // ═══════════════════════════════════════════════════════════════════════════ - - public void VerifyConnection(Action onResult) - { - StartCoroutine(VerifyConnectionRoutine(onResult)); - } - - private IEnumerator VerifyConnectionRoutine(Action onResult) - { - using var req = BuildGet($"{_baseUrl}/v1/me", config.defaultTimeoutSeconds); - yield return req.SendWebRequest(); - onResult?.Invoke(req.result == UnityWebRequest.Result.Success); - } - - /// - /// Verify a specific JWT session token against the server. - /// Used during Magic session restore to confirm the server-side JWT - /// is still valid even when the Magic JS session has expired. - /// - public void VerifySessionToken(string sessionToken, Action onResult) - { - StartCoroutine(VerifySessionTokenRoutine(sessionToken, onResult)); - } - - private IEnumerator VerifySessionTokenRoutine(string sessionToken, Action onResult) - { - using var req = new UnityWebRequest($"{_baseUrl}/v1/auth/session", "GET") - { - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(config.defaultTimeoutSeconds) - }; - req.SetRequestHeader("Authorization", $"Bearer {sessionToken}"); - yield return req.SendWebRequest(); - onResult?.Invoke(req.result == UnityWebRequest.Result.Success); - } - - /// - /// Exchange a refresh token for a new JWT + rotated refresh token. - /// Call on session restore to get a fresh short-lived JWT. - /// - public void RefreshToken(string refreshToken, Action onSuccess, Action onError = null) - { - StartCoroutine(RefreshTokenRoutine(refreshToken, onSuccess, onError)); - } - - private IEnumerator RefreshTokenRoutine(string refreshToken, Action onSuccess, Action onError) - { - var body = JsonUtility.ToJson(new RefreshTokenRequest { refreshToken = refreshToken }); - using var req = new UnityWebRequest($"{_baseUrl}/v1/auth/refresh", "POST") - { - uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)), - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(config.defaultTimeoutSeconds) - }; - req.SetRequestHeader("Content-Type", "application/json"); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - /// - /// Invalidate a refresh token on the server (logout). - /// Fire-and-forget — does not report errors. - /// - public void ServerLogout(string refreshToken) - { - if (string.IsNullOrEmpty(refreshToken)) return; - StartCoroutine(ServerLogoutRoutine(refreshToken)); - } - - private IEnumerator ServerLogoutRoutine(string refreshToken) - { - var body = JsonUtility.ToJson(new LogoutRequest { refreshToken = refreshToken }); - using var req = new UnityWebRequest($"{_baseUrl}/v1/auth/logout", "POST") - { - uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)), - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(config.defaultTimeoutSeconds) - }; - req.SetRequestHeader("Content-Type", "application/json"); - yield return req.SendWebRequest(); - } - - // ═══════════════════════════════════════════════════════════════════════════ - private string ProfileUrl(string path) - { - string url = $"{_baseUrl}{path}"; - string wallet = BlockmakerAuth.Instance?.Address; - if (!string.IsNullOrEmpty(wallet)) - url += (url.Contains("?") ? "&" : "?") + $"wallet={UnityWebRequest.EscapeURL(wallet)}"; - return url; - } - - // PROFILE - // ═══════════════════════════════════════════════════════════════════════════ - - /// Fetch the current player's profile + game data. - public void GetProfile(Action onSuccess, Action onError = null) - { - StartCoroutine(GetJson( - ProfileUrl("/v1/profile"), - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// Fetch all ASA holdings for the authenticated wallet. - public void GetHoldings(Action onSuccess, Action onError = null) - { - StartCoroutine(GetJson( - ProfileUrl("/v1/profile/holdings"), - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// Check if the wallet holds NFTs from the given creator addresses. - public void CheckCollections( - string[] creators, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/collection-check"), - new CollectionCheckRequest { creators = creators }, - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// Fetch the full asset registry (NFT collections and tokens). - public void GetRegistry(Action onSuccess, Action onError = null) - { - StartCoroutine(GetJson( - $"{_baseUrl}/v1/profile/registry", - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// Check if a username is available. Set isChange=true for change flow pricing. - public void CheckUsername( - string username, - Action onSuccess, - Action onError = null, - bool isChange = false) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/username/check"), - new UsernameCheckRequest - { - username = username, - walletAddress = BlockmakerAuth.Instance?.Address, - isChange = isChange - }, - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Step 1 of username claim: server validates, reserves the username, - /// pins ARC-3 metadata to IPFS, and returns an unsigned AssetCreateTxn. - /// - public void PrepareUsernameClaim( - string username, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/username/claim/prepare"), - new UsernameClaimPrepareRequest { username = username }, - config.longRequestTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Step 2 of username claim: send signed group txns; server broadcasts, - /// confirms, and activates the username on-chain. - /// - public void CompleteUsernameClaim( - string reservationId, - string[] signedTxnsBase64, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/username/claim/complete"), - new UsernameCompleteRequest - { - reservationId = reservationId, - signedTxnsBase64 = signedTxnsBase64, - }, - config.longRequestTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Step 1 of username change: builds atomic 3-txn group - /// (deregister old + payment + register new). - /// - public void PrepareUsernameChange( - string newUsername, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/username/change/prepare"), - new UsernameChangePrepareRequest { newUsername = newUsername }, - config.longRequestTimeoutSeconds, - onSuccess, onError - )); - } - - /// Step 2 of username change: broadcast signed group, server confirms. - public void CompleteUsernameChange( - string reservationId, - string[] signedTxnsBase64, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/username/change/complete"), - new UsernameCompleteRequest - { - reservationId = reservationId, - signedTxnsBase64 = signedTxnsBase64, - }, - config.longRequestTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Set the player's profile picture to an NFT they own. - /// The server verifies ownership via the Algorand indexer, resolves the - /// ARC-3/ARC-69 image URL, and persists both on the profile. - /// Endpoint: POST /v1/profile/pfp - /// - public void SetProfilePicNft( - long assetId, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/pfp"), - new SetProfilePicRequest { assetId = assetId }, - config.longRequestTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Search the current player's wallet for NFTs. - /// The server queries the Algorand indexer for all ASAs held by the wallet, - /// filters by the search term (matches name or asset ID), and returns - /// name + assetId + imageUrl for each match. - /// Endpoint: POST /v1/profile/wallet/nfts - /// - public void SearchWalletNFTs( - string searchTerm, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/wallet/nfts"), - new WalletNFTSearchRequest { search = searchTerm ?? "" }, - config.walletTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Resolve the image URL for a single NFT (preview before setting as pfp). - /// Endpoint: POST /v1/profile/nft/image - /// - public void GetNftImageUrl( - long assetId, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/nft/image"), - new NftImageRequest { assetId = assetId }, - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// - /// Set the player's profile picture to a built-in default avatar. - /// No NFT ownership required. - /// Endpoint: POST /v1/profile/pfp/default - /// - public void SetDefaultAvatar( - string avatarId, - Action onSuccess, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/pfp/default"), - new SetDefaultAvatarRequest { avatarId = avatarId }, - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// Upload a profile image. Pass raw image bytes and MIME type. - public IEnumerator UploadProfileImage( - byte[] imageBytes, - string mimeType, - string filename, - Action onSuccess, - Action onError = null) - { - string url = ProfileUrl("/v1/profile/image"); - var form = new WWWForm(); - form.AddBinaryData("image", imageBytes, filename, mimeType); - - using var req = UnityWebRequest.Post(url, form); - req.timeout = SafeTimeout(config.longRequestTimeoutSeconds); - var token = GetSessionToken(); - if (!string.IsNullOrEmpty(token)) - req.SetRequestHeader("Authorization", $"Bearer {token}"); - - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - /// Fetch onboarding status and wallet balances for the current player. - public void GetOnboardingStatus( - Action onSuccess, - Action onError = null) - { - StartCoroutine(GetJson( - ProfileUrl("/v1/profile/onboarding-status"), - config.defaultTimeoutSeconds, - onSuccess, onError - )); - } - - /// Advance the player's onboarding step. - public void AdvanceOnboardingStep( - string step, - Action onSuccess = null, - Action onError = null) - { - StartCoroutine(PostJsonAuth( - ProfileUrl("/v1/profile/onboarding/advance"), - new OnboardingAdvanceRequest { step = step }, - config.defaultTimeoutSeconds, - _ => onSuccess?.Invoke(), onError - )); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // CONVENIENCE METHODS - // ═══════════════════════════════════════════════════════════════════════════ - - /// True when a wallet is connected and the SDK can make authenticated requests. - public bool IsConnected => BlockmakerAuth.Instance != null && BlockmakerAuth.Instance.HasWallet; - - /// Check whether the current player holds a specific asset (any amount > 0). - public void OwnsAsset(long assetId, Action onResult, Action onError = null) - { - GetHoldings(result => - { - bool owns = result?.holdings != null && result.holdings.Exists(h => h.assetId == assetId && h.amount > 0); - onResult?.Invoke(owns); - }, onError); - } - - /// Get the balance of a specific ASA for the current player. - public void GetAssetBalance(long assetId, Action onResult, Action onError = null) - { - GetHoldings(result => - { - long balance = 0; - if (result?.holdings != null) - { - var holding = result.holdings.Find(h => h.assetId == assetId); - if (holding != null) balance = holding.amount; - } - onResult?.Invoke(balance); - }, onError); - } - - /// Check whether the current player is opted into a specific ASA. - public void IsOptedIn(long assetId, Action onResult, Action onError = null) - { - GetHoldings(result => - { - bool optedIn = result?.holdings != null && result.holdings.Exists(h => h.assetId == assetId); - onResult?.Invoke(optedIn); - }, onError); - } - - /// Build, sign, and submit a payment transaction in one call. - public void SendPayment(string recipient, long amountMicroAlgo, Action onTxId, Action onError = null, string note = null) - { - BuildPayment(recipient, amountMicroAlgo, note, buildResult => - { - if (!buildResult.success) { onError?.Invoke(buildResult.error ?? "Something went wrong while preparing your payment. Please try again."); return; } - var txnB64 = buildResult.unsignedTxnBase64; - var identity = BlockmakerAuth.Instance?.Identity; - if (identity == null || !identity.CanSign) { onError?.Invoke("No wallet connected. Please connect a wallet or sign in first."); return; } - StartCoroutine(SignAndSubmit(txnB64, onTxId, onError)); - }, onError); - } - - /// Build, sign, and submit an ASA opt-in transaction in one call. - public void OptInToAsset(long assetId, Action onTxId, Action onError = null) - { - BuildAssetOptIn(assetId, buildResult => - { - if (!buildResult.success) { onError?.Invoke(buildResult.error ?? "Something went wrong. Please try again."); return; } - var txnB64 = buildResult.unsignedTxnBase64; - var identity = BlockmakerAuth.Instance?.Identity; - if (identity == null || !identity.CanSign) { onError?.Invoke("No wallet connected. Please connect a wallet or sign in first."); return; } - StartCoroutine(SignAndSubmit(txnB64, onTxId, onError)); - }, onError); - } - - private IEnumerator SignAndSubmit(string unsignedTxnBase64, Action onTxId, Action onError) - { - var identity = BlockmakerAuth.Instance?.Identity; - string signedTxn = null; - string signError = null; - bool signDone = false; - - StartCoroutine(identity.SignTransaction(unsignedTxnBase64, signed => - { - signedTxn = signed; - signDone = true; - }, err => - { - signError = err; - signDone = true; - })); - - while (!signDone) yield return null; - - if (!string.IsNullOrEmpty(signError)) { onError?.Invoke(signError); yield break; } - if (string.IsNullOrEmpty(signedTxn)) { onError?.Invoke("Something went wrong. Please try again."); yield break; } - - SubmitTransaction(signedTxn, result => - { - if (result.success) - onTxId?.Invoke(result.txId); - else - onError?.Invoke(result.error ?? "Something went wrong while completing your payment. Please try again."); - }, onError); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // HTTP HELPERS - // ═══════════════════════════════════════════════════════════════════════════ - - /// - /// True when a request would carry real backend auth (player JWT — or the - /// dev API key in the editor). Guests get false in builds: use this to skip - /// best-effort backend calls that would otherwise just spam 401s. - /// - public bool HasBackendSession => !string.IsNullOrEmpty(GetSessionToken()); - - /// - /// Returns the best available auth token for the current identity: - /// JWT session token for signed-in tiers, API key in Editor only. - /// In player builds, returns empty string if no JWT is available — - /// the server API key must never be shipped in client builds. - /// - private string GetSessionToken() - { - var identity = BlockmakerAuth.Instance?.Identity; - if (identity is ServerSignedIdentity ss && !string.IsNullOrEmpty(ss.SessionToken)) - return ss.SessionToken; - if (identity is MagicIdentity magic && !string.IsNullOrEmpty(magic.SessionToken)) - return magic.SessionToken; - if (identity is WalletConnectIdentity wc && !string.IsNullOrEmpty(wc.SessionToken)) - return wc.SessionToken; - if (identity is EvmXChainIdentity evm && !string.IsNullOrEmpty(evm.SessionToken)) - return evm.SessionToken; - #if UNITY_EDITOR - return config?.apiKey ?? ""; - #else - return ""; - #endif - } - - /// - /// Returns the auth token for server-to-server calls (Editor/testing only) - /// or the session token in player builds. - /// - private string GetAuthHeader() - { - var session = GetSessionToken(); - if (!string.IsNullOrEmpty(session)) - return session; - #if UNITY_EDITOR - return config?.apiKey ?? ""; - #else - return ""; - #endif - } - - private IEnumerator PostJsonAuth( - string url, object payload, float timeout, - Action onSuccess, Action onError) where T : class - { - if (_baseUrl == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - string body = JsonUtility.ToJson(payload); - using var req = new UnityWebRequest(url, "POST") - { - uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)), - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(timeout) - }; - req.SetRequestHeader("Content-Type", "application/json"); - var token = GetSessionToken(); - if (!string.IsNullOrEmpty(token)) - req.SetRequestHeader("Authorization", $"Bearer {token}"); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - private IEnumerator PostJson( - string url, object payload, float timeout, - Action onSuccess, Action onError) where T : class - { - if (_baseUrl == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - string body = JsonUtility.ToJson(payload); - using var req = BuildPost(url, body, timeout); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - private IEnumerator GetJson( - string url, float timeout, - Action onSuccess, Action onError) where T : class - { - if (_baseUrl == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - using var req = BuildGet(url, timeout); - // Override with session token so JWT-auth players work on profile endpoints - var token = GetSessionToken(); - if (!string.IsNullOrEmpty(token)) - req.SetRequestHeader("Authorization", $"Bearer {token}"); - yield return req.SendWebRequest(); - HandleResponse(req, onSuccess, onError); - } - - private static int SafeTimeout(float seconds) - { - return Mathf.Max(1, Mathf.RoundToInt(seconds)); - } - - private UnityWebRequest BuildPost(string url, string jsonBody, float timeout) - { - var req = new UnityWebRequest(url, "POST") - { - uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonBody)), - downloadHandler = new DownloadHandlerBuffer(), - timeout = SafeTimeout(timeout) - }; - req.SetRequestHeader("Content-Type", "application/json"); - var token = GetAuthHeader(); - if (!string.IsNullOrEmpty(token)) - req.SetRequestHeader("Authorization", $"Bearer {token}"); - return req; - } - - private UnityWebRequest BuildGet(string url, float timeout) - { - var req = UnityWebRequest.Get(url); - req.timeout = SafeTimeout(timeout); - var token = GetAuthHeader(); - if (!string.IsNullOrEmpty(token)) - req.SetRequestHeader("Authorization", $"Bearer {token}"); - return req; - } - - private void HandleResponse( - UnityWebRequest req, - Action onSuccess, - Action onError, - Action onBlockmakerError = null) where T : class - { - if (req.result != UnityWebRequest.Result.Success) - { - string err; - if (req.result == UnityWebRequest.Result.ConnectionError) - err = "Could not reach the server. Please check your connection and try again."; - else if ((int)req.responseCode == 401 || (int)req.responseCode == 403) - err = "Your session has expired. Please sign in again."; - else if ((int)req.responseCode >= 500) - err = "The server is having trouble right now. Please try again in a moment."; - else - err = "Something went wrong. Please try again."; - string code = "NETWORK"; - int httpStatus = (int)req.responseCode; - try - { - var body = req.downloadHandler?.text; - if (!string.IsNullOrEmpty(body)) - { - var parsed = JsonUtility.FromJson(body); - if (!string.IsNullOrEmpty(parsed.error)) - { - BlockmakerLog.Verbose($"[BlockmakerClient] Server error: {parsed.error}"); - err = parsed.error; - } - if (!string.IsNullOrEmpty(parsed.code)) - code = parsed.code; - } - } - catch (Exception parseEx) { BlockmakerLog.Warning($"[BlockmakerClient] Error response parse failed: {parseEx.Message}"); } - BlockmakerLog.Error($"[BlockmakerClient] HTTP {req.responseCode}: {req.error}"); - - if (onBlockmakerError != null) - onBlockmakerError.Invoke(new BlockmakerError(code, err, httpStatus)); - onError?.Invoke(err); - return; - } - try - { - var body = req.downloadHandler?.text; - if (string.IsNullOrEmpty(body)) - { - onError?.Invoke("Something went wrong. Please try again."); - return; - } - onSuccess?.Invoke(JsonUtility.FromJson(body)); - } - catch (Exception e) - { - BlockmakerLog.Error($"[BlockmakerClient] JSON parse error: {e.Message}"); - onError?.Invoke("Something went wrong. Please try again."); - } - } - - } -} \ No newline at end of file diff --git a/Core/BlockmakerClient.cs.meta b/Core/BlockmakerClient.cs.meta deleted file mode 100644 index 8117dce..0000000 --- a/Core/BlockmakerClient.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e5be56d05799a4cf5b56f2d33362c74c \ No newline at end of file diff --git a/Core/BlockmakerConfig.cs b/Core/BlockmakerConfig.cs deleted file mode 100644 index 726b69c..0000000 --- a/Core/BlockmakerConfig.cs +++ /dev/null @@ -1,82 +0,0 @@ -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Stores Blockmaker connection settings. - /// Create one via Assets → Create → Blockmaker → Config - /// and assign it to BlockmakerClient in the scene. - /// - /// Never commit your API key to source control — - /// use a separate Config asset that lives outside your Assets/Scenes folder - /// and is listed in .gitignore, or load it from environment at build time. - /// - [CreateAssetMenu(fileName = "BlockmakerConfig", menuName = "Blockmaker/Config")] - public class BlockmakerConfig : ScriptableObject - { - [Header("Server")] - [Tooltip("Optional — your Blockmaker server URL. Leave empty to use the shared default.")] - public string serverUrl = ""; - - internal const string DefaultServerUrl = "https://blockmaker-production.up.railway.app"; - - [Tooltip("Short unique ID for your game (e.g. 'myrace'). Used to namespace saved sessions so multiple Blockmaker games on the same device don't conflict. If left empty, falls back to Application.identifier.")] - public string gameId = ""; - - [Header("Auth")] - [Tooltip("Your Blockmaker API key — sk_ prefix, 48 hex chars")] - public string apiKey = ""; - - [Header("WalletConnect")] - [Tooltip("Optional — your own WalletConnect project ID from https://cloud.walletconnect.com. Leave empty to use the Blockmaker shared ID (works out of the box).")] - public string walletConnectProjectId = ""; - - internal const string DefaultWalletConnectProjectId = "dc1f45e68a8fb53fa03adb645365c9bd"; - - [Header("Magic SDK (Email Wallet)")] - [Tooltip("Magic publishable API key (pk_live_ prefix). Get one at https://dashboard.magic.link")] - public string magicPublishableKey = ""; - - [Tooltip("Enable Magic SDK for email login. Creates a client-side wallet — private key stays on the player's device.")] - public bool enableMagicEmail = true; - - [Header("xChain EVM")] - [Tooltip("Enable xChain Accounts — lets EVM wallet users sign Algorand transactions from " + - "their existing wallet. Works on WebGL (any installed browser wallet via EIP-6963) " + - "and native (WalletConnect QR). BETA: off by default while it matures — flip on " + - "to show EVM wallets in the connect modal.")] - public bool enableEvmXChain = false; - - [Header("Branding")] - [Tooltip("URL shown in wallet apps when players approve connections. Defaults to your server URL if empty.")] - public string dAppUrl = ""; - - [Tooltip("Icon URL shown in wallet apps. Leave empty for default.")] - public string dAppIconUrl = ""; - - [Header("Timeouts (seconds)")] - public float defaultTimeoutSeconds = 10f; - public float longRequestTimeoutSeconds = 20f; - [Tooltip("Timeout for wallet API queries (e.g. NFT search). Not for signing — see walletSignTimeoutSeconds.")] - public float walletTimeoutSeconds = 30f; - [Tooltip("Timeout for interactive wallet operations (QR scan, transaction approval). Set high because the player must interact with their wallet app.")] - public float walletSignTimeoutSeconds = 120f; - - private void OnValidate() - { - // Server URL and API key are optional — shared defaults are used when empty. - if (!string.IsNullOrEmpty(serverUrl) && - !serverUrl.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase) && - !serverUrl.StartsWith("http://localhost", System.StringComparison.OrdinalIgnoreCase) && - !serverUrl.StartsWith("http://127.0.0.1", System.StringComparison.OrdinalIgnoreCase)) - BlockmakerLog.Warning("[BlockmakerConfig] Server URL does not use HTTPS. Use HTTPS in production."); - - if (!string.IsNullOrEmpty(apiKey) && !apiKey.StartsWith("sk_")) - BlockmakerLog.Warning("[BlockmakerConfig] API key should start with 'sk_'."); - - // WalletConnect Project ID is optional — falls back to Blockmaker's shared ID. - // Magic SDK is WebGL-only. Email login via OTP works on all platforms. - } - } -} \ No newline at end of file diff --git a/Core/BlockmakerConfig.cs.meta b/Core/BlockmakerConfig.cs.meta deleted file mode 100644 index dd91384..0000000 --- a/Core/BlockmakerConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d94481a5ea87a4a6cbfb7804fefc7bcb \ No newline at end of file diff --git a/Core/BlockmakerErrors.cs b/Core/BlockmakerErrors.cs deleted file mode 100644 index 3219dc2..0000000 --- a/Core/BlockmakerErrors.cs +++ /dev/null @@ -1,42 +0,0 @@ - -namespace Blockmaker -{ - /// - /// Helpers for classifying error strings returned by sign and auth callbacks. - /// Use these to decide whether to retry, show specific UI, or fail gracefully. - /// - public static class BlockmakerErrors - { - public static bool IsTimeout(string error) => - error != null && (error.IndexOf("timed out", System.StringComparison.OrdinalIgnoreCase) >= 0 || - error.IndexOf("timeout", System.StringComparison.OrdinalIgnoreCase) >= 0); - - public static bool IsSessionExpired(string error) => - error != null && (error.Contains("session has expired") || - error.Contains("session has ended") || - error.Contains("sign in again")); - - public static bool IsUserCancelled(string error) => - error != null && (error.Contains("not approved") || - error.Contains("cancelled") || - error.Contains("rejected") || - error.Contains("User closed") || - error.Contains("User denied")); - - public static bool IsNotConnected(string error) => - error != null && (error.Contains("not connected") || - error.Contains("connection was lost") || - error.Contains("connect your wallet again")); - - public static bool IsPlatformUnsupported(string error) => - error != null && (error.Contains("only available in the web browser") || - error.Contains("not available right now")); - - public static bool IsInterrupted(string error) => - error != null && error.Contains("interrupted"); - - public static bool IsRetryable(string error) => - IsTimeout(error) || IsNotConnected(error) || IsInterrupted(error); - } - -} \ No newline at end of file diff --git a/Core/BlockmakerErrors.cs.meta b/Core/BlockmakerErrors.cs.meta deleted file mode 100644 index 2c83fa6..0000000 --- a/Core/BlockmakerErrors.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9ce06a0db52f24ef58dd346bbe3a647b \ No newline at end of file diff --git a/Core/BlockmakerLog.cs b/Core/BlockmakerLog.cs deleted file mode 100644 index 1bc55dc..0000000 --- a/Core/BlockmakerLog.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Diagnostics; -using UnityEngine; -using Debug = UnityEngine.Debug; - -namespace Blockmaker -{ - - /// - /// Centralized logging for the Blockmaker SDK. - /// All SDK log output goes through here so game developers can control verbosity. - /// - /// Default behavior: - /// - Editor / Development builds: all levels enabled - /// - Release builds: only warnings and errors - /// - /// To silence SDK logs entirely: - /// BlockmakerLog.Level = BlockmakerLogLevel.None; - /// - /// To get full diagnostics: - /// BlockmakerLog.Level = BlockmakerLogLevel.Verbose; - /// - /// To intercept logs (e.g. send to your own analytics): - /// BlockmakerLog.OnLog += (level, msg) => MyAnalytics.Track(msg); - /// - public static class BlockmakerLog - { - public static BlockmakerLogLevel Level { get; set; } = - Debug.isDebugBuild ? BlockmakerLogLevel.Verbose : BlockmakerLogLevel.Warning; - - /// - /// Optional hook for game code to intercept all SDK log messages. - /// Fires regardless of Level — filtering is the subscriber's responsibility. - /// - public static event Action OnLog; - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() - { - Level = Debug.isDebugBuild ? BlockmakerLogLevel.Verbose : BlockmakerLogLevel.Warning; - OnLog = null; - } - - public static void Verbose(string message) - { - OnLog?.Invoke(BlockmakerLogLevel.Verbose, message); - if (Level <= BlockmakerLogLevel.Verbose) - Debug.Log(message); - } - - public static void Info(string message) - { - OnLog?.Invoke(BlockmakerLogLevel.Info, message); - if (Level <= BlockmakerLogLevel.Info) - Debug.Log(message); - } - - public static void Warning(string message) - { - OnLog?.Invoke(BlockmakerLogLevel.Warning, message); - if (Level <= BlockmakerLogLevel.Warning) - Debug.LogWarning(message); - } - - public static void Error(string message) - { - OnLog?.Invoke(BlockmakerLogLevel.Error, message); - if (Level <= BlockmakerLogLevel.Error) - Debug.LogError(message); - } - - public static void Exception(Exception ex) - { - OnLog?.Invoke(BlockmakerLogLevel.Error, ex.ToString()); - if (Level <= BlockmakerLogLevel.Error) - Debug.LogException(ex); - } - } - - public enum BlockmakerLogLevel - { - Verbose = 0, - Info = 1, - Warning = 2, - Error = 3, - None = 4 - } - -} \ No newline at end of file diff --git a/Core/BlockmakerLog.cs.meta b/Core/BlockmakerLog.cs.meta deleted file mode 100644 index 60a3c3c..0000000 --- a/Core/BlockmakerLog.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 5fb8c6e08aaae442b8224f3a0150e56a \ No newline at end of file diff --git a/Core/BlockmakerProfileManager.cs b/Core/BlockmakerProfileManager.cs deleted file mode 100644 index 5b578cf..0000000 --- a/Core/BlockmakerProfileManager.cs +++ /dev/null @@ -1,702 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Networking; - -namespace Blockmaker -{ - - /// - /// Singleton that owns the current player's profile state and orchestrates - /// all profile-mutating flows (claim/change username, onboarding steps). - /// - /// Subscribe to the static events to react to profile changes: - /// BlockmakerProfileManager.OnProfileLoaded - /// BlockmakerProfileManager.OnProfileUpdated - /// BlockmakerProfileManager.OnOnboardingStatusLoaded - /// - /// Read identity-derived display info via the static accessors: - /// BlockmakerProfileManager.Profile - /// BlockmakerProfileManager.DisplayName - /// BlockmakerProfileManager.ProfileImageUrl - /// BlockmakerProfileManager.HasUsername - /// BlockmakerProfileManager.WalletAddress - /// BlockmakerProfileManager.IsEmailTier - /// BlockmakerProfileManager.OnboardingStep - /// - public class BlockmakerProfileManager : MonoBehaviour - { - // ── Singleton ────────────────────────────────────────────────────────────── - public static BlockmakerProfileManager Instance { get; private set; } - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() - { - Instance = null; - _profile = null; - _onboarding = null; - IsProfileLoading = false; - OnProfileLoaded = null; - OnProfileUpdated = null; - OnOnboardingStatusLoaded = null; - } - - // ── Events ───────────────────────────────────────────────────────────────── - public static event Action OnProfileLoaded; - public static event Action OnProfileUpdated; - public static event Action OnOnboardingStatusLoaded; - - // ── Cached state ─────────────────────────────────────────────────────────── - private static BlockmakerProfile _profile; - private static OnboardingStatus _onboarding; - private Texture2D _profileTexture; - - // ── Static accessors ─────────────────────────────────────────────────────── - public static BlockmakerProfile Profile => _profile; - public static OnboardingStatus Onboarding => _onboarding; - public static bool IsProfileLoaded => _profile != null; - public static bool IsProfileLoading { get; private set; } - - public static string DisplayName => - _profile?.displayName - ?? BlockmakerAuth.Instance?.DisplayName - ?? "Guest"; - - public static string ProfileImageUrl => - _profile?.profileImageUrl ?? string.Empty; - - /// Asset ID of the NFT set as the player's profile picture. 0 = not set. - public static long ProfilePicAssetId => - _profile?.profilePicAssetId ?? 0; - - public static bool HasUsername => - !string.IsNullOrEmpty(_profile?.username); - - public static string WalletAddress => - _profile?.walletAddress - ?? BlockmakerAuth.Instance?.Address - ?? string.Empty; - - public static bool IsEmailTier => - string.Equals(_profile?.tier, "email", StringComparison.OrdinalIgnoreCase); - - public static string OnboardingStep => - _profile?.onboardingStep ?? "none"; - - private static readonly List _emptyBalances = new List(); - public static List TokenBalances => - _onboarding?.tokenBalances ?? _emptyBalances; - - // ── Lifecycle ────────────────────────────────────────────────────────────── - - private void Awake() - { - if (Instance != null && Instance != this) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); - } - - private void OnEnable() - { - BlockmakerAuth.OnIdentityChanged += HandleIdentityChanged; - } - - private void OnDisable() - { - BlockmakerAuth.OnIdentityChanged -= HandleIdentityChanged; - } - - private void OnDestroy() - { - if (_profileTexture != null) { Destroy(_profileTexture); _profileTexture = null; } - - if (Instance == this) - { - _profile = null; - _onboarding = null; - Instance = null; - } - } - - // ── Identity change ──────────────────────────────────────────────────────── - - private int _generation; - private Coroutine _profileCoroutine; - private Coroutine _onboardingCoroutine; - - private void HandleIdentityChanged(IBlockmakerIdentity identity) - { - if (Instance != this) return; - if (_profileCoroutine != null) { StopCoroutine(_profileCoroutine); _profileCoroutine = null; } - if (_onboardingCoroutine != null) { StopCoroutine(_onboardingCoroutine); _onboardingCoroutine = null; } - - _generation++; - _profile = null; - _onboarding = null; - IsProfileLoading = false; - _loadingTexture = false; - _pendingTextureCallbacks.Clear(); - if (_profileTexture != null) { Destroy(_profileTexture); _profileTexture = null; } - - if (identity == null || identity.Tier == IdentityTier.Guest) return; - if (!identity.HasWallet) return; - if (!isActiveAndEnabled) return; - - // A restored wallet identity often has NO backend session yet (the JWT arrives - // after the wallet-signature login round-trip) — loading now just 401s. When - // the login completes, OnIdentityChanged re-fires and we land here again. - if (BlockmakerClient.Instance == null || !BlockmakerClient.Instance.HasBackendSession) return; - - _profileCoroutine = StartCoroutine(LoadProfileRoutine(_generation)); - _onboardingCoroutine = StartCoroutine(LoadOnboardingStatusRoutine(_generation)); - } - - // ── Public refresh methods ───────────────────────────────────────────────── - - public void RefreshProfile() - { - if (BlockmakerAuth.Instance == null || !BlockmakerAuth.Instance.HasWallet) return; - if (_profileCoroutine != null) StopCoroutine(_profileCoroutine); - _profileCoroutine = StartCoroutine(LoadProfileRoutine(_generation)); - } - - public void RefreshOnboardingStatus() - { - if (BlockmakerAuth.Instance == null || !BlockmakerAuth.Instance.HasWallet) return; - if (_onboardingCoroutine != null) StopCoroutine(_onboardingCoroutine); - _onboardingCoroutine = StartCoroutine(LoadOnboardingStatusRoutine(_generation)); - } - - // ── Profile load ─────────────────────────────────────────────────────────── - - private const float SERVER_TIMEOUT = 30f; - - private IEnumerator LoadProfileRoutine(int gen) - { - if (BlockmakerClient.Instance == null) { IsProfileLoading = false; yield break; } - - IsProfileLoading = true; - bool done = false; - - BlockmakerClient.Instance.GetProfile( - result => - { - if (gen != _generation) { done = true; return; } - if (result?.success == true && result.profile != null) - { - bool isFirst = (_profile == null); - _profile = result.profile; - - if (isFirst) SafeInvoke(OnProfileLoaded, _profile); - else SafeInvoke(OnProfileUpdated, _profile); - } - done = true; - }, - err => { BlockmakerLog.Info($"[BlockmakerProfileManager] Profile load failed: {err}"); done = true; } - ); - - float elapsed = 0f; - while (!done && elapsed < SERVER_TIMEOUT) - { - if (gen != _generation) { IsProfileLoading = false; yield break; } - elapsed += Time.unscaledDeltaTime; - yield return null; - } - IsProfileLoading = false; - } - - private IEnumerator LoadOnboardingStatusRoutine(int gen) - { - if (BlockmakerClient.Instance == null) yield break; - - bool done = false; - - BlockmakerClient.Instance.GetOnboardingStatus( - result => - { - if (gen != _generation) { done = true; return; } - if (result?.success == true) - { - _onboarding = result; - SafeInvoke(OnOnboardingStatusLoaded, _onboarding); - } - done = true; - }, - err => { BlockmakerLog.Info($"[BlockmakerProfileManager] Onboarding status load failed: {err}"); done = true; } - ); - - float elapsed = 0f; - while (!done && elapsed < SERVER_TIMEOUT) - { - if (gen != _generation) yield break; - elapsed += Time.unscaledDeltaTime; - yield return null; - } - } - - // ── Profile texture ──────────────────────────────────────────────────────── - - private bool _loadingTexture; - private readonly List> _pendingTextureCallbacks = new List>(); - - /// - /// Load the profile image as a Texture2D. Cached after first load. - /// Callback receives null if no image is set or the load fails. - /// - public void LoadProfileTexture(Action callback) - { - if (_profileTexture != null) { callback?.Invoke(_profileTexture); return; } - - if (_loadingTexture) - { - if (callback != null) _pendingTextureCallbacks.Add(callback); - return; - } - - string url = ProfileImageUrl; - if (string.IsNullOrEmpty(url)) { callback?.Invoke(null); return; } - - if (callback != null) _pendingTextureCallbacks.Add(callback); - _loadingTexture = true; - StartCoroutine(LoadTextureRoutine(url, tex => - { - _loadingTexture = false; - if (url != ProfileImageUrl) - { - if (tex != null) Destroy(tex); - _pendingTextureCallbacks.Clear(); - return; - } - _profileTexture = tex; - foreach (var cb in _pendingTextureCallbacks) - cb?.Invoke(tex); - _pendingTextureCallbacks.Clear(); - })); - } - - private IEnumerator LoadTextureRoutine(string url, Action callback) - { - using var req = UnityWebRequestTexture.GetTexture(url); - req.timeout = 15; - yield return req.SendWebRequest(); - - if (req.result == UnityWebRequest.Result.Success) - callback?.Invoke(DownloadHandlerTexture.GetContent(req)); - else - callback?.Invoke(null); - } - - // ── Username flow ────────────────────────────────────────────────────────── - - /// - /// Full claim flow: prepare (server) → sign (wallet) → complete (server) → refresh. - /// Requires BlockmakerAuth.Instance.CanSign == true. - /// - public void ClaimUsername( - string username, - Action onSuccess, - Action onError = null) - { - StartCoroutine(ClaimUsernameRoutine(username, onSuccess, onError)); - } - - private IEnumerator ClaimUsernameRoutine( - string username, - Action onSuccess, - Action onError) - { - int gen = _generation; - - var auth = BlockmakerAuth.Instance; - if (auth == null || !auth.CanSign) - { - onError?.Invoke("Please connect a wallet to continue."); - yield break; - } - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - // ── Step 1: prepare ──────────────────────────────────────────────────── - UsernamePrepareResult prepared = null; - string prepareError = null; - bool prepDone = false; - - BlockmakerClient.Instance.PrepareUsernameClaim(username, - r => { prepared = r; prepDone = true; }, - e => { prepareError = e; prepDone = true; } - ); - - float elapsed = 0f; - while (!prepDone && elapsed < SERVER_TIMEOUT) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!prepDone) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) { onError?.Invoke("Your account changed during this action. Please try again."); yield break; } - - if (prepareError != null || prepared?.success != true) - { - onError?.Invoke(prepareError ?? prepared?.error ?? "We couldn't set up that username. Please try again."); - yield break; - } - - // ── Step 2: sign group ──────────────────────────────────────────────── - string[] signedTxns = null; - string signError = null; - bool signDone = false; - - var txnsToSign = (prepared.unsignedTxnsBase64 != null && prepared.unsignedTxnsBase64.Length > 0) - ? prepared.unsignedTxnsBase64 - : new[] { prepared.unsignedTxnBase64 }; - yield return auth.Identity.SignTransactions( - txnsToSign, - s => { signedTxns = s; signDone = true; }, - e => { signError = e; signDone = true; } - ); - - if (!signDone) - { - elapsed = 0f; - while (!signDone && elapsed < 120f) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - } - if (!signDone) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) { onError?.Invoke("Your account changed during this action. Please try again."); yield break; } - - if (signError != null || signedTxns == null || signedTxns.Length == 0) - { - onError?.Invoke(signError ?? "You cancelled the request. Please try again when you're ready."); - yield break; - } - - // ── Step 3: complete ─────────────────────────────────────────────────── - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - UsernameClaimResult claimResult = null; - string claimError = null; - bool claimDone = false; - - BlockmakerClient.Instance.CompleteUsernameClaim( - prepared.reservationId, - signedTxns, - r => { claimResult = r; claimDone = true; }, - e => { claimError = e; claimDone = true; } - ); - - elapsed = 0f; - while (!claimDone && elapsed < SERVER_TIMEOUT) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!claimDone) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) { onError?.Invoke("Your account changed during this action. Please try again."); yield break; } - - if (claimError != null || claimResult?.success != true) - { - onError?.Invoke(claimError ?? claimResult?.error ?? "We couldn't finish claiming that username. Please try again."); - yield break; - } - - // ── Refresh and notify ───────────────────────────────────────────────── - if (claimResult.profile != null) - { - _profile = claimResult.profile; - SafeInvoke(OnProfileUpdated, _profile); - } - - onSuccess?.Invoke(_profile ?? claimResult.profile); - } - - /// - /// Full change flow: prepare → sign → complete → refresh. - /// Player must already have a username. - /// - public void ChangeUsername( - string newUsername, - Action onSuccess, - Action onError = null) - { - StartCoroutine(ChangeUsernameRoutine(newUsername, onSuccess, onError)); - } - - private IEnumerator ChangeUsernameRoutine( - string newUsername, - Action onSuccess, - Action onError) - { - int gen = _generation; - - var auth = BlockmakerAuth.Instance; - if (auth == null || !auth.CanSign) - { - onError?.Invoke("Please connect a wallet to continue."); - yield break; - } - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - // ── Step 1: prepare ──────────────────────────────────────────────────── - UsernamePrepareResult prepared = null; - string prepareError = null; - bool prepDone = false; - - BlockmakerClient.Instance.PrepareUsernameChange(newUsername, - r => { prepared = r; prepDone = true; }, - e => { prepareError = e; prepDone = true; } - ); - - float elapsed = 0f; - while (!prepDone && elapsed < SERVER_TIMEOUT) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!prepDone) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) { onError?.Invoke("Your account changed during this action. Please try again."); yield break; } - - if (prepareError != null || prepared?.success != true) - { - onError?.Invoke(prepareError ?? prepared?.error ?? "We couldn't set up the username change. Please try again."); - yield break; - } - - // ── Step 2: sign group ──────────────────────────────────────────────── - string[] signedTxns = null; - string signError = null; - bool signDone = false; - - var txnsToSign = (prepared.unsignedTxnsBase64 != null && prepared.unsignedTxnsBase64.Length > 0) - ? prepared.unsignedTxnsBase64 - : new[] { prepared.unsignedTxnBase64 }; - yield return auth.Identity.SignTransactions( - txnsToSign, - s => { signedTxns = s; signDone = true; }, - e => { signError = e; signDone = true; } - ); - - if (!signDone) - { - elapsed = 0f; - while (!signDone && elapsed < 120f) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - } - if (!signDone) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) { onError?.Invoke("Your account changed during this action. Please try again."); yield break; } - - if (signError != null || signedTxns == null || signedTxns.Length == 0) - { - onError?.Invoke(signError ?? "You cancelled the request. Please try again when you're ready."); - yield break; - } - - // ── Step 3: complete ─────────────────────────────────────────────────── - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - UsernameClaimResult changeResult = null; - string changeError = null; - bool changeDone = false; - - BlockmakerClient.Instance.CompleteUsernameChange( - prepared.reservationId, - signedTxns, - r => { changeResult = r; changeDone = true; }, - e => { changeError = e; changeDone = true; } - ); - - elapsed = 0f; - while (!changeDone && elapsed < SERVER_TIMEOUT) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!changeDone) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) { onError?.Invoke("Your account changed during this action. Please try again."); yield break; } - - if (changeError != null || changeResult?.success != true) - { - onError?.Invoke(changeError ?? changeResult?.error ?? "We couldn't finish changing your username. Please try again."); - yield break; - } - - if (changeResult.profile != null) - { - _profile = changeResult.profile; - SafeInvoke(OnProfileUpdated, _profile); - } - - onSuccess?.Invoke(_profile ?? changeResult.profile); - } - - // ── Profile picture NFT ──────────────────────────────────────────────────── - - /// - /// Set the player's profile picture to an NFT they own. - /// The server verifies ownership via the Algorand indexer, resolves the - /// image URL from ARC metadata, and persists it on the profile. - /// On success the cached texture is cleared so the new image loads fresh. - /// - public void SetProfilePicNft( - long assetId, - Action onSuccess, - Action onError = null) - { - StartCoroutine(SetProfilePicNftRoutine(assetId, onSuccess, onError)); - } - - private IEnumerator SetProfilePicNftRoutine( - long assetId, - Action onSuccess, - Action onError) - { - int gen = _generation; - - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - SetProfilePicResult result = null; - string error = null; - bool done = false; - - BlockmakerClient.Instance.SetProfilePicNft(assetId, - r => { result = r; done = true; }, - e => { error = e; done = true; } - ); - - float elapsed = 0f; - while (!done && elapsed < SERVER_TIMEOUT) - { - if (gen != _generation) yield break; - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!done) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) yield break; - - if (error != null || result?.success != true) - { - onError?.Invoke(error ?? result?.error ?? "We couldn't update your profile picture. Please try again."); - yield break; - } - - if (result.profile != null) - { - _profile = result.profile; - if (_profileTexture != null) { Destroy(_profileTexture); _profileTexture = null; } - SafeInvoke(OnProfileUpdated, _profile); - } - - onSuccess?.Invoke(_profile ?? result.profile); - } - - // ── Default avatar ────────────────────────────────────────────────────────── - - public void SetDefaultAvatar( - string avatarId, - Action onSuccess, - Action onError = null) - { - StartCoroutine(SetDefaultAvatarRoutine(avatarId, onSuccess, onError)); - } - - private IEnumerator SetDefaultAvatarRoutine( - string avatarId, - Action onSuccess, - Action onError) - { - int gen = _generation; - - if (BlockmakerClient.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - SetProfilePicResult result = null; - string error = null; - bool done = false; - - BlockmakerClient.Instance.SetDefaultAvatar(avatarId, - r => { result = r; done = true; }, - e => { error = e; done = true; } - ); - - float elapsed = 0f; - while (!done && elapsed < SERVER_TIMEOUT) - { - if (gen != _generation) yield break; - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!done) { onError?.Invoke("The request timed out. Please check your connection and try again."); yield break; } - if (gen != _generation) yield break; - - if (error != null || result?.success != true) - { - onError?.Invoke(error ?? result?.error ?? "We couldn't update your avatar. Please try again."); - yield break; - } - - if (result.profile != null) - { - _profile = result.profile; - if (_profileTexture != null) { Destroy(_profileTexture); _profileTexture = null; } - SafeInvoke(OnProfileUpdated, _profile); - } - - onSuccess?.Invoke(_profile ?? result.profile); - } - - // ── Safe event helpers ────────────────────────────────────────────────────── - - private static void SafeInvoke(Action handler, T arg) - { - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(arg); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - // ── Onboarding helpers ───────────────────────────────────────────────────── - - /// - /// Returns true if the current player should see an onboarding nudge. - /// Only true for non-guest players who haven't completed onboarding. - /// - public static bool ShouldShowOnboardingNudge() - { - if (BlockmakerAuth.Instance == null) return false; - if (BlockmakerAuth.Instance.Tier == IdentityTier.Guest) return false; - var step = OnboardingStep; - return step != "complete"; - } - } - -} \ No newline at end of file diff --git a/Core/BlockmakerProfileManager.cs.meta b/Core/BlockmakerProfileManager.cs.meta deleted file mode 100644 index 55353fc..0000000 --- a/Core/BlockmakerProfileManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 737f86ea0c68448a79987ed0f0305fa9 \ No newline at end of file diff --git a/Core/BlockmakerTypes.cs b/Core/BlockmakerTypes.cs deleted file mode 100644 index 008f305..0000000 --- a/Core/BlockmakerTypes.cs +++ /dev/null @@ -1,551 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Blockmaker -{ - - public enum ProfileTier { Guest, Email, Wallet } - public enum WalletType { Guest, EmailDeviceKey, SelfCustody } - public enum OnboardingStep { None, HasProfile, HasUsername, Complete } - public enum TransactionType { Payment, AssetTransfer, AssetOptIn } - - // ── Flow types ───────────────────────────────────────────────────────────────── - - [Serializable] public class FlowRunRequest - { - public string wallet; - public string context; - } - - [Serializable] public class FlowResult - { - public bool success; - public bool ownsNFT; - public string output; - public string error; - } - - // ── Profile types ────────────────────────────────────────────────────────────── - - [Serializable] public class GameProfileField - { - public string key; - public string label; - public string type; // "number" | "string" | "boolean" | "token_balance" - public string value; - } - - [Serializable] public class GameProfileData - { - public List fields = new List(); - } - - [Serializable] public class BlockmakerProfile - { - public string walletAddress; - public string username; - public string displayName; - public string profileImageUrl; - /// Algorand asset ID of the NFT set as the player's profile picture. 0 = not set. - public long profilePicAssetId; - public string nfdName; - public string tier; // "guest" | "email" | "wallet" - public string walletType; // "guest" | "email_device_key" | "self_custody" - public string onboardingStep; // "none" | "has_profile" | "has_username" | "complete" - public long createdAt; - public GameProfileData gameData; - } - - [Serializable] public class ProfileResponse - { - public bool success; - public BlockmakerProfile profile; - public string error; - } - - [Serializable] public class TokenBalance - { - public long assetId; - public string name; - public string symbol; - public long amount; - public int decimals; - } - - [Serializable] public class OnboardingStatus - { - public bool success; - public string step; - public string nextAction; - public string walletAddress; - public string walletType; - public string walletMessage; - public long walletBalance; - public List tokenBalances = new List(); - public string error; - } - - [Serializable] public class UsernameCheckResult - { - public bool available; - public string username; - public string reason; - public string code; - public string error; - public long priceMicroAlgo; - public float priceAlgo; - } - - [Serializable] public class UsernamePrepareResult - { - public bool success; - public string username; - public string newUsername; - public string oldUsername; - public string unsignedTxnBase64; - public string[] unsignedTxnsBase64; - public string reservationId; - public long priceMicroAlgo; - public float priceAlgo; - public long appId; - public string error; - } - - [Serializable] public class UsernameClaimResult - { - public bool success; - public string username; - public long appId; - public string txId; - public BlockmakerProfile profile; - public string error; - } - - [Serializable] public class ProfileImageResult - { - public bool success; - public string profileImageUrl; - public string cid; - public BlockmakerProfile profile; - public string error; - } - - [Serializable] public class GameDataResponse - { - public bool success; - public string gameId; - public string walletAddress; - // Raw JSON object — use JsonUtility or a custom parser on this field - public string data; - public string error; - } - - [Serializable] public class SchemaField - { - public string key; - public string label; - public string type; // "number" | "string" | "boolean" | "token_balance" - public string defaultValue; - public long assetId; // required when type = "token_balance" - } - - [Serializable] public class SchemaResponse - { - public bool success; - public List schema = new List(); - public string error; - } - - // ── Username request types ──────────────────────────────────────────────────── - - [Serializable] public class UsernameCheckRequest - { - public string username; - public string walletAddress; - public bool isChange; - } - - [Serializable] public class UsernameClaimPrepareRequest - { - public string username; - } - - [Serializable] public class UsernameChangePrepareRequest - { - public string newUsername; - } - - [Serializable] public class UsernameCompleteRequest - { - public string reservationId; - public string signedTxnBase64; - public string[] signedTxnsBase64; - } - - [Serializable] public class OnboardingAdvanceRequest - { - public string step; - } - - // ── Profile picture NFT types ───────────────────────────────────────────────── - - /// - /// POST /v1/profile/pfp - /// Server verifies the player owns the NFT, fetches its ARC image URL, and - /// stores both assetId and imageUrl on the profile. - /// - [Serializable] public class SetProfilePicRequest - { - /// Algorand asset ID of the NFT to use as the profile picture. - public long assetId; - } - - [Serializable] public class SetProfilePicResult - { - public bool success; - /// Resolved image URL fetched from the NFT's ARC metadata. - public string profileImageUrl; - public long assetId; - /// Full updated profile returned after the change is persisted. - public BlockmakerProfile profile; - public string error; - } - - // ── Default avatar types ────────────────────────────────────────────────────── - - [Serializable] public class SetDefaultAvatarRequest - { - public string avatarId; - } - - // ── Wallet NFT search types ─────────────────────────────────────────────────── - - [Serializable] public class WalletNFTSearchRequest - { - public string search; - } - - [Serializable] public class WalletNFTAsset - { - public long assetId; - public string name; - public string unitName; - public string imageUrl; - } - - [Serializable] public class WalletNFTSearchResult - { - public bool success; - public List assets = new List(); - public string error; - } - - [Serializable] public class NftImageRequest - { - public long assetId; - } - - [Serializable] public class NftImageResult - { - public bool success; - public long assetId; - public string imageUrl; - public string error; - } - - // ── Wallet holdings types ───────────────────────────────────────────────────── - - [Serializable] public class AssetHolding - { - public long assetId; - public long amount; - } - - [Serializable] public class HoldingsResponse - { - public bool success; - public string walletAddress; - public List holdings = new List(); - public string error; - } - - // ── Collection check types ─────────────────────────────────────────────────── - - [Serializable] public class CollectionCheckRequest - { - public string[] creators; - } - - [Serializable] public class CollectionCheckEntry - { - public string creator; - public bool holds; - public int count; - public bool reliable; - } - - [Serializable] public class CollectionCheckResponse - { - public bool success; - public string walletAddress; - public List results = new List(); - public string error; - public bool degraded; - } - - // ── Asset registry types ───────────────────────────────────────────────────── - - [Serializable] public class AssetRegistryEntry - { - public string id; - public string slug; - public string name; - public string type; - public string creatorAddress; - public string unitName; - public long assetId; - public int decimals; - public string imageUrl; - public long totalSupply; - public int verified; - } - - [Serializable] public class AssetRegistryResponse - { - public bool success; - public List entries = new List(); - public string error; - } - - // ── Magic auth types ────────────────────────────────────────────────────────── - - [Serializable] public class MagicVerifyRequest - { - public string didToken; - public string email; - } - - // ── Email auth types ─────────────────────────────────────────────────────────── - - [Serializable] public class EmailOTPRequest - { - public string email; - } - - /// Returned by POST /v1/auth/email/request. - [Serializable] public class OTPRequestResult - { - public bool success; - public string error; - } - - [Serializable] public class EmailVerifyRequest - { - public string email; - public string otp; - } - - /// Returned by POST /v1/auth/email/verify. - [Serializable] public class EmailVerifyResult - { - public bool success; - public string walletAddress; // managed Algorand wallet for this email - public string sessionToken; // JWT — pass as Bearer token to /v1/auth/sign - public string refreshToken; // long-lived refresh token for token rotation - public string displayName; - public bool isNewAccount; // true on first login (wallet was just created) - public string error; - } - - // ── Wallet-signature auth types ───────────────────────────────────────────────── - - /// Request body for POST /v1/auth/wallet/challenge. - [Serializable] public class WalletChallengeRequest - { - public string walletAddress; // Algorand address (derived address for EVM xChain) - public string chain; // "algorand" | "evm" - public string evmAddress; // required when chain == "evm"; null otherwise - } - - /// Returned by POST /v1/auth/wallet/challenge. - [Serializable] public class WalletChallengeResult - { - public bool success; - public string nonce; // base64url single-use nonce - public string message; // EXACT bytes to sign (UTF-8); sign byte-for-byte - public long expiresAt; // epoch ms - public string error; - } - - /// Request body for POST /v1/auth/wallet/verify. - [Serializable] public class WalletVerifyRequest - { - public string walletAddress; // same value sent to /challenge - public string chain; // "algorand" | "evm" - public string signature; // evm only: 0x… personal_sign hex (null for algorand) - public string signedTxn; // algorand only: base64 of the SIGNED 0-amount self-payment - // whose note == nonce (null for evm) - public string nonce; // echoes the /challenge nonce - public string evmAddress; // required when chain == "evm"; null otherwise - } - - // ── Token refresh types ────────────────────────────────────────────────────── - - /// Returned by POST /v1/auth/refresh. - [Serializable] public class RefreshTokenResult - { - public bool success; - public string sessionToken; - public string refreshToken; - public string error; - } - - [Serializable] public class RefreshTokenRequest - { - public string refreshToken; - } - - [Serializable] public class LogoutRequest - { - public string refreshToken; - } - - // ── Server-side signing types ────────────────────────────────────────────────── - - [Serializable] public class ServerSignRequest - { - public string unsignedTxnBase64; - public string[] unsignedTxnsBase64; - } - - /// Returned by POST /v1/auth/sign. Supports both single and group signing. - [Serializable] public class ServerSignResult - { - public bool success; - public string signedTxnBase64; - public string[] signedTxnsBase64; - public string txId; - public string error; - } - - [Serializable] public class ServerErrorResponse - { - public bool success; - public string code; - public string error; - } - - [Serializable] public class BlockmakerError - { - public string Code { get; } - public string Message { get; } - public int HttpStatus { get; } - - public BlockmakerError(string code, string message, int httpStatus = 0) - { - Code = code ?? ""; - Message = message ?? "Something went wrong."; - HttpStatus = httpStatus; - } - - public bool IsAuthError => Code == "AUTH_MISSING" || Code == "AUTH_INVALID" || Code == "AUTH_EXPIRED"; - public bool IsConfigError => Code == "SERVER_CONFIG"; - public bool IsRateLimited => Code == "RATE_LIMITED"; - public bool IsNetworkError => Code == "NETWORK" || HttpStatus == 0; - public bool IsTransactionError => Code.StartsWith("TX_") || Code == "INVALID_AMOUNT" || Code == "NOTE_TOO_LONG"; - } - - // ── WalletConnect v1 JSON-RPC response types ───────────────────────────────── - - [Serializable] public class WcJsonRpcResult - { - public long id; - public string[] result; - } - - [Serializable] public class WcJsonRpcError - { - public long id; - public WcRpcError error; - } - - [Serializable] public class WcRpcError - { - public int code; - public string message; - } - - // ── Rewards (generic platform feature) ─────────────────────────────────────── - - [Serializable] public class RewardResult - { - public bool success; - public bool sent; - public string txId; - public string recipientWallet; - public long amountMicroAlgo; - public long assetId; - public string reason; - public string contextId; - public string code; - public string error; - } - - [Serializable] public class RewardRequest - { - public string recipientWallet; - public long assetId; - public long amountMicroAlgo; - public string reason; - public string contextId; - } - - // ── Transaction builder types ─────────────────────────────────────────────── - - [Serializable] public class BuildTransactionRequest - { - public string type; - public string recipient; - public long amount; - public long assetId; - public string note; - public string walletAddress; - - public static string TypeToString(TransactionType t) - { - if (t == TransactionType.AssetTransfer) return "asset_transfer"; - if (t == TransactionType.AssetOptIn) return "asset_optin"; - return "payment"; - } - } - - [Serializable] public class BuildTransactionResult - { - public bool success; - public string unsignedTxnBase64; - public string[] unsignedTxnsBase64; - public string txType; - public string from; - public string code; - public string error; - } - - [Serializable] public class SubmitTransactionRequest - { - public string signedTxnBase64; - public string[] signedTxnsBase64; - } - - [Serializable] public class SubmitTransactionResult - { - public bool success; - public string txId; - public bool alreadyConfirmed; - public string code; - public string error; - } - - // Game-specific types (inventory results, character stats, leaderboard rows, - // match-result requests, etc.) live in the game project, not the SDK. - -} \ No newline at end of file diff --git a/Core/BlockmakerTypes.cs.meta b/Core/BlockmakerTypes.cs.meta deleted file mode 100644 index 9ec620f..0000000 --- a/Core/BlockmakerTypes.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f5cece798021b422bbab8bca2c12ba79 \ No newline at end of file diff --git a/Core/BlockmakerUIUtils.cs b/Core/BlockmakerUIUtils.cs deleted file mode 100644 index 7bcd2a6..0000000 --- a/Core/BlockmakerUIUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -using UnityEngine; - -namespace Blockmaker -{ - - public static class BlockmakerUIUtils - { - public static string ShortenAddress(string address, int prefixLen = 6, int suffixLen = 6) - { - if (string.IsNullOrEmpty(address) || address.Length <= prefixLen + suffixLen + 1) - return address ?? string.Empty; - return $"{address[..prefixLen]}…{address[^suffixLen..]}"; - } - } - -} \ No newline at end of file diff --git a/Core/BlockmakerUIUtils.cs.meta b/Core/BlockmakerUIUtils.cs.meta deleted file mode 100644 index 50f84a9..0000000 --- a/Core/BlockmakerUIUtils.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b400bd02452e0403ea0f97df0dc88817 \ No newline at end of file diff --git a/Core/DefaultAvatarSet.cs b/Core/DefaultAvatarSet.cs deleted file mode 100644 index be9aba1..0000000 --- a/Core/DefaultAvatarSet.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace Blockmaker -{ - - [CreateAssetMenu(fileName = "DefaultAvatars", menuName = "Blockmaker/Default Avatar Set")] - public class DefaultAvatarSet : ScriptableObject - { - public List avatars = new List(); - } - - [Serializable] - public class DefaultAvatarEntry - { - public string id; - public string name; - public Sprite thumbnail; - } - -} \ No newline at end of file diff --git a/Core/DefaultAvatarSet.cs.meta b/Core/DefaultAvatarSet.cs.meta deleted file mode 100644 index 48ee289..0000000 --- a/Core/DefaultAvatarSet.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 18f1a35a78ba343018aed731cf2d96bb \ No newline at end of file diff --git a/Core/IBlockmakerIdentity.cs b/Core/IBlockmakerIdentity.cs deleted file mode 100644 index 71243cb..0000000 --- a/Core/IBlockmakerIdentity.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections; - -namespace Blockmaker -{ - - /// - /// The single identity abstraction used throughout Blockmaker and any game - /// built on top of it. Game code never references a specific wallet or - /// auth provider — it always talks to IBlockmakerIdentity. - /// - /// Three tiers exist (see IdentityTier): - /// Guest — device ID only, no signing capability - /// Email — email login, server-managed wallet, transparent signing - /// SelfCustody — user-owned wallet (Pera, Defly, etc.), in-browser signing - /// - public interface IBlockmakerIdentity - { - // ── Identity ─────────────────────────────────────────────────────────────── - - /// Algorand wallet address, or a local device ID for guests. - string Address { get; } - - /// Human-readable name for UI display. - string DisplayName { get; } - - /// Which auth tier this identity represents. - IdentityTier Tier { get; } - - /// Provider label e.g. "Pera", "Defly", "Email", "Guest". - string ProviderName { get; } - - /// True if this identity has a real on-chain Algorand address. - bool HasWallet { get; } - - /// True if this identity can sign transactions (Email + SelfCustody). - bool CanSign { get; } - - /// - /// The player's Algorand address if they have a wallet, null otherwise. - /// Convenience property — same as checking HasWallet then reading Address. - /// - string AlgorandAddress => HasWallet ? Address : null; - - /// - /// True if this identity can sign an atomic group of transactions in a single - /// wallet approval. Override to false for identities that require N separate - /// wallet popups (e.g. EVM xChain on native platforms). - /// - bool SupportsAtomicGroupSign => true; - - // ── Signing ──────────────────────────────────────────────────────────────── - - /// - /// Sign a single base64-encoded unsigned msgpack transaction. - /// For Email tier: the server signs on the player's behalf. - /// For SelfCustody: opens the wallet app for user approval. - /// For Guest: always calls onError — check CanSign first. - /// - IEnumerator SignTransaction( - string unsignedTxnBase64, - Action onSigned, - Action onError); - - /// - /// Sign a group of base64-encoded unsigned msgpack transactions. - /// All transactions in the group are signed atomically. - /// - IEnumerator SignTransactions( - string[] unsignedTxnsBase64, - Action onSigned, - Action onError); - - // ── Session ──────────────────────────────────────────────────────────────── - - /// Persist session data so it can be restored on next launch. - void SaveSession(); - - /// Clear all session data (logout). - void ClearSession(); - } - - // ── Supporting types ─────────────────────────────────────────────────────────── - - public enum IdentityTier - { - /// No account. Local device ID only. Cannot sign transactions. - Guest = 0, - - /// Email login with a server-managed Algorand wallet. - Email = 1, - - /// Self-custody wallet (Pera, Defly). User signs in-browser. - SelfCustody = 2 - } -} \ No newline at end of file diff --git a/Core/IBlockmakerIdentity.cs.meta b/Core/IBlockmakerIdentity.cs.meta deleted file mode 100644 index 6d5a38c..0000000 --- a/Core/IBlockmakerIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c1e52d01427fd449087f6504bcb46adb \ No newline at end of file diff --git a/Core/QRCodeEncoder.cs b/Core/QRCodeEncoder.cs deleted file mode 100644 index 757cd73..0000000 --- a/Core/QRCodeEncoder.cs +++ /dev/null @@ -1,554 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Blockmaker -{ - - /// - /// Self-contained QR code encoder. No external dependencies. - /// Supports byte-mode encoding with error correction level M. - /// Returns a bool[,] module matrix suitable for rendering. - /// - public static class QRCodeEncoder - { - public static bool[,] Encode(string text) - { - byte[] data = Encoding.UTF8.GetBytes(text); - int version = FindMinVersion(data.Length); - if (version < 0) - throw new ArgumentException("Data too long for QR code"); - - int totalDataCW = TotalDataCodewords(version); - byte[] encoded = EncodeData(data, version, totalDataCW); - byte[] withEC = AddErrorCorrection(encoded, version); - bool[,] matrix = BuildMatrix(version, withEC); - return matrix; - } - - // ── Version selection ──────────────────────────────────────────────────── - - static int FindMinVersion(int byteCount) - { - for (int v = 1; v <= 40; v++) - { - int overhead = v < 10 ? 2 : 3; - int cap = TotalDataCodewords(v) - overhead; - if (cap >= byteCount) return v; - } - return -1; - } - - // ── Data encoding (byte mode = 0100) ───────────────────────────────────── - - static byte[] EncodeData(byte[] data, int version, int totalDataCW) - { - var bits = new BitBuffer(); - bits.Append(0b0100, 4); // byte mode indicator - int lenBits = version < 10 ? 8 : 16; - bits.Append(data.Length, lenBits); - foreach (byte b in data) - bits.Append(b, 8); - bits.Append(0, Math.Min(4, totalDataCW * 8 - bits.Length)); - while (bits.Length % 8 != 0) - bits.Append(0, 1); - byte[] pads = { 0xEC, 0x11 }; - for (int i = 0; bits.Length < totalDataCW * 8; i++) - bits.Append(pads[i % 2], 8); - return bits.ToBytes(); - } - - // ── Error correction ───────────────────────────────────────────────────── - - static byte[] AddErrorCorrection(byte[] data, int version) - { - var spec = GetECSpec(version); - int ecCWPerBlock = spec.ecCWPerBlock; - var dataBlocks = new List(); - var ecBlocks = new List(); - int offset = 0; - foreach (var (count, dcw) in spec.blocks) - { - for (int i = 0; i < count; i++) - { - byte[] block = new byte[dcw]; - Array.Copy(data, offset, block, 0, dcw); - offset += dcw; - dataBlocks.Add(block); - ecBlocks.Add(ReedSolomon(block, ecCWPerBlock)); - } - } - var result = new List(); - int maxData = 0; - foreach (var b in dataBlocks) - if (b.Length > maxData) maxData = b.Length; - for (int i = 0; i < maxData; i++) - foreach (var b in dataBlocks) - if (i < b.Length) result.Add(b[i]); - for (int i = 0; i < ecCWPerBlock; i++) - foreach (var b in ecBlocks) - if (i < b.Length) result.Add(b[i]); - int remainder = RemainderBits(version); - // Pad remainder bits (not full bytes — handled during placement) - return result.ToArray(); - } - - // ── Reed-Solomon over GF(256) ──────────────────────────────────────────── - - static readonly int[] GF_EXP = new int[512]; - static readonly int[] GF_LOG = new int[256]; - - static QRCodeEncoder() - { - int x = 1; - for (int i = 0; i < 255; i++) - { - GF_EXP[i] = x; - GF_LOG[x] = i; - x <<= 1; - if (x >= 256) x ^= 0x11D; - } - for (int i = 255; i < 512; i++) - GF_EXP[i] = GF_EXP[i - 255]; - } - - static int GFMul(int a, int b) - { - if (a == 0 || b == 0) return 0; - return GF_EXP[GF_LOG[a] + GF_LOG[b]]; - } - - static byte[] ReedSolomon(byte[] data, int ecLen) - { - int[] gen = GeneratorPoly(ecLen); - int[] msg = new int[data.Length + ecLen]; - for (int i = 0; i < data.Length; i++) - msg[i] = data[i]; - for (int i = 0; i < data.Length; i++) - { - int coef = msg[i]; - if (coef == 0) continue; - for (int j = 0; j < gen.Length; j++) - msg[i + j] ^= GFMul(gen[j], coef); - } - byte[] result = new byte[ecLen]; - for (int i = 0; i < ecLen; i++) - result[i] = (byte)msg[data.Length + i]; - return result; - } - - static int[] GeneratorPoly(int degree) - { - int[] poly = { 1 }; - for (int i = 0; i < degree; i++) - { - int[] factor = { 1, GF_EXP[i] }; - poly = PolyMul(poly, factor); - } - return poly; - } - - static int[] PolyMul(int[] a, int[] b) - { - int[] result = new int[a.Length + b.Length - 1]; - for (int i = 0; i < a.Length; i++) - for (int j = 0; j < b.Length; j++) - result[i + j] ^= GFMul(a[i], b[j]); - return result; - } - - // ── Matrix construction ────────────────────────────────────────────────── - - static bool[,] BuildMatrix(int version, byte[] codewords) - { - int size = 17 + version * 4; - bool[,] modules = new bool[size, size]; - bool[,] reserved = new bool[size, size]; - - PlaceFinderPatterns(modules, reserved, size); - PlaceAlignmentPatterns(modules, reserved, version, size); - PlaceTimingPatterns(modules, reserved, size); - ReserveFormatArea(reserved, size); - modules[size - 8, 8] = true; // dark module - - if (version >= 7) - PlaceVersionInfo(modules, reserved, version, size); - - PlaceData(modules, reserved, codewords, version, size); - - int bestMask = 0; - int bestScore = int.MaxValue; - bool[,] bestMatrix = null; - - for (int mask = 0; mask < 8; mask++) - { - bool[,] trial = (bool[,])modules.Clone(); - ApplyMask(trial, reserved, mask, size); - PlaceFormatInfo(trial, mask, size); - int score = EvaluatePenalty(trial, size); - if (score < bestScore) - { - bestScore = score; - bestMask = mask; - bestMatrix = trial; - } - } - return bestMatrix; - } - - static void PlaceFinderPatterns(bool[,] m, bool[,] r, int size) - { - int[][] positions = { new[] { 0, 0 }, new[] { size - 7, 0 }, new[] { 0, size - 7 } }; - foreach (var pos in positions) - { - int row = pos[0], col = pos[1]; - for (int dr = -1; dr <= 7; dr++) - for (int dc = -1; dc <= 7; dc++) - { - int rr = row + dr, cc = col + dc; - if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue; - r[rr, cc] = true; - bool inOuter = dr == 0 || dr == 6 || dc == 0 || dc == 6; - bool inInner = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4; - m[rr, cc] = (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) && (inOuter || inInner); - } - } - } - - static void PlaceAlignmentPatterns(bool[,] m, bool[,] r, int version, int size) - { - if (version < 2) return; - int[] coords = AlignmentPositions(version); - for (int i = 0; i < coords.Length; i++) - for (int j = 0; j < coords.Length; j++) - { - int cr = coords[i], cc = coords[j]; - if (r[cr, cc]) continue; - for (int dr = -2; dr <= 2; dr++) - for (int dc = -2; dc <= 2; dc++) - { - int rr = cr + dr, ccc = cc + dc; - r[rr, ccc] = true; - m[rr, ccc] = Math.Abs(dr) == 2 || Math.Abs(dc) == 2 || (dr == 0 && dc == 0); - } - } - } - - static void PlaceTimingPatterns(bool[,] m, bool[,] r, int size) - { - for (int i = 8; i < size - 8; i++) - { - m[6, i] = i % 2 == 0; - r[6, i] = true; - m[i, 6] = i % 2 == 0; - r[i, 6] = true; - } - } - - static void ReserveFormatArea(bool[,] r, int size) - { - for (int c = 0; c <= 8; c++) r[8, c] = true; - for (int rr = 0; rr <= 8; rr++) r[rr, 8] = true; - for (int c = size - 8; c < size; c++) r[8, c] = true; - for (int rr = size - 7; rr < size; rr++) r[rr, 8] = true; - r[size - 8, 8] = true; - } - - static void PlaceVersionInfo(bool[,] m, bool[,] r, int version, int size) - { - int bits = VersionInfoBits(version); - for (int i = 0; i < 18; i++) - { - bool bit = ((bits >> i) & 1) == 1; - int row = i / 3, col = size - 11 + (i % 3); - m[row, col] = bit; r[row, col] = true; - m[col, row] = bit; r[col, row] = true; - } - } - - static void PlaceFormatInfo(bool[,] m, int mask, int size) - { - int bits = FormatInfoBits(mask); - int[] rowPositions = { 0, 1, 2, 3, 4, 5, 7, 8, size - 7, size - 6, size - 5, size - 4, size - 3, size - 2, size - 1 }; - int[] colPositions = { size - 1, size - 2, size - 3, size - 4, size - 5, size - 6, size - 7, size - 8, 7, 5, 4, 3, 2, 1, 0 }; - for (int i = 0; i < 15; i++) - { - bool bit = ((bits >> i) & 1) == 1; - m[8, colPositions[i]] = bit; - m[rowPositions[i], 8] = bit; - } - } - - static void PlaceData(bool[,] m, bool[,] r, byte[] codewords, int version, int size) - { - int bitIdx = 0; - int totalBits = codewords.Length * 8 + RemainderBits(version); - int col = size - 1; - bool upward = true; - while (col >= 0) - { - if (col == 6) col--; - for (int row = 0; row < size; row++) - { - int actualRow = upward ? size - 1 - row : row; - for (int dc = 0; dc <= 1; dc++) - { - int c = col - dc; - if (c < 0 || r[actualRow, c]) continue; - if (bitIdx < totalBits) - { - int byteIdx = bitIdx / 8; - int bitPos = 7 - (bitIdx % 8); - if (byteIdx < codewords.Length) - m[actualRow, c] = ((codewords[byteIdx] >> bitPos) & 1) == 1; - bitIdx++; - } - } - } - upward = !upward; - col -= 2; - } - } - - static void ApplyMask(bool[,] m, bool[,] r, int mask, int size) - { - for (int row = 0; row < size; row++) - for (int col = 0; col < size; col++) - { - if (r[row, col]) continue; - bool flip = mask switch - { - 0 => (row + col) % 2 == 0, - 1 => row % 2 == 0, - 2 => col % 3 == 0, - 3 => (row + col) % 3 == 0, - 4 => (row / 2 + col / 3) % 2 == 0, - 5 => (row * col) % 2 + (row * col) % 3 == 0, - 6 => ((row * col) % 2 + (row * col) % 3) % 2 == 0, - 7 => ((row + col) % 2 + (row * col) % 3) % 2 == 0, - _ => false - }; - if (flip) m[row, col] = !m[row, col]; - } - } - - // ── Penalty evaluation ─────────────────────────────────────────────────── - - static int EvaluatePenalty(bool[,] m, int size) - { - int penalty = 0; - // Rule 1: runs of same color - for (int row = 0; row < size; row++) - { - int run = 1; - for (int col = 1; col < size; col++) - { - if (m[row, col] == m[row, col - 1]) run++; - else { if (run >= 5) penalty += run - 2; run = 1; } - } - if (run >= 5) penalty += run - 2; - } - for (int col = 0; col < size; col++) - { - int run = 1; - for (int row = 1; row < size; row++) - { - if (m[row, col] == m[row - 1, col]) run++; - else { if (run >= 5) penalty += run - 2; run = 1; } - } - if (run >= 5) penalty += run - 2; - } - // Rule 2: 2x2 blocks - for (int row = 0; row < size - 1; row++) - for (int col = 0; col < size - 1; col++) - { - bool v = m[row, col]; - if (v == m[row, col + 1] && v == m[row + 1, col] && v == m[row + 1, col + 1]) - penalty += 3; - } - // Rule 3: finder-like patterns (1011101 0000 or 0000 1011101) - for (int row = 0; row < size; row++) - for (int col = 0; col <= size - 11; col++) - { - if (m[row,col] && !m[row,col+1] && m[row,col+2] && m[row,col+3] && m[row,col+4] && !m[row,col+5] && m[row,col+6] - && !m[row,col+7] && !m[row,col+8] && !m[row,col+9] && !m[row,col+10]) - penalty += 40; - if (!m[row,col] && !m[row,col+1] && !m[row,col+2] && !m[row,col+3] && m[row,col+4] - && !m[row,col+5] && m[row,col+6] && m[row,col+7] && m[row,col+8] && !m[row,col+9] && m[row,col+10]) - penalty += 40; - } - for (int col = 0; col < size; col++) - for (int row = 0; row <= size - 11; row++) - { - if (m[row,col] && !m[row+1,col] && m[row+2,col] && m[row+3,col] && m[row+4,col] && !m[row+5,col] && m[row+6,col] - && !m[row+7,col] && !m[row+8,col] && !m[row+9,col] && !m[row+10,col]) - penalty += 40; - if (!m[row,col] && !m[row+1,col] && !m[row+2,col] && !m[row+3,col] && m[row+4,col] - && !m[row+5,col] && m[row+6,col] && m[row+7,col] && m[row+8,col] && !m[row+9,col] && m[row+10,col]) - penalty += 40; - } - // Rule 4: proportion of dark modules - int dark = 0; - for (int r = 0; r < size; r++) - for (int c = 0; c < size; c++) - if (m[r, c]) dark++; - int pct = dark * 100 / (size * size); - int prev5 = pct - (pct % 5); - int next5 = prev5 + 5; - penalty += Math.Min(Math.Abs(prev5 - 50) / 5, Math.Abs(next5 - 50) / 5) * 10; - return penalty; - } - - // ── Format / version info ──────────────────────────────────────────────── - - static int FormatInfoBits(int mask) - { - // EC level M = 00, mask 0-7 - int data = (0b00 << 3) | mask; - int rem = data << 10; - int gen = 0b10100110111; - for (int i = 4; i >= 0; i--) - { - if ((rem & (1 << (i + 10))) != 0) - rem ^= gen << i; - } - int result = ((data << 10) | rem) ^ 0b101010000010010; - return result; - } - - static int VersionInfoBits(int version) - { - int rem = version << 12; - int gen = 0b1111100100101; - for (int i = 5; i >= 0; i--) - { - if ((rem & (1 << (i + 12))) != 0) - rem ^= gen << i; - } - return (version << 12) | rem; - } - - // ── EC specification tables ────────────────────────────────────────────── - - struct ECSpec - { - public int ecCWPerBlock; - public (int count, int dcw)[] blocks; - } - - static int TotalDataCodewords(int version) - { - var spec = GetECSpec(version); - int total = 0; - foreach (var (count, dcw) in spec.blocks) - total += count * dcw; - return total; - } - - static ECSpec GetECSpec(int version) - { - // EC level M specifications per version - return version switch - { - 1 => new ECSpec { ecCWPerBlock = 10, blocks = new[] { (1, 16) } }, - 2 => new ECSpec { ecCWPerBlock = 16, blocks = new[] { (1, 28) } }, - 3 => new ECSpec { ecCWPerBlock = 26, blocks = new[] { (1, 44) } }, - 4 => new ECSpec { ecCWPerBlock = 18, blocks = new[] { (2, 32) } }, - 5 => new ECSpec { ecCWPerBlock = 24, blocks = new[] { (2, 43) } }, - 6 => new ECSpec { ecCWPerBlock = 16, blocks = new[] { (4, 27) } }, - 7 => new ECSpec { ecCWPerBlock = 18, blocks = new[] { (4, 31) } }, - 8 => new ECSpec { ecCWPerBlock = 22, blocks = new[] { (2, 38), (2, 39) } }, - 9 => new ECSpec { ecCWPerBlock = 22, blocks = new[] { (3, 36), (2, 37) } }, - 10 => new ECSpec { ecCWPerBlock = 26, blocks = new[] { (4, 43), (1, 44) } }, - 11 => new ECSpec { ecCWPerBlock = 30, blocks = new[] { (1, 50), (4, 51) } }, - 12 => new ECSpec { ecCWPerBlock = 22, blocks = new[] { (6, 36), (2, 37) } }, - 13 => new ECSpec { ecCWPerBlock = 22, blocks = new[] { (8, 37), (1, 38) } }, - 14 => new ECSpec { ecCWPerBlock = 24, blocks = new[] { (4, 40), (5, 41) } }, - 15 => new ECSpec { ecCWPerBlock = 24, blocks = new[] { (5, 41), (5, 42) } }, - 16 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (7, 45), (3, 46) } }, - 17 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (10, 46), (1, 47) } }, - 18 => new ECSpec { ecCWPerBlock = 26, blocks = new[] { (9, 43), (4, 44) } }, - 19 => new ECSpec { ecCWPerBlock = 26, blocks = new[] { (3, 44), (11, 45) } }, - 20 => new ECSpec { ecCWPerBlock = 26, blocks = new[] { (3, 41), (13, 42) } }, - 21 => new ECSpec { ecCWPerBlock = 26, blocks = new[] { (17, 42) } }, - 22 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (17, 46) } }, - 23 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (4, 47), (14, 48) } }, - 24 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (6, 45), (14, 46) } }, - 25 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (8, 47), (13, 48) } }, - 26 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (19, 46), (4, 47) } }, - 27 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (22, 45), (3, 46) } }, - 28 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (3, 45), (23, 46) } }, - 29 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (21, 45), (7, 46) } }, - 30 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (19, 47), (10, 48) } }, - 31 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (2, 46), (29, 47) } }, - 32 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (10, 46), (23, 47) } }, - 33 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (14, 46), (21, 47) } }, - 34 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (14, 46), (23, 47) } }, - 35 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (12, 47), (26, 48) } }, - 36 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (6, 47), (34, 48) } }, - 37 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (29, 46), (14, 47) } }, - 38 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (13, 46), (32, 47) } }, - 39 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (40, 47), (7, 48) } }, - 40 => new ECSpec { ecCWPerBlock = 28, blocks = new[] { (18, 47), (31, 48) } }, - _ => throw new ArgumentException($"Invalid QR version: {version}") - }; - } - - static int RemainderBits(int version) - { - if (version <= 1) return 0; - if (version <= 6) return 7; - if (version <= 13) return 0; - if (version <= 20) return 3; - if (version <= 27) return 4; - if (version <= 34) return 3; - return 0; - } - - static int[] AlignmentPositions(int version) - { - if (version == 1) return Array.Empty(); - int first = 6; - int last = 17 + version * 4 - 7; - int count = version / 7 + 2; - if (count == 2) - return new[] { first, last }; - int step = (int)Math.Ceiling((double)(last - first) / (count - 1)); - if (step % 2 != 0) step++; - var positions = new List { first }; - for (int pos = last; positions.Count < count; pos -= step) - positions.Insert(1, pos); - return positions.ToArray(); - } - - // ── Bit buffer ─────────────────────────────────────────────────────────── - - class BitBuffer - { - readonly List _bytes = new List(); - int _bitCount; - - public int Length => _bitCount; - - public void Append(int value, int numBits) - { - for (int i = numBits - 1; i >= 0; i--) - { - int byteIdx = _bitCount / 8; - int bitIdx = 7 - (_bitCount % 8); - while (_bytes.Count <= byteIdx) - _bytes.Add(0); - if (((value >> i) & 1) == 1) - _bytes[byteIdx] |= (byte)(1 << bitIdx); - _bitCount++; - } - } - - public byte[] ToBytes() - { - return _bytes.ToArray(); - } - } - } - -} \ No newline at end of file diff --git a/Core/QRCodeEncoder.cs.meta b/Core/QRCodeEncoder.cs.meta deleted file mode 100644 index 71b338f..0000000 --- a/Core/QRCodeEncoder.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f754c4b5dc2e24de1a8bc7e353904497 \ No newline at end of file diff --git a/Core/QRTextureGenerator.cs b/Core/QRTextureGenerator.cs deleted file mode 100644 index ebf70b6..0000000 --- a/Core/QRTextureGenerator.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Generates QR code textures from string data using the built-in QRCodeEncoder. - /// No external dependencies required. - /// The caller owns the returned Texture2D and must call Destroy() when done. - /// - public static class QRTextureGenerator - { - private const int QUIET_ZONE = 4; - - public static Texture2D Generate(string data, int pixelSize = 256) - { - pixelSize = Mathf.Max(1, pixelSize); - - if (string.IsNullOrEmpty(data)) - { - BlockmakerLog.Warning("[QRTextureGenerator] No data provided for QR generation."); - return CreatePlaceholder(pixelSize); - } - - try - { - bool[,] matrix = QRCodeEncoder.Encode(data); - int moduleCount = matrix.GetLength(0); - int totalModules = moduleCount + QUIET_ZONE * 2; - int scale = Mathf.Max(1, pixelSize / totalModules); - int texSize = totalModules * scale; - - var tex = new Texture2D(texSize, texSize, TextureFormat.RGB24, false) - { - filterMode = FilterMode.Point - }; - - var pixels = new Color[texSize * texSize]; - for (int i = 0; i < pixels.Length; i++) - pixels[i] = Color.white; - - for (int row = 0; row < moduleCount; row++) - for (int col = 0; col < moduleCount; col++) - { - if (!matrix[row, col]) continue; - int px = (col + QUIET_ZONE) * scale; - int py = (totalModules - 1 - row - QUIET_ZONE) * scale; - for (int dy = 0; dy < scale; dy++) - for (int dx = 0; dx < scale; dx++) - pixels[(py + dy) * texSize + px + dx] = Color.black; - } - - tex.SetPixels(pixels); - tex.Apply(); - return tex; - } - catch (Exception e) - { - BlockmakerLog.Error($"[QRTextureGenerator] Failed to generate QR code: {e.Message}"); - return CreatePlaceholder(pixelSize); - } - } - - private static Texture2D CreatePlaceholder(int size) - { - var tex = new Texture2D(size, size, TextureFormat.RGB24, false); - var pixels = new Color[size * size]; - for (int i = 0; i < pixels.Length; i++) - pixels[i] = new Color(0.85f, 0.85f, 0.85f); - tex.SetPixels(pixels); - tex.Apply(); - return tex; - } - } - -} \ No newline at end of file diff --git a/Core/QRTextureGenerator.cs.meta b/Core/QRTextureGenerator.cs.meta deleted file mode 100644 index 2db78fd..0000000 --- a/Core/QRTextureGenerator.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 426f0851c5e0f47c08d048ff0fdbbebd \ No newline at end of file diff --git a/Core/ReownWalletConnector.cs b/Core/ReownWalletConnector.cs deleted file mode 100644 index 9cda336..0000000 --- a/Core/ReownWalletConnector.cs +++ /dev/null @@ -1,995 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using UnityEngine; -using UnityEngine.Scripting; -using Reown.Sign; -using Reown.Sign.Unity; -using Reown.Sign.Models; -using Reown.Sign.Models.Engine; -using Reown.Sign.Models.Engine.Events; -using Reown.Core; -using Reown.Core.Models; -using Reown.Core.Models.Relay; -using Reown.Core.Network.Models; - -namespace Blockmaker -{ - - [Preserve] - public class ReownWalletConnector : MonoBehaviour - { - public static ReownWalletConnector Instance { get; private set; } - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() - { - Instance = null; - OnQRReady = null; - OnConnected = null; - OnConnectionError = null; - OnTransactionSigned = null; - OnSignError = null; - } - - /// - /// Internal event — game code should subscribe to BlockmakerAuth events instead. - /// Warning: Static event. Unsubscribe in OnDestroy() to avoid scene-load leaks. - /// - internal static event Action OnQRReady; - /// - internal static event Action OnConnected; - /// - internal static event Action OnConnectionError; - /// - internal static event Action OnTransactionSigned; - /// - internal static event Action OnSignError; - - internal static void FireQRReadyForBridge(string provider, string uri, Texture2D qr) - => OnQRReady?.Invoke(provider, uri, qr); - - public bool IsInitialized { get; private set; } - public bool IsConnected => _session != null; - public string ConnectedAddress { get; private set; } - public string SessionTopic { get { var s = _session; return s?.Topic; } } - - public event Action OnInitialized; - - private SignClientUnity _signClient; - private Session _session; - private bool _isInitializing; - private bool _isConnecting; - private Texture2D _lastQRTexture; - private CancellationTokenSource _connectCts; - - private EventHandler _onRelayErrored; - private EventHandler _onRelayDisconnected; - private EventHandler _onRelayConnected; - private EventHandler _onRelayMessageReceived; - private EventHandler _onProviderPayloadReceived; - - private const string ALGO_CHAIN_MAINNET = "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k"; - private const string ALGO_CHAIN_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDe"; - private const string ALGO_METHOD = "algo_signTxn"; - private const string ALGO_METHOD_SIGN_DATA = "algo_signData"; - private const string EVM_CHAIN = "eip155:1"; - private const string EVM_METHOD = "eth_signTypedData_v4"; - - private static float ConnectTimeoutSeconds => BlockmakerAuth.WalletSignTimeout; - - // ── Lifecycle ───────────────────────────────────────────────────────────── - - private void Awake() - { - if (Instance != null && Instance != this) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); - } - - private void OnDestroy() - { - if (_signClient != null) - { - _signClient.SessionDeleted -= OnSessionDeleted; - _signClient.SessionExpired -= OnSessionExpired; - _signClient.SessionConnectionErrored -= OnSessionConnectionErrored; - _signClient.SessionConnectedUnity -= OnSessionConnectedUnity; - _signClient.SessionRejected -= OnSessionRejectedHandler; - _signClient.SessionApproved -= OnSessionApprovedHandler; - - if (_onRelayErrored != null) - _signClient.CoreClient.Relayer.OnErrored -= _onRelayErrored; - if (_onRelayDisconnected != null) - _signClient.CoreClient.Relayer.OnDisconnected -= _onRelayDisconnected; - if (_onRelayConnected != null) - _signClient.CoreClient.Relayer.OnConnected -= _onRelayConnected; - if (_onRelayMessageReceived != null) - _signClient.CoreClient.Relayer.OnMessageReceived -= _onRelayMessageReceived; - if (_onProviderPayloadReceived != null) - _signClient.CoreClient.Relayer.Provider.PayloadReceived -= _onProviderPayloadReceived; - } - _connectCts?.Cancel(); - _connectCts?.Dispose(); - DestroyQRTexture(); - _signClient = null; - _session = null; - - if (Instance == this) - Instance = null; - } - - public static void ClearStorage() - { - var storagePath = $"{Application.persistentDataPath}/Reown"; - if (System.IO.Directory.Exists(storagePath)) - { - BlockmakerLog.Verbose($"[ReownWalletConnector] Clearing Reown storage at {storagePath}"); - try { System.IO.Directory.Delete(storagePath, true); } - catch (Exception ex) { BlockmakerLog.Warning($"[ReownWalletConnector] Storage cleanup: {ex.Message}"); } - } - } - - public async void Initialize(string projectId) - { - if (IsInitialized || _isInitializing) return; - _isInitializing = true; - - try - { - var cfg = BlockmakerAuth.Instance?.blockmakerConfig; - var url = cfg?.dAppUrl; - if (string.IsNullOrEmpty(url)) url = Application.absoluteURL; - if (string.IsNullOrEmpty(url)) url = cfg?.serverUrl; - if (string.IsNullOrEmpty(url)) url = BlockmakerConfig.DefaultServerUrl; - var icon = cfg?.dAppIconUrl; - if (string.IsNullOrEmpty(icon)) icon = url.TrimEnd('/') + "/icon.png"; - - _signClient = await SignClientUnity.Create(new SignClientOptions - { - ProjectId = projectId, - RelayUrl = "wss://relay.walletconnect.com", - Metadata = new Metadata - { - Name = Application.productName, - Description = Application.productName, - Url = url, - Icons = new[] { icon } - } - }); - - if (this == null) return; - - _signClient.SessionDeleted += OnSessionDeleted; - _signClient.SessionExpired += OnSessionExpired; - _signClient.SessionConnectionErrored += OnSessionConnectionErrored; - _signClient.SessionConnectedUnity += OnSessionConnectedUnity; - _signClient.SessionRejected += OnSessionRejectedHandler; - _signClient.SessionApproved += OnSessionApprovedHandler; - - _onRelayErrored = (_, ex) => - BlockmakerLog.Error($"[ReownWalletConnector] RELAY ERROR: {ex.Message}"); - _onRelayDisconnected = (_, _) => - BlockmakerLog.Warning("[ReownWalletConnector] RELAY DISCONNECTED"); - _onRelayConnected = (_, _) => - BlockmakerLog.Info("[ReownWalletConnector] RELAY CONNECTED"); - _onRelayMessageReceived = (_, msg) => - BlockmakerLog.Verbose($"[ReownWalletConnector] RELAY MSG on topic {msg.Topic?.Substring(0, Math.Min(8, msg.Topic?.Length ?? 0))}..."); - _onProviderPayloadReceived = (_, payload) => - { - try - { - var method = payload.IsRequest ? payload.Method : "(response)"; - BlockmakerLog.Verbose($"[ReownWalletConnector] RAW PAYLOAD: {method}, id={payload.Id}, isReq={payload.IsRequest}"); - } - catch { } - }; - - _signClient.CoreClient.Relayer.OnErrored += _onRelayErrored; - _signClient.CoreClient.Relayer.OnDisconnected += _onRelayDisconnected; - _signClient.CoreClient.Relayer.OnConnected += _onRelayConnected; - _signClient.CoreClient.Relayer.OnMessageReceived += _onRelayMessageReceived; - _signClient.CoreClient.Relayer.Provider.PayloadReceived += _onProviderPayloadReceived; - - IsInitialized = true; - var relayConnected = _signClient.CoreClient?.Relayer?.Connected == true; - BlockmakerLog.Verbose($"[ReownWalletConnector] Initialized. Relay: {(relayConnected ? "connected" : "not connected")}, ProjectId: {projectId.Substring(0, Math.Min(6, projectId.Length))}..."); - try - { - var wsUrl = _signClient.CoreClient?.Relayer?.Provider?.Connection?.Url; - BlockmakerLog.Verbose($"[ReownWalletConnector] WebSocket URL: {wsUrl}"); - } - catch { } - - try { OnInitialized?.Invoke(); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] Init failed: {e.Message}"); - } - finally - { - _isInitializing = false; - } - } - - // ── QR texture management ───────────────────────────────────────────────── - - private Texture2D CreateQRTexture(string uri) - { - DestroyQRTexture(); - _lastQRTexture = QRTextureGenerator.Generate(uri, 1024); - return _lastQRTexture; - } - - private void DestroyQRTexture() - { - if (_lastQRTexture != null) - { - Destroy(_lastQRTexture); - _lastQRTexture = null; - } - } - - // ── Safe event invocation ───────────────────────────────────────────────── - - private static void FireEvent(Action handler, T arg) - { - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(arg); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - private static void FireEvent(Action handler, T1 a, T2 b) - { - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(a, b); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - private static void FireEvent(Action handler, T1 a, T2 b, T3 c) - { - if (handler == null) return; - foreach (var d in handler.GetInvocationList()) - { - try { ((Action)d).Invoke(a, b, c); } - catch (Exception ex) { BlockmakerLog.Exception(ex); } - } - } - - // ── Algorand wallet connect ─────────────────────────────────────────────── - - public void ConnectAlgorand( - string walletHint, - Action onConnected = null, - Action onError = null) - { - if (!IsInitialized) - { - BlockmakerLog.Error("[ReownWalletConnector] Not initialized."); - onError?.Invoke("Something went wrong. Please restart the game and try again."); - return; - } - - if (_isConnecting) - { - onError?.Invoke("A connection is already in progress. Please wait."); - return; - } - - _isConnecting = true; - ConnectedAddress = null; - ConnectAlgorandAsync(walletHint, onConnected, onError); - } - - private async void ConnectAlgorandAsync( - string walletHint, Action onConnected, Action onError) - { - var oldCts = _connectCts; - _connectCts = new CancellationTokenSource(); - oldCts?.Cancel(); - oldCts?.Dispose(); - var token = _connectCts.Token; - - try - { - var client = _signClient; - if (client == null) { onError?.Invoke("Wallet connector is not ready. Please try again."); return; } - - BlockmakerLog.Verbose($"[ReownWalletConnector] Creating pairing for {walletHint}..."); - BlockmakerLog.Verbose($"[ReownWalletConnector] Relay connected: {client.CoreClient.Relayer.Connected}"); - - try - { - var stalePairings = client.PairingStore.Values; - if (stalePairings != null && stalePairings.Length > 0) - { - BlockmakerLog.Verbose($"[ReownWalletConnector] Clearing {stalePairings.Length} stale pairing(s)..."); - foreach (var p in stalePairings) - { - try { await client.CoreClient.Pairing.Disconnect(p.Topic); } - catch { } - } - } - } - catch (Exception cleanupEx) - { - BlockmakerLog.Warning($"[ReownWalletConnector] Pairing cleanup: {cleanupEx.Message}"); - } - - var connectData = await client.Connect(new ConnectOptions - { - RequiredNamespaces = new RequiredNamespaces - { - { - "algorand", new ProposedNamespace - { - Methods = new[] { ALGO_METHOD, ALGO_METHOD_SIGN_DATA }, - Chains = new[] { ALGO_CHAIN_MAINNET, ALGO_CHAIN_TESTNET }, - Events = Array.Empty() - } - } - } - }); - - if (_signClient == null) { onError?.Invoke("Wallet connector was reset. Please try again."); return; } - token.ThrowIfCancellationRequested(); - - var uri = connectData.Uri; - BlockmakerLog.Verbose("[ReownWalletConnector] WC URI generated — ready for QR scan."); - - var qrTexture = CreateQRTexture(uri); - if (qrTexture != null) - FireEvent(OnQRReady, walletHint, uri, qrTexture); - - BlockmakerLog.Verbose($"[ReownWalletConnector] Waiting for wallet approval (timeout={ConnectTimeoutSeconds}s)..."); - - var approvalTask = connectData.Approval; - var pollCts = CancellationTokenSource.CreateLinkedTokenSource(token); - - _ = PollForSessionAsync(client, walletHint, pollCts.Token); - - var timeoutTask = Task.Delay(TimeSpan.FromSeconds(ConnectTimeoutSeconds)); - var completed = await Task.WhenAny(approvalTask, timeoutTask); - - pollCts.Cancel(); - pollCts.Dispose(); - - if (_signClient == null) { onError?.Invoke("Wallet connector was reset. Please try again."); return; } - token.ThrowIfCancellationRequested(); - - if (completed == timeoutTask) - { - var sessions = client.Session?.Values; - BlockmakerLog.Warning($"[ReownWalletConnector] Timed out. Active sessions: {sessions?.Length ?? 0}"); - - if (sessions != null && sessions.Length > 0) - { - BlockmakerLog.Info("[ReownWalletConnector] Found session despite timeout — using it."); - var fallbackSession = sessions[^1]; - _session = fallbackSession; - - string fbAddress = null; - if (fallbackSession.Namespaces.TryGetValue("algorand", out var fbAlgo) && - fbAlgo.Accounts?.Length > 0) - fbAddress = fbAlgo.Accounts[0].Split(':')[^1]; - - if (fbAddress != null) - { - ConnectedAddress = fbAddress; - BlockmakerLog.Info($"[ReownWalletConnector] {walletHint} connected (via fallback): {fbAddress}"); - onConnected?.Invoke(walletHint, fbAddress); - FireEvent(OnConnected, walletHint, fbAddress); - return; - } - } - - var msg = "Connection timed out. Please try again."; - onError?.Invoke(msg); - FireEvent(OnConnectionError, msg); - return; - } - - BlockmakerLog.Verbose("[ReownWalletConnector] Approval task completed, extracting session..."); - - var session = await approvalTask; - _session = session; - - BlockmakerLog.Verbose($"[ReownWalletConnector] Session topic: {session.Topic}, namespaces: {string.Join(", ", session.Namespaces.Keys)}"); - - string[] accounts = null; - if (session.Namespaces.TryGetValue("algorand", out var algoNs)) - accounts = algoNs.Accounts; - - if (accounts == null || accounts.Length == 0) - { - BlockmakerLog.Warning($"[ReownWalletConnector] No Algorand accounts. Namespaces: {string.Join(", ", session.Namespaces.Keys)}"); - var msg = "No accounts found in your wallet. Please make sure you have an Algorand account and try again."; - onError?.Invoke(msg); - FireEvent(OnConnectionError, msg); - return; - } - - var address = accounts[0].Split(':').Last(); - ConnectedAddress = address; - - BlockmakerLog.Info($"[ReownWalletConnector] {walletHint} connected: {address}"); - onConnected?.Invoke(walletHint, address); - FireEvent(OnConnected, walletHint, address); - } - catch (OperationCanceledException) - { - BlockmakerLog.Info("[ReownWalletConnector] ConnectAlgorand cancelled."); - onError?.Invoke("Connection was cancelled."); - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] ConnectAlgorand error: {e.Message}"); - var msg = "Wallet connection failed. Please try again."; - onError?.Invoke(msg); - FireEvent(OnConnectionError, msg); - } - finally - { - _isConnecting = false; - DestroyQRTexture(); - } - } - - // ── EVM wallet connect ───────────────────────────────────────────────────── - - public void ConnectEvm( - Action onConnected = null, - Action onError = null) - { - if (!IsInitialized) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - return; - } - - if (_isConnecting) - { - onError?.Invoke("A connection is already in progress. Please wait."); - return; - } - - _isConnecting = true; - ConnectedAddress = null; - ConnectEvmAsync(onConnected, onError); - } - - private async void ConnectEvmAsync(Action onConnected, Action onError) - { - var oldCts = _connectCts; - _connectCts = new CancellationTokenSource(); - oldCts?.Cancel(); - oldCts?.Dispose(); - var token = _connectCts.Token; - - try - { - var client = _signClient; - if (client == null) { onError?.Invoke("Wallet connector is not ready. Please try again."); return; } - - var connectData = await client.Connect(new ConnectOptions - { - RequiredNamespaces = new RequiredNamespaces - { - { - "eip155", new ProposedNamespace - { - Methods = new[] { EVM_METHOD, "personal_sign", "eth_sendTransaction" }, - Chains = new[] { EVM_CHAIN }, - Events = new[] { "accountsChanged", "chainChanged" } - } - } - } - }); - - if (_signClient == null) { onError?.Invoke("Wallet connector was reset. Please try again."); return; } - token.ThrowIfCancellationRequested(); - - var uri = connectData.Uri; - var qrTexture = CreateQRTexture(uri); - if (qrTexture != null) - FireEvent(OnQRReady, "EVM Wallet", uri, qrTexture); - - var approvalTask = connectData.Approval; - var timeoutTask = Task.Delay(TimeSpan.FromSeconds(ConnectTimeoutSeconds)); - var completed = await Task.WhenAny(approvalTask, timeoutTask); - - if (_signClient == null) { onError?.Invoke("Wallet connector was reset. Please try again."); return; } - token.ThrowIfCancellationRequested(); - - if (completed == timeoutTask) - { - var msg = "Connection timed out. Please try again."; - onError?.Invoke(msg); - FireEvent(OnConnectionError, msg); - return; - } - - var session = await approvalTask; - _session = session; - - string[] accounts = null; - if (session.Namespaces.TryGetValue("eip155", out var evmNs)) - accounts = evmNs.Accounts; - - if (accounts == null || accounts.Length == 0) - { - var msg = "No accounts found in your wallet. Please try again."; - onError?.Invoke(msg); - FireEvent(OnConnectionError, msg); - return; - } - - var evmAddress = accounts[0].Split(':').Last(); - ConnectedAddress = evmAddress; - - BlockmakerLog.Info($"[ReownWalletConnector] EVM wallet connected: {evmAddress}"); - onConnected?.Invoke(evmAddress); - FireEvent(OnConnected, "EVM Wallet", evmAddress); - } - catch (OperationCanceledException) - { - BlockmakerLog.Info("[ReownWalletConnector] ConnectEvm cancelled."); - onError?.Invoke("Connection was cancelled."); - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] ConnectEvm error: {e.Message}"); - var msg = "Wallet connection failed. Please try again."; - onError?.Invoke(msg); - FireEvent(OnConnectionError, msg); - } - finally - { - _isConnecting = false; - DestroyQRTexture(); - } - } - - // ── Algorand transaction signing ────────────────────────────────────────── - - public void SignAlgorandTransaction( - string txnBase64, - Action onSigned = null, - Action onError = null) - { - var session = _session; - if (session == null) - { - onError?.Invoke("Your wallet is not connected. Please connect your wallet again to continue."); - return; - } - - SignAlgorandAsync(session.Topic, txnBase64, onSigned, onError); - } - - private async void SignAlgorandAsync( - string topic, string txnBase64, Action onSigned, Action onError) - { - try - { - var client = _signClient; - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); return; } - - // algo_signTxn params: [[{txn: "base64..."}]] - // Verify wire format matches ARC-0025 with your installed Reown.Sign version. - var txns = new List> - { - new List - { - new AlgoSignTxnParam { txn = txnBase64 } - } - }; - - var result = await client.Request>, string[]>( - topic, txns, ALGO_CHAIN_MAINNET - ); - - if (_signClient == null) - { - onError?.Invoke("Wallet was disconnected. Please reconnect and try again."); - return; - } - - if (result == null || result.Length == 0 || result[0] == null) - { - var msg = "Wallet declined to sign. Please approve the request in your wallet app."; - onError?.Invoke(msg); - FireEvent(OnSignError, msg); - return; - } - - var signedTxn = result[0]; - BlockmakerLog.Info("[ReownWalletConnector] Transaction signed."); - onSigned?.Invoke(signedTxn); - FireEvent(OnTransactionSigned, signedTxn); - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] Sign error: {e.Message}"); - var msg = "Something went wrong. Please try again."; - onError?.Invoke(msg); - FireEvent(OnSignError, msg); - } - } - - public void SignAlgorandTransactions( - string[] txnsBase64, - Action onSigned = null, - Action onError = null) - { - var session = _session; - if (session == null) - { - onError?.Invoke("Your wallet is not connected. Please connect your wallet again to continue."); - return; - } - - SignAlgorandGroupAsync(session.Topic, txnsBase64, onSigned, onError); - } - - private async void SignAlgorandGroupAsync( - string topic, string[] txnsBase64, Action onSigned, Action onError) - { - try - { - var client = _signClient; - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); return; } - - var txnParams = new List(); - foreach (var b64 in txnsBase64) - txnParams.Add(new AlgoSignTxnParam { txn = b64 }); - - var txns = new List> { txnParams }; - - var result = await client.Request>, string[]>( - topic, txns, ALGO_CHAIN_MAINNET - ); - - if (_signClient == null) - { - onError?.Invoke("Wallet was disconnected. Please reconnect and try again."); - return; - } - - if (result == null || result.Length < txnsBase64.Length) - { - onError?.Invoke("Wallet declined to sign. Please approve the request in your wallet app."); - return; - } - - for (int i = 0; i < result.Length; i++) - { - if (string.IsNullOrEmpty(result[i])) - { - onError?.Invoke("Wallet did not approve all required signatures. Please try again."); - return; - } - } - - onSigned?.Invoke(result); - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] Group sign error: {e.Message}"); - var msg = "Something went wrong. Please try again."; - onError?.Invoke(msg); - } - } - - // ── EVM signing ──────────────────────────────────────────────────────────── - - public void SignEvmTypedData( - string evmAddress, - string typedDataJson, - Action onSigned = null, - Action onError = null) - { - var session = _session; - if (session == null) - { - onError?.Invoke("Your wallet is not connected. Please connect your wallet again to continue."); - return; - } - - SignEvmTypedDataAsync(session.Topic, evmAddress, typedDataJson, onSigned, onError); - } - - private async void SignEvmTypedDataAsync( - string topic, string evmAddress, string typedDataJson, - Action onSigned, Action onError) - { - try - { - var client = _signClient; - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); return; } - - var result = await client.Request( - topic, new[] { evmAddress, typedDataJson }, EVM_CHAIN - ); - - if (_signClient == null) - { - onError?.Invoke("Wallet was disconnected. Please reconnect and try again."); - return; - } - - if (string.IsNullOrEmpty(result)) - { - onError?.Invoke("Wallet declined to sign. Please approve the request in your wallet app."); - return; - } - - onSigned?.Invoke(result); - FireEvent(OnTransactionSigned, result); - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] EVM sign error: {e.Message}"); - var msg = "Something went wrong. Please try again."; - onError?.Invoke(msg); - FireEvent(OnSignError, msg); - } - } - - /// - /// Sign an arbitrary UTF-8 message with the connected EVM wallet via - /// personal_sign (sign-in proof). Returns the 0x-hex signature. Mirrors - /// but uses the personal_sign method. - /// - public void SignEvmPersonalMessage( - string evmAddress, - string messageUtf8, - Action onSignedHex = null, - Action onError = null) - { - var session = _session; - if (session == null) - { - onError?.Invoke("Your wallet is not connected. Please connect your wallet again to continue."); - return; - } - - SignEvmPersonalMessageAsync(session.Topic, evmAddress, messageUtf8, onSignedHex, onError); - } - - private async void SignEvmPersonalMessageAsync( - string topic, string evmAddress, string messageUtf8, - Action onSignedHex, Action onError) - { - try - { - var client = _signClient; - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); return; } - - // personal_sign params: [hexMessage, address]. The message is hex-encoded - // UTF-8 bytes; the wallet applies EIP-191 framing, and the server re-applies - // it (viem.recoverMessageAddress) when recovering the signer. - var hexMessage = "0x" + BytesToHex(System.Text.Encoding.UTF8.GetBytes(messageUtf8)); - - var result = await client.Request( - topic, new[] { hexMessage, evmAddress }, EVM_CHAIN - ); - - if (_signClient == null) - { - onError?.Invoke("Wallet was disconnected. Please reconnect and try again."); - return; - } - - if (string.IsNullOrEmpty(result)) - { - onError?.Invoke("Wallet declined to sign. Please approve the request in your wallet app."); - return; - } - - onSignedHex?.Invoke(result); - FireEvent(OnTransactionSigned, result); - } - catch (Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] EVM personal_sign error: {e.Message}"); - var msg = "Something went wrong. Please try again."; - onError?.Invoke(msg); - FireEvent(OnSignError, msg); - } - } - - private static string BytesToHex(byte[] bytes) - { - var sb = new System.Text.StringBuilder(bytes.Length * 2); - foreach (var b in bytes) sb.Append(b.ToString("x2")); - return sb.ToString(); - } - - // ── Cancel / Disconnect ─────────────────────────────────────────────────── - - public void CancelConnection() - { - _connectCts?.Cancel(); - _isConnecting = false; - DestroyQRTexture(); - } - - public async void Disconnect() - { - CancelConnection(); - - var session = _session; - var client = _signClient; - _session = null; - ConnectedAddress = null; - - if (session == null || client == null) return; - - try - { - await client.DisconnectAsync(session.Topic); - } - catch (Exception e) - { - BlockmakerLog.Warning($"[ReownWalletConnector] Disconnect error: {e.Message}"); - } - - BlockmakerLog.Info("[ReownWalletConnector] Disconnected."); - } - - // ── Session restore ─────────────────────────────────────────────────────── - - public string TryRestoreSession() - { - var client = _signClient; - if (client == null) return null; - - try - { - var sessions = client.Session.Values; - if (sessions == null || sessions.Length == 0) return null; - - var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var validSessions = sessions.Where(s => s.Expiry > now).ToList(); - if (validSessions.Count == 0) return null; - - var session = validSessions.OrderByDescending(s => s.Expiry).First(); - - if (session.Namespaces.TryGetValue("algorand", out var algoNs) && - algoNs.Accounts != null && algoNs.Accounts.Length > 0) - { - _session = session; - ConnectedAddress = algoNs.Accounts[0].Split(':').Last(); - BlockmakerLog.Info($"[ReownWalletConnector] Session restored: {ConnectedAddress}"); - return ConnectedAddress; - } - - if (session.Namespaces.TryGetValue("eip155", out var evmNs) && - evmNs.Accounts != null && evmNs.Accounts.Length > 0) - { - _session = session; - ConnectedAddress = evmNs.Accounts[0].Split(':').Last(); - BlockmakerLog.Info($"[ReownWalletConnector] EVM session restored: {ConnectedAddress}"); - return ConnectedAddress; - } - } - catch (Exception e) - { - BlockmakerLog.Warning($"[ReownWalletConnector] Session restore failed: {e.Message}"); - } - - return null; - } - - // ── Session polling (fallback) ────────────────────────────────────────────── - - private static async Task PollForSessionAsync(SignClientUnity client, string hint, CancellationToken ct) - { - try - { - while (!ct.IsCancellationRequested) - { - await Task.Delay(3000, ct); - var sessions = client.Session?.Values; - bool relayOk = client.CoreClient?.Relayer?.Connected ?? false; - var pairings = client.PairingStore?.Values; - var proposals = client.Proposal?.Values; - BlockmakerLog.Verbose($"[ReownWalletConnector] Poll — relay={relayOk}, sessions={sessions?.Length ?? 0}, pairings={pairings?.Length ?? 0}, proposals={proposals?.Length ?? 0}"); - } - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - BlockmakerLog.Warning($"[ReownWalletConnector] Polling error: {ex.Message}"); - } - } - - // ── Session events ──────────────────────────────────────────────────────── - - [Preserve] - private void OnSessionDeleted(object sender, SessionEvent e) - { - var session = _session; - if (session?.Topic == e.Topic) - { - _session = null; - ConnectedAddress = null; - BlockmakerLog.Info("[ReownWalletConnector] Session deleted by wallet."); - } - } - - [Preserve] - private void OnSessionExpired(object sender, Session e) - { - var session = _session; - if (session?.Topic == e.Topic) - { - _session = null; - ConnectedAddress = null; - BlockmakerLog.Info("[ReownWalletConnector] Session expired."); - } - } - - [Preserve] - private void OnSessionConnectionErrored(object sender, Exception e) - { - BlockmakerLog.Error($"[ReownWalletConnector] SessionConnectionErrored: {e.Message}"); - if (_isConnecting) - { - FireEvent(OnConnectionError, "Wallet connection failed. Please try again."); - } - } - - [Preserve] - private void OnSessionConnectedUnity(object sender, Session session) - { - BlockmakerLog.Verbose($"[ReownWalletConnector] SessionConnectedUnity fired — topic: {session.Topic}"); - - if (_session != null) return; - - _session = session; - - string address = null; - if (session.Namespaces.TryGetValue("algorand", out var algoNs) && - algoNs.Accounts != null && algoNs.Accounts.Length > 0) - { - address = algoNs.Accounts[0].Split(':')[^1]; - } - else if (session.Namespaces.TryGetValue("eip155", out var evmNs) && - evmNs.Accounts != null && evmNs.Accounts.Length > 0) - { - address = evmNs.Accounts[0].Split(':')[^1]; - } - - if (address != null) - { - ConnectedAddress = address; - BlockmakerLog.Verbose($"[ReownWalletConnector] Backup: connected via SessionConnectedUnity — {address}"); - } - } - - [Preserve] - private void OnSessionRejectedHandler(object sender, Session e) - { - BlockmakerLog.Warning($"[ReownWalletConnector] Session REJECTED by wallet. Topic: {e.Topic}"); - } - - [Preserve] - private void OnSessionApprovedHandler(object sender, Session e) - { - BlockmakerLog.Verbose($"[ReownWalletConnector] Session APPROVED. Topic: {e.Topic}"); - } - } - -} \ No newline at end of file diff --git a/Core/ReownWalletConnector.cs.meta b/Core/ReownWalletConnector.cs.meta deleted file mode 100644 index 91aeb9a..0000000 --- a/Core/ReownWalletConnector.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a42fe57e1c91e4c6485ce21fb1745c87 \ No newline at end of file diff --git a/Core/SecurePrefs.cs b/Core/SecurePrefs.cs deleted file mode 100644 index 1c01bf7..0000000 --- a/Core/SecurePrefs.cs +++ /dev/null @@ -1,257 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Encrypted wrapper around PlayerPrefs. Values are stored as - /// AES-256-CBC ciphertext with HMAC-SHA256 (encrypt-then-MAC). - /// - /// Key storage: - /// Native: random 32-byte key file in Application.persistentDataPath - /// WebGL: key in PlayerPrefs (same-origin policy provides isolation) - /// - /// v2 format: "$bm2:" + base64( iv[16] + hmac[32] + ciphertext ) - /// - Separate encryption/MAC subkeys derived from master key - /// - HMAC covers IV + ciphertext (prevents IV bit-flipping) - /// v1 format: "$bm1:" + base64( iv[16] + hmac[32] + ciphertext ) - /// - Single key for both AES and HMAC; HMAC covers ciphertext only - /// - Read-only for backward compat; re-encrypted as v2 on read - /// Legacy (non-prefixed) values are migrated on first read. - /// - public static class SecurePrefs - { - private const string PrefixV2 = "$bm2:"; - private const string PrefixV1 = "$bm1:"; - private const int IvLen = 16; - private const int HmacLen = 32; - private const string KeyPlayerPrefsKey = "bm_secure_prefs_key"; - - private static byte[] _key; - - private static byte[] GetOrCreateKey() - { - if (_key != null) return _key; - - #if UNITY_WEBGL && !UNITY_EDITOR - string stored = PlayerPrefs.GetString(KeyPlayerPrefsKey, ""); - if (!string.IsNullOrEmpty(stored)) - { - _key = Convert.FromBase64String(stored); - return _key; - } - _key = new byte[32]; - using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(_key); - PlayerPrefs.SetString(KeyPlayerPrefsKey, Convert.ToBase64String(_key)); - PlayerPrefs.Save(); - return _key; - #else - string dir = Application.persistentDataPath; - string path = Path.Combine(dir, ".bm_key"); - if (File.Exists(path)) - { - _key = File.ReadAllBytes(path); - if (_key.Length == 32) return _key; - } - _key = new byte[32]; - using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(_key); - File.WriteAllBytes(path, _key); - return _key; - #endif - } - - public static void SetString(string prefsKey, string value) - { - if (string.IsNullOrEmpty(value)) - { - PlayerPrefs.DeleteKey(prefsKey); - return; - } - - byte[] masterKey = GetOrCreateKey(); - DeriveSubkeys(masterKey, out byte[] encKey, out byte[] macKey); - byte[] plaintext = Encoding.UTF8.GetBytes(value); - - byte[] iv = new byte[IvLen]; - using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(iv); - - byte[] ciphertext; - using (var aes = Aes.Create()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - aes.Key = encKey; - aes.IV = iv; - using var enc = aes.CreateEncryptor(); - ciphertext = enc.TransformFinalBlock(plaintext, 0, plaintext.Length); - } - - byte[] hmac; - using (var hmacSha = new HMACSHA256(macKey)) - { - hmacSha.TransformBlock(iv, 0, iv.Length, null, 0); - hmacSha.TransformFinalBlock(ciphertext, 0, ciphertext.Length); - hmac = hmacSha.Hash; - } - - byte[] blob = new byte[IvLen + HmacLen + ciphertext.Length]; - Buffer.BlockCopy(iv, 0, blob, 0, IvLen); - Buffer.BlockCopy(hmac, 0, blob, IvLen, HmacLen); - Buffer.BlockCopy(ciphertext, 0, blob, IvLen + HmacLen, ciphertext.Length); - - PlayerPrefs.SetString(prefsKey, PrefixV2 + Convert.ToBase64String(blob)); - } - - public static string GetString(string prefsKey, string defaultValue = "") - { - string raw = PlayerPrefs.GetString(prefsKey, ""); - if (string.IsNullOrEmpty(raw)) return defaultValue; - - if (raw.StartsWith(PrefixV2)) - return DecryptV2(prefsKey, raw, defaultValue); - - if (raw.StartsWith(PrefixV1)) - { - string value = DecryptV1(prefsKey, raw, defaultValue); - if (value != defaultValue) - { - BlockmakerLog.Info($"[SecurePrefs] Migrating key '{prefsKey}' from v1 to v2 format."); - SetString(prefsKey, value); - PlayerPrefs.Save(); - } - return value; - } - - BlockmakerLog.Info($"[SecurePrefs] Migrating plaintext key '{prefsKey}' to encrypted v2 format."); - SetString(prefsKey, raw); - PlayerPrefs.Save(); - return raw; - } - - private static string DecryptV2(string prefsKey, string raw, string defaultValue) - { - try - { - byte[] masterKey = GetOrCreateKey(); - DeriveSubkeys(masterKey, out byte[] encKey, out byte[] macKey); - byte[] blob = Convert.FromBase64String(raw.Substring(PrefixV2.Length)); - if (blob.Length < IvLen + HmacLen + 1) return defaultValue; - - byte[] iv = new byte[IvLen]; - byte[] storedHmac = new byte[HmacLen]; - int ctLen = blob.Length - IvLen - HmacLen; - byte[] ciphertext = new byte[ctLen]; - Buffer.BlockCopy(blob, 0, iv, 0, IvLen); - Buffer.BlockCopy(blob, IvLen, storedHmac, 0, HmacLen); - Buffer.BlockCopy(blob, IvLen + HmacLen, ciphertext, 0, ctLen); - - byte[] computedHmac; - using (var hmacSha = new HMACSHA256(macKey)) - { - hmacSha.TransformBlock(iv, 0, iv.Length, null, 0); - hmacSha.TransformFinalBlock(ciphertext, 0, ciphertext.Length); - computedHmac = hmacSha.Hash; - } - - if (!ConstantTimeEquals(computedHmac, storedHmac)) - { - BlockmakerLog.Warning($"[SecurePrefs] HMAC mismatch for key '{prefsKey}' — data may be tampered"); - return defaultValue; - } - - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - aes.Key = encKey; - aes.IV = iv; - using var dec = aes.CreateDecryptor(); - byte[] plainBytes = dec.TransformFinalBlock(ciphertext, 0, ciphertext.Length); - return Encoding.UTF8.GetString(plainBytes); - } - catch (Exception ex) - { - BlockmakerLog.Warning($"[SecurePrefs] Decryption failed for key '{prefsKey}': {ex.Message}"); - return defaultValue; - } - } - - private static string DecryptV1(string prefsKey, string raw, string defaultValue) - { - try - { - byte[] key = GetOrCreateKey(); - byte[] blob = Convert.FromBase64String(raw.Substring(PrefixV1.Length)); - if (blob.Length < IvLen + HmacLen + 1) return defaultValue; - - byte[] iv = new byte[IvLen]; - byte[] storedHmac = new byte[HmacLen]; - int ctLen = blob.Length - IvLen - HmacLen; - byte[] ciphertext = new byte[ctLen]; - Buffer.BlockCopy(blob, 0, iv, 0, IvLen); - Buffer.BlockCopy(blob, IvLen, storedHmac, 0, HmacLen); - Buffer.BlockCopy(blob, IvLen + HmacLen, ciphertext, 0, ctLen); - - byte[] computedHmac; - using (var hmacSha = new HMACSHA256(key)) - computedHmac = hmacSha.ComputeHash(ciphertext); - - if (!ConstantTimeEquals(computedHmac, storedHmac)) - { - BlockmakerLog.Warning($"[SecurePrefs] HMAC mismatch for key '{prefsKey}' — data may be tampered"); - return defaultValue; - } - - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - aes.Key = key; - aes.IV = iv; - using var dec = aes.CreateDecryptor(); - byte[] plainBytes = dec.TransformFinalBlock(ciphertext, 0, ciphertext.Length); - return Encoding.UTF8.GetString(plainBytes); - } - catch (Exception ex) - { - BlockmakerLog.Warning($"[SecurePrefs] Decryption failed for key '{prefsKey}': {ex.Message}"); - return defaultValue; - } - } - - public static void DeleteKey(string prefsKey) - { - PlayerPrefs.DeleteKey(prefsKey); - } - - public static bool HasKey(string prefsKey) - { - return PlayerPrefs.HasKey(prefsKey); - } - - public static void Save() - { - PlayerPrefs.Save(); - } - - private static void DeriveSubkeys(byte[] masterKey, out byte[] encKey, out byte[] macKey) - { - using (var h = new HMACSHA256(masterKey)) - encKey = h.ComputeHash(Encoding.UTF8.GetBytes("blockmaker-enc")); - using (var h = new HMACSHA256(masterKey)) - macKey = h.ComputeHash(Encoding.UTF8.GetBytes("blockmaker-mac")); - } - - private static bool ConstantTimeEquals(byte[] a, byte[] b) - { - if (a.Length != b.Length) return false; - int diff = 0; - for (int i = 0; i < a.Length; i++) - diff |= a[i] ^ b[i]; - return diff == 0; - } - } - -} \ No newline at end of file diff --git a/Core/SecurePrefs.cs.meta b/Core/SecurePrefs.cs.meta deleted file mode 100644 index 2f3f001..0000000 --- a/Core/SecurePrefs.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: bb5504f19b134428da512030964fda8e \ No newline at end of file diff --git a/Core/TokenBalanceTracker.cs b/Core/TokenBalanceTracker.cs deleted file mode 100644 index 5db0b5b..0000000 --- a/Core/TokenBalanceTracker.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Collections; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Tracks a player's live Algorand ASA token balance with optimistic updates. - /// - /// Set in the Inspector to your game's token. - /// When an in-game reward is sent, call to - /// immediately bump the displayed balance. The next poll confirms the - /// on-chain balance and clears the pending amount. - /// - /// Persists across scenes (DontDestroyOnLoad). Use from any UI: - /// TokenBalanceTracker.Instance.DisplayBalance - /// TokenBalanceTracker.OnBalanceChanged += myHandler; - /// - public class TokenBalanceTracker : MonoBehaviour - { - public static TokenBalanceTracker Instance { get; private set; } - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - static void ResetStatics() - { - Instance = null; - OnBalanceChanged = null; - OnRewardAdded = null; - } - - [Tooltip("Algorand ASA ID of the token to track. Set this in the Inspector for your game's token.")] - public long tokenAssetId; - - private const int DefaultTokenDecimals = 6; - - [Tooltip("Seconds between on-chain balance polls.")] - public float pollInterval = 10f; - - // ── Events ──────────────────────────────────────────────────────────────── - - /// Fires whenever the display balance changes (poll or optimistic). - public static event Action OnBalanceChanged; - - /// Fires when a pending reward is added. Amount = the reward size. - public static event Action OnRewardAdded; - - // ── State ───────────────────────────────────────────────────────────────── - - private long _confirmedBalance; - private long _pendingRewards; - private int _tokenDecimals = DefaultTokenDecimals; - private Coroutine _pollCoroutine; - private float _lastRewardTime; - - [Tooltip("Seconds before unconfirmed pending rewards are cleared. " + - "Handles failed transactions gracefully.")] - public float pendingExpirySeconds = 30f; - - /// Confirmed on-chain balance + optimistic pending rewards. - public long DisplayBalance => _confirmedBalance + _pendingRewards; - - /// Decimals for the tracked token (from chain metadata). - public int Decimals => _tokenDecimals; - - /// Display balance formatted as a human-readable string. - public string FormattedBalance => FormatBalance(DisplayBalance, _tokenDecimals); - - // ── Lifecycle ───────────────────────────────────────────────────────────── - - private void Awake() - { - if (Instance != null && Instance != this) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); - } - - - /// The balance endpoint needs a PLAYER session (JWT) — a wallet identity that - /// hasn't completed backend login (or an editor with only the dev API key) - /// would just 401 every poll. Each poll REQUEST is gated on a real session - /// token (see PollBalance), but the poll LOOP itself only requires a wallet: - /// the JWT can land SILENTLY (restore-time refresh, proactive refresh) with - /// no OnIdentityChanged, so a loop stopped on "no session yet" would never - /// learn the session arrived — the lobby chip then shows 0 forever. - private static bool HasPlayerSession() - { - var id = BlockmakerAuth.Instance?.Identity; - if (id is ServerSignedIdentity ss) return !string.IsNullOrEmpty(ss.SessionToken); - if (id is MagicIdentity magic) return !string.IsNullOrEmpty(magic.SessionToken); - if (id is WalletConnectIdentity wc) return !string.IsNullOrEmpty(wc.SessionToken); - if (id is EvmXChainIdentity evm) return !string.IsNullOrEmpty(evm.SessionToken); - return false; - } - - private void OnEnable() - { - if (Instance != this) return; - - BlockmakerAuth.OnIdentityChanged += HandleIdentityChanged; - - if (BlockmakerAuth.Instance?.HasWallet == true) - StartPolling(); - } - - private void OnDisable() - { - BlockmakerAuth.OnIdentityChanged -= HandleIdentityChanged; - StopPolling(); - } - - private void OnDestroy() - { - if (Instance == this) - Instance = null; - } - - // ── Public API ──────────────────────────────────────────────────────────── - - /// - /// Call when an instant token reward has been sent. Updates the display - /// balance immediately without waiting for the chain to confirm. - /// Negative amounts are CORRECTIONS (e.g. the backend paid a smaller - /// server-rolled crate tier, or refused the payout entirely) — they reduce - /// the pending total, clamped so display never drops below chain truth. - /// - public void AddPendingReward(long amount) - { - if (amount == 0) return; - _pendingRewards += amount; - if (_pendingRewards < 0) _pendingRewards = 0; - _lastRewardTime = Time.realtimeSinceStartup; - if (amount > 0) OnRewardAdded?.Invoke(amount); - OnBalanceChanged?.Invoke(DisplayBalance); - } - - /// Force an immediate balance poll. - public void RefreshNow() - { - if (BlockmakerAuth.Instance?.HasWallet != true) return; - PollBalance(); - } - - // ── Identity ────────────────────────────────────────────────────────────── - - private void HandleIdentityChanged(IBlockmakerIdentity identity) - { - _confirmedBalance = 0; - _pendingRewards = 0; - _tokenDecimals = DefaultTokenDecimals; - OnBalanceChanged?.Invoke(0); - - if (identity != null && identity.HasWallet) - StartPolling(); - else - StopPolling(); - } - - // ── Polling ─────────────────────────────────────────────────────────────── - - private void StartPolling() - { - StopPolling(); - PollBalance(); - _pollCoroutine = StartCoroutine(PollRoutine()); - } - - private void StopPolling() - { - if (_pollCoroutine != null) - { - StopCoroutine(_pollCoroutine); - _pollCoroutine = null; - } - } - - private IEnumerator PollRoutine() - { - while (true) - { - yield return new WaitForSecondsRealtime(pollInterval); - PollBalance(); - } - } - - private bool _waitingForSessionLogged; - - private void PollBalance() - { - // SKIP (never stop the loop) while the backend session hasn't arrived yet. - // A restored wallet identity often has no JWT at boot; the JWT can then land - // silently (restore-time RefreshToken, proactive refresh) without any - // OnIdentityChanged — the live loop picks it up within one poll interval. - // Skipping also keeps the cdf8e14 goal: no doomed 401 polls every 10s. - if (!HasPlayerSession()) - { - if (!_waitingForSessionLogged) - { - BlockmakerLog.Info("[TokenBalanceTracker] Wallet present but no backend session yet — polls deferred until the JWT arrives."); - _waitingForSessionLogged = true; - } - return; - } - _waitingForSessionLogged = false; - - var bm = BlockmakerClient.Instance; - if (bm == null) return; - - var expectedAddress = BlockmakerAuth.Instance?.Address; - - bm.GetOnboardingStatus( - status => - { - if (this == null) return; - if (status?.success != true) return; - if (BlockmakerAuth.Instance?.Address != expectedAddress) return; - - var token = status.tokenBalances?.Find(t => t.assetId == tokenAssetId); - long newConfirmed = token?.amount ?? 0; - if (token != null) _tokenDecimals = token.decimals; - - if (newConfirmed > _confirmedBalance) - { - long catchUp = newConfirmed - _confirmedBalance; - _pendingRewards = Math.Max(0, _pendingRewards - catchUp); - } - else if (_pendingRewards > 0 && - Time.realtimeSinceStartup - _lastRewardTime > pendingExpirySeconds) - { - _pendingRewards = 0; - } - - _confirmedBalance = newConfirmed; - OnBalanceChanged?.Invoke(DisplayBalance); - }, - err => BlockmakerLog.Warning($"[TokenBalanceTracker] Poll failed: {err}") - ); - } - - // ── Formatting ──────────────────────────────────────────────────────────── - - public static string FormatBalance(long amount, int decimals) - { - if (decimals <= 0) - return amount.ToString("N0"); - - double divisor = Math.Pow(10, decimals); - double value = amount / divisor; - return value.ToString("N" + Math.Min(decimals, 2)); - } - } - -} \ No newline at end of file diff --git a/Core/TokenBalanceTracker.cs.meta b/Core/TokenBalanceTracker.cs.meta deleted file mode 100644 index 5cbb78e..0000000 --- a/Core/TokenBalanceTracker.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: cbb586956f8bf40468ff8d838a811132 \ No newline at end of file diff --git a/Core/WalletConnectV1Client.cs b/Core/WalletConnectV1Client.cs deleted file mode 100644 index 53c843e..0000000 --- a/Core/WalletConnectV1Client.cs +++ /dev/null @@ -1,688 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Native WalletConnect v1 client for Unity. - /// Handles the full WC v1 protocol: WebSocket transport, AES-256-CBC encryption, - /// JSON-RPC session handshake, and custom requests (e.g. algo_signTxn). - /// No external dependencies — uses only System.* and UnityEngine. - /// - public class WalletConnectV1Client : IDisposable - { - // ── Public state ────────────────────────────────────────────────────────── - public string Uri { get; private set; } - public string Address { get; private set; } - public string PeerId { get; private set; } - - private volatile bool _connected; - public bool IsConnected => _connected; - - // ── Events (marshaled to the main thread via SynchronizationContext) ──────── - public event Action OnSessionApproved; - public event Action OnSessionRejected; - public event Action OnDisconnected; - public event Action OnError; - - private readonly SynchronizationContext _syncContext; - - // ── Session params ──────────────────────────────────────────────────────── - private readonly string _bridgeUrl; - private readonly byte[] _key; - private readonly string _handshakeTopic; - private readonly string _clientId; - private readonly long _handshakeId; - private readonly string _appName; - private readonly string _appDescription; - private readonly string _appUrl; - private readonly int _chainId; - - // ── WebSocket ───────────────────────────────────────────────────────────── - private ClientWebSocket _ws; - private CancellationTokenSource _cts; - - // ── Request tracking ────────────────────────────────────────────────────── - private readonly Dictionary> _pendingRequests - = new Dictionary>(); - private long _nextRequestId; - - private volatile bool _disposed; - public bool IsDisposed => _disposed; - - // ── Pera bridge servers ────────────────────────────────────────────────── - private static readonly string[] PERA_BRIDGES = new[] - { - "https://wallet-connect-a.perawallet.app", - "https://wallet-connect-b.perawallet.app", - "https://wallet-connect-c.perawallet.app", - "https://wallet-connect-d.perawallet.app", - "https://wallet-connect-e.perawallet.app", - "https://wallet-connect-f.perawallet.app", - "https://wallet-connect-g.perawallet.app", - "https://wallet-connect-h.perawallet.app", - }; - - // ── Session persistence ────────────────────────────────────────────────── - - [Serializable] - public class SavedSession - { - public string bridgeUrl; - public string keyHex; - public string clientId; - public string peerId; - public string address; - public int chainId; - } - - public SavedSession GetSessionData() - { - if (string.IsNullOrEmpty(PeerId) || string.IsNullOrEmpty(Address)) return null; - return new SavedSession - { - bridgeUrl = _bridgeUrl, - keyHex = BytesToHex(_key), - clientId = _clientId, - peerId = PeerId, - address = Address, - chainId = _chainId, - }; - } - - // ── Constructor ─────────────────────────────────────────────────────────── - - public WalletConnectV1Client( - string bridgeUrl = null, - int chainId = 4160, - string appName = null, - string appDescription = null, - string appUrl = null) - { - _syncContext = SynchronizationContext.Current; - _bridgeUrl = bridgeUrl ?? PERA_BRIDGES[GenerateRandomBytes(1)[0] % PERA_BRIDGES.Length]; - _chainId = chainId; - _appName = appName ?? Application.productName; - _appDescription = appDescription ?? Application.productName; - _appUrl = string.IsNullOrEmpty(appUrl) ? BlockmakerConfig.DefaultServerUrl : appUrl; - - _key = GenerateRandomBytes(32); - _handshakeTopic = Guid.NewGuid().ToString(); - _clientId = Guid.NewGuid().ToString(); - _handshakeId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - _nextRequestId = _handshakeId + 1; - - string hexKey = BytesToHex(_key); - string encodedBridge = System.Uri.EscapeDataString(_bridgeUrl); - Uri = $"wc:{_handshakeTopic}@1?bridge={encodedBridge}&key={hexKey}"; - - BlockmakerLog.Info($"[WCv1] Using bridge: {_bridgeUrl}"); - } - - public static WalletConnectV1Client FromSession(SavedSession session) - { - return new WalletConnectV1Client(session); - } - - private WalletConnectV1Client(SavedSession session) - { - _syncContext = SynchronizationContext.Current; - _bridgeUrl = session.bridgeUrl; - _chainId = session.chainId; - _appName = Application.productName; - _appDescription = Application.productName; - var cfgUrl = BlockmakerAuth.Instance?.blockmakerConfig?.dAppUrl; - if (string.IsNullOrEmpty(cfgUrl)) cfgUrl = BlockmakerAuth.Instance?.blockmakerConfig?.serverUrl; - _appUrl = string.IsNullOrEmpty(cfgUrl) ? BlockmakerConfig.DefaultServerUrl : cfgUrl; - - _key = HexToBytes(session.keyHex); - _handshakeTopic = ""; - _clientId = session.clientId; - _handshakeId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - _nextRequestId = _handshakeId + 1; - - PeerId = session.peerId; - Address = session.address; - - BlockmakerLog.Info($"[WCv1] Restored from session — bridge: {_bridgeUrl}, address: {Address}"); - } - - // ── Public API ──────────────────────────────────────────────────────────── - - public async Task Connect() - { - if (_disposed) - throw new ObjectDisposedException(nameof(WalletConnectV1Client)); - if (_ws != null) - throw new InvalidOperationException("Already connected or connecting"); - - if (_bridgeUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && - !_bridgeUrl.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase) && - !_bridgeUrl.StartsWith("http://127.0.0.1", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("WalletConnect bridge must use HTTPS. Insecure HTTP bridges are not allowed."); - } - - _cts = new CancellationTokenSource(); - _ws = new ClientWebSocket(); - _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(15); - - string wsUrl = _bridgeUrl - .Replace("https://", "wss://") - .Replace("http://", "ws://"); - wsUrl += $"?protocol=wc&version=1&env=unity"; - - await _ws.ConnectAsync(new System.Uri(wsUrl), _cts.Token); - BlockmakerLog.Info("[WCv1] WebSocket connected to bridge"); - - _ = ReceiveLoop(); - _ = HeartbeatLoop(); - - await Subscribe(_clientId); - await Subscribe(_handshakeTopic); - - await SendSessionRequest(); - } - - public async Task Reconnect() - { - if (_disposed) - throw new ObjectDisposedException(nameof(WalletConnectV1Client)); - if (string.IsNullOrEmpty(PeerId)) - throw new InvalidOperationException("No session to reconnect"); - - _connected = false; - var oldCts = _cts; - var oldWs = _ws; - _cts = new CancellationTokenSource(); - _ws = new ClientWebSocket(); - try { oldCts?.Cancel(); } catch { } - try { oldWs?.Dispose(); } catch { } - try { oldCts?.Dispose(); } catch { } - _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(15); - - string wsUrl = _bridgeUrl - .Replace("https://", "wss://") - .Replace("http://", "ws://"); - wsUrl += $"?protocol=wc&version=1&env=unity"; - - await _ws.ConnectAsync(new System.Uri(wsUrl), _cts.Token); - BlockmakerLog.Info("[WCv1] WebSocket reconnected to bridge"); - - _ = ReceiveLoop(); - _ = HeartbeatLoop(); - - await Subscribe(_clientId); - - _connected = true; - BlockmakerLog.Info($"[WCv1] Session restored — Address: {Address}, PeerId: {PeerId}"); - } - - public async Task SendCustomRequest(string method, string paramsJson) - { - if (_disposed) - throw new ObjectDisposedException(nameof(WalletConnectV1Client)); - if (!IsConnected || string.IsNullOrEmpty(PeerId)) - throw new InvalidOperationException("No active session"); - - long id = Interlocked.Increment(ref _nextRequestId); - string request = $"{{\"id\":{id},\"jsonrpc\":\"2.0\",\"method\":\"{method}\",\"params\":{paramsJson}}}"; - - var tcs = new TaskCompletionSource(); - lock (_pendingRequests) _pendingRequests[id] = tcs; - - await PublishEncrypted(PeerId, request); - - using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - timeout.Token.Register(() => - { - lock (_pendingRequests) _pendingRequests.Remove(id); - tcs.TrySetException(new TimeoutException("Wallet did not respond in time")); - }); - - return await tcs.Task; - } - - public async Task Disconnect() - { - bool wasConnected = _connected; - - if (_ws != null && _ws.State == WebSocketState.Open) - { - try - { - if (!string.IsNullOrEmpty(PeerId)) - { - long id = Interlocked.Increment(ref _nextRequestId); - string req = $"{{\"id\":{id},\"jsonrpc\":\"2.0\",\"method\":\"wc_sessionUpdate\",\"params\":[{{\"approved\":false,\"chainId\":{_chainId},\"accounts\":[]}}]}}"; - await PublishEncrypted(PeerId, req); - } - await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - catch { } - } - - _cts?.Cancel(); - _connected = false; - Address = null; - PeerId = null; - - if (wasConnected) - PostToMain(() => OnDisconnected?.Invoke()); - } - - // ── Session request ─────────────────────────────────────────────────────── - - private async Task SendSessionRequest() - { - string request = - $"{{\"id\":{_handshakeId},\"jsonrpc\":\"2.0\",\"method\":\"wc_sessionRequest\"," + - $"\"params\":[{{\"peerId\":\"{_clientId}\",\"peerMeta\":{{\"name\":\"{EscapeJson(_appName)}\"," + - $"\"description\":\"{EscapeJson(_appDescription)}\",\"url\":\"{EscapeJson(_appUrl)}\",\"icons\":[]}}," + - $"\"chainId\":{_chainId}}}]}}"; - - await PublishEncrypted(_handshakeTopic, request); - BlockmakerLog.Info("[WCv1] Session request sent"); - } - - private static string EscapeJson(string s) => - s.Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t"); - - // ── WebSocket transport ─────────────────────────────────────────────────── - - private async Task HeartbeatLoop() - { - var ct = _cts.Token; - try - { - while (!_disposed && !ct.IsCancellationRequested && _ws != null && _ws.State == WebSocketState.Open) - { - await Task.Delay(TimeSpan.FromSeconds(20), ct); - if (_ws?.State == WebSocketState.Open) - await Subscribe(_clientId); - } - } - catch (OperationCanceledException) { } - catch { } - } - - private async Task Subscribe(string topic) - { - string msg = $"{{\"topic\":\"{topic}\",\"type\":\"sub\",\"payload\":\"\",\"silent\":true}}"; - await SendRaw(msg); - BlockmakerLog.Info($"[WCv1] Subscribed to {topic.Substring(0, 8)}..."); - } - - private async Task PublishEncrypted(string topic, string plaintext) - { - string envelope = Encrypt(plaintext, _key); - string escapedPayload = envelope.Replace("\\", "\\\\").Replace("\"", "\\\""); - string msg = $"{{\"topic\":\"{topic}\",\"type\":\"pub\",\"payload\":\"{escapedPayload}\",\"silent\":true}}"; - await SendRaw(msg); - } - - private async Task SendRaw(string message) - { - var ws = _ws; - if (ws == null || ws.State != WebSocketState.Open) return; - byte[] bytes = Encoding.UTF8.GetBytes(message); - await ws.SendAsync( - new ArraySegment(bytes), - WebSocketMessageType.Text, - true, - _cts?.Token ?? CancellationToken.None); - } - - private async Task ReceiveLoop() - { - var buffer = new byte[16384]; - var sb = new StringBuilder(); - var ct = _cts.Token; - - try - { - while (_ws.State == WebSocketState.Open && !ct.IsCancellationRequested) - { - sb.Clear(); - WebSocketReceiveResult result; - do - { - result = await _ws.ReceiveAsync( - new ArraySegment(buffer), ct); - if (result.MessageType == WebSocketMessageType.Close) return; - sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } while (!result.EndOfMessage); - - string raw = sb.ToString(); - ProcessMessage(raw); - } - } - catch (OperationCanceledException) { } - catch (WebSocketException ex) - { - if (!_disposed) - { - BlockmakerLog.Warning($"[WCv1] WebSocket error: {ex.Message}"); - var msg = ex.Message; - PostToMain(() => OnError?.Invoke(msg)); - } - } - catch (Exception ex) - { - if (!_disposed) - { - BlockmakerLog.Error($"[WCv1] Receive error: {ex.Message}"); - var msg = ex.Message; - PostToMain(() => OnError?.Invoke(msg)); - } - } - finally - { - if (_connected && !_disposed) - { - _connected = false; - PostToMain(() => OnDisconnected?.Invoke()); - } - } - } - - // ── Message processing ──────────────────────────────────────────────────── - - private void ProcessMessage(string raw) - { - try - { - var msg = JsonUtility.FromJson(raw); - - if (msg.type == "ack" || string.IsNullOrEmpty(msg.payload)) return; - - string decrypted = Decrypt(msg.payload, _key); - if (decrypted == null) return; - - var rpc = JsonUtility.FromJson(decrypted); - - if (rpc.id == _handshakeId && !IsConnected) - { - HandleSessionResponse(decrypted); - return; - } - - if (!string.IsNullOrEmpty(rpc.method)) - { - HandleIncomingRequest(decrypted, rpc.method); - return; - } - - lock (_pendingRequests) - { - if (_pendingRequests.TryGetValue(rpc.id, out var tcs)) - { - _pendingRequests.Remove(rpc.id); - tcs.TrySetResult(decrypted); - } - } - } - catch (Exception ex) - { - BlockmakerLog.Warning($"[WCv1] Failed to process message: {ex.Message}"); - } - } - - private void HandleSessionResponse(string json) - { - try - { - var response = JsonUtility.FromJson(json); - - if (response.result == null || !response.result.approved) - { - BlockmakerLog.Info("[WCv1] Session rejected by wallet"); - PostToMain(() => OnSessionRejected?.Invoke("Wallet rejected the connection")); - return; - } - - PeerId = response.result.peerId; - if (response.result.accounts != null && response.result.accounts.Length > 0) - Address = response.result.accounts[0]; - - _connected = true; - - BlockmakerLog.Info($"[WCv1] Session approved! Address: {Address}, PeerId: {PeerId}"); - var addr = Address; - PostToMain(() => OnSessionApproved?.Invoke(addr)); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[WCv1] Failed to parse session response: {ex.Message}"); - PostToMain(() => OnError?.Invoke("Failed to parse wallet response")); - } - } - - private void HandleIncomingRequest(string json, string method) - { - if (method == "wc_sessionUpdate") - { - var update = JsonUtility.FromJson(json); - if (update.@params != null && update.@params.Length > 0 && !update.@params[0].approved - && _connected) - { - BlockmakerLog.Info("[WCv1] Wallet disconnected session"); - _connected = false; - Address = null; - PeerId = null; - PostToMain(() => OnDisconnected?.Invoke()); - } - } - } - - // ── AES-256-CBC + HMAC-SHA256 encryption ────────────────────────────────── - - private static string Encrypt(string plaintext, byte[] key) - { - byte[] iv = GenerateRandomBytes(16); - byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext); - - byte[] ciphertext; - using (var aes = Aes.Create()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - aes.Key = key; - aes.IV = iv; - using var enc = aes.CreateEncryptor(); - ciphertext = enc.TransformFinalBlock(plainBytes, 0, plainBytes.Length); - } - - byte[] dataToSign = new byte[ciphertext.Length + iv.Length]; - Buffer.BlockCopy(ciphertext, 0, dataToSign, 0, ciphertext.Length); - Buffer.BlockCopy(iv, 0, dataToSign, ciphertext.Length, iv.Length); - - byte[] hmac; - using (var hmacSha = new HMACSHA256(key)) - { - hmac = hmacSha.ComputeHash(dataToSign); - } - - string dataHex = BytesToHex(ciphertext); - string ivHex = BytesToHex(iv); - string hmacHex = BytesToHex(hmac); - - return $"{{\"data\":\"{dataHex}\",\"iv\":\"{ivHex}\",\"hmac\":\"{hmacHex}\"}}"; - } - - private static string Decrypt(string envelopeJson, byte[] key) - { - var env = JsonUtility.FromJson(envelopeJson); - if (string.IsNullOrEmpty(env.data) || string.IsNullOrEmpty(env.iv) || string.IsNullOrEmpty(env.hmac)) - return null; - - byte[] ciphertext = HexToBytes(env.data); - byte[] iv = HexToBytes(env.iv); - byte[] receivedHmac = HexToBytes(env.hmac); - - byte[] dataToVerify = new byte[ciphertext.Length + iv.Length]; - Buffer.BlockCopy(ciphertext, 0, dataToVerify, 0, ciphertext.Length); - Buffer.BlockCopy(iv, 0, dataToVerify, ciphertext.Length, iv.Length); - - using (var hmacSha = new HMACSHA256(key)) - { - byte[] computedHmac = hmacSha.ComputeHash(dataToVerify); - if (!ConstantTimeEquals(computedHmac, receivedHmac)) - { - BlockmakerLog.Warning("[WCv1] HMAC verification failed"); - return null; - } - } - - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - aes.Key = key; - aes.IV = iv; - using var dec = aes.CreateDecryptor(); - byte[] plainBytes = dec.TransformFinalBlock(ciphertext, 0, ciphertext.Length); - return Encoding.UTF8.GetString(plainBytes); - } - - // ── Thread marshaling ────────────────────────────────────────────────────── - - private void PostToMain(Action action) - { - if (_syncContext != null) - _syncContext.Post(_ => action(), null); - else - action(); - } - - // ── Helpers ─────────────────────────────────────────────────────────────── - - private static byte[] GenerateRandomBytes(int count) - { - byte[] bytes = new byte[count]; - using var rng = RandomNumberGenerator.Create(); - rng.GetBytes(bytes); - return bytes; - } - - private static string BytesToHex(byte[] bytes) - { - var sb = new StringBuilder(bytes.Length * 2); - foreach (byte b in bytes) - sb.Append(b.ToString("x2")); - return sb.ToString(); - } - - private static byte[] HexToBytes(string hex) - { - if (string.IsNullOrEmpty(hex) || hex.Length % 2 != 0) - throw new ArgumentException("Hex string must be non-empty and even length."); - byte[] bytes = new byte[hex.Length / 2]; - for (int i = 0; i < bytes.Length; i++) - bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); - return bytes; - } - - private static bool ConstantTimeEquals(byte[] a, byte[] b) - { - if (a.Length != b.Length) return false; - int diff = 0; - for (int i = 0; i < a.Length; i++) - diff |= a[i] ^ b[i]; - return diff == 0; - } - - // ── Serialization types ─────────────────────────────────────────────────── - - [Serializable] - private class BridgeMessage - { - public string topic; - public string type; - public string payload; - } - - [Serializable] - private class EncryptionEnvelope - { - public string data; - public string iv; - public string hmac; - } - - [Serializable] - private class JsonRpcEnvelope - { - public long id; - public string method; - } - - [Serializable] - private class SessionResponseWrapper - { - public long id; - public SessionResult result; - } - - [Serializable] - private class SessionResult - { - public bool approved; - public int chainId; - public string[] accounts; - public string peerId; - } - - [Serializable] - private class SessionUpdateWrapper - { - public long id; - public string method; - public SessionUpdate[] @params; - } - - [Serializable] - private class SessionUpdate - { - public bool approved; - } - - // ── IDisposable ─────────────────────────────────────────────────────────── - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - _connected = false; - - if (_key != null) Array.Clear(_key, 0, _key.Length); - - OnSessionApproved = null; - OnSessionRejected = null; - OnDisconnected = null; - OnError = null; - - lock (_pendingRequests) - { - foreach (var tcs in _pendingRequests.Values) - tcs.TrySetCanceled(); - _pendingRequests.Clear(); - } - - _cts?.Cancel(); - _cts?.Dispose(); - try { _ws?.Dispose(); } catch { } - } - } - -} \ No newline at end of file diff --git a/Core/WalletConnectV1Client.cs.meta b/Core/WalletConnectV1Client.cs.meta deleted file mode 100644 index 16c3620..0000000 --- a/Core/WalletConnectV1Client.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8c0b730c30efa475fb0e823e7861d94a \ No newline at end of file diff --git a/Core/WalletDeepLink.cs b/Core/WalletDeepLink.cs deleted file mode 100644 index 9c225c9..0000000 --- a/Core/WalletDeepLink.cs +++ /dev/null @@ -1,108 +0,0 @@ -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Converts WalletConnect URIs to mobile deep links for Pera and Defly wallets. - /// On mobile platforms — including WebGL running in a mobile browser — opens the - /// wallet app directly. On desktop, no-ops (the QR code is the desktop flow). - /// - /// Note for callers: on WebGL, Application.OpenURL performs a browser location - /// change. To survive iOS Safari's popup/navigation blocking it must be invoked - /// synchronously from a user-gesture handler (e.g. a button's clicked event) — - /// which is how the AuthPrompt / PeraConnectModal "Open in wallet app" buttons - /// call into this class. - /// - public static class WalletDeepLink - { - /// Open the Pera wallet app with a WalletConnect URI. - public static void OpenPera(string wcUri) - { - if (!IsMobilePlatform) return; - string encoded = UnityEngine.Networking.UnityWebRequest.EscapeURL(wcUri); - #if UNITY_WEBGL && !UNITY_EDITOR - // Mobile browser: prefer Pera's https universal link — it opens the app when - // installed and falls back to a Pera web page otherwise, whereas an - // unregistered custom scheme fails silently in the browser. This is the same - // link form Pera encodes in its own QR codes. - Application.OpenURL($"https://perawallet.app/qr/perawallet-wc/?uri={encoded}"); - #else - // Native platforms: Pera's registered custom scheme. - Application.OpenURL($"perawallet-wc://wc?uri={encoded}"); - #endif - } - - /// Open the Defly wallet app with a WalletConnect URI. - public static void OpenDefly(string wcUri) - { - if (!IsMobilePlatform) return; - string encoded = UnityEngine.Networking.UnityWebRequest.EscapeURL(wcUri); - #if UNITY_WEBGL && !UNITY_EDITOR - // Defly has no documented https universal link. Mirror the official - // defly-connect JS SDK (blockshake-io/defly-connect, deflyWalletUtils.ts): - // Android browser → open the raw wc: URI (Android intent resolution - // routes it to the installed WC-capable wallet); - // iOS browser → Defly's registered "defly-wc://wc?uri=" scheme. - bool isAndroidBrowser = false; - try { isAndroidBrowser = SystemInfo.operatingSystem.IndexOf("Android", System.StringComparison.OrdinalIgnoreCase) >= 0; } - catch { /* fall through to the iOS-style scheme link */ } - - if (isAndroidBrowser) - Application.OpenURL(wcUri); - else - Application.OpenURL($"defly-wc://wc?uri={encoded}"); - #else - Application.OpenURL($"algorand-wc://wc?uri={encoded}"); - #endif - } - - /// Open the appropriate wallet app based on provider name. - public static void OpenWallet(string providerName, string wcUri) - { - if (string.IsNullOrEmpty(wcUri)) return; - - switch (providerName?.ToLower()) - { - case "pera": - OpenPera(wcUri); - break; - case "defly": - OpenDefly(wcUri); - break; - default: - BlockmakerLog.Warning($"[WalletDeepLink] Unknown provider: {providerName}"); - break; - } - } - - /// - /// True on iOS and Android, and on WebGL when the browser reports a mobile - /// device (see BmIsMobileBrowser in BlockmakerWalletBridge.jslib). - /// - public static bool IsMobilePlatform - { - get - { - if (Application.platform == RuntimePlatform.IPhonePlayer || - Application.platform == RuntimePlatform.Android) - return true; - - #if UNITY_WEBGL && !UNITY_EDITOR - try { return BlockmakerWalletBridge.BmIsMobileBrowser() != 0; } - catch { return false; } - #else - return false; - #endif - } - } - - /// Open a generic WalletConnect URI — lets the OS choose the handler. - public static void OpenGeneric(string wcUri) - { - if (!IsMobilePlatform || string.IsNullOrEmpty(wcUri)) return; - Application.OpenURL(wcUri); - } - } - -} diff --git a/Core/WalletDeepLink.cs.meta b/Core/WalletDeepLink.cs.meta deleted file mode 100644 index 7600748..0000000 --- a/Core/WalletDeepLink.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a47b43f736c6d4775a63f3116200823b \ No newline at end of file diff --git a/Core/XChainAddressDeriver.cs b/Core/XChainAddressDeriver.cs deleted file mode 100644 index 396002d..0000000 --- a/Core/XChainAddressDeriver.cs +++ /dev/null @@ -1,518 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace Blockmaker -{ - - /// - /// Pure C# xChain Accounts implementation. Derives deterministic Algorand - /// LogicSig addresses from EVM addresses and provides signing utilities. - /// SHA-512/256, Base32, and msgpack encoding are implemented internally - /// so the Blockmaker SDK has zero external crypto dependencies. - /// LogicSig program template per the Algorand Foundation's xchain-accounts - /// (github.com/algorandfoundation/xchain-accounts, MIT). - /// - public static class XChainAddressDeriver - { - private static readonly byte[] ProgramTag = Encoding.ASCII.GetBytes("Program"); - private static readonly byte[] TxTag = Encoding.ASCII.GetBytes("TX"); - - private static readonly byte[] ProgramPrefix = { 0x0b, 0x26, 0x01, 0x14 }; - - private static readonly byte[] ProgramSuffix = { - 0x32, 0x04, 0x81, 0x01, 0x12, 0x41, 0x00, 0x79, 0x31, 0x17, - 0x2d, 0x49, 0x57, 0x00, 0x01, 0x80, 0x01, 0x01, 0x12, 0x44, - 0x49, 0x57, 0x01, 0x20, 0x4b, 0x01, 0x57, 0x21, 0x20, 0x4f, - 0x02, 0x81, 0x41, 0x55, 0x81, 0x1b, 0x09, 0x80, 0x20, 0x61, - 0x2f, 0x25, 0x98, 0xeb, 0xd9, 0x64, 0xc1, 0x6b, 0xa6, 0x7a, - 0x8b, 0x06, 0xd6, 0xf0, 0x8c, 0xe2, 0x4a, 0xb0, 0x91, 0x1f, - 0x0f, 0xf5, 0xa2, 0x67, 0xa2, 0x2f, 0xe0, 0x1e, 0x68, 0x73, - 0x34, 0x4f, 0x04, 0x50, 0x02, 0x80, 0x22, 0x19, 0x01, 0xce, - 0xf8, 0xb9, 0x82, 0x94, 0x14, 0xba, 0x4a, 0x13, 0xea, 0x8f, - 0x8c, 0x44, 0x2b, 0x74, 0x7f, 0xfe, 0x11, 0x9c, 0x64, 0x3d, - 0x22, 0x13, 0xd2, 0x2b, 0x4e, 0x13, 0x70, 0x36, 0xa2, 0xd5, - 0x73, 0x4c, 0x50, 0x02, 0x4c, 0x4f, 0x03, 0x4f, 0x03, 0x07, - 0x00, 0x50, 0x02, 0x57, 0x0c, 0x14, 0x28, 0x12, 0x43, 0x32, - 0x0b, 0x42, 0xff, 0x84 - }; - - public static string DeriveAlgorandAddress(string evmAddress) - { - var program = GetLogicSigProgram(evmAddress); - var data = new byte[ProgramTag.Length + program.Length]; - Buffer.BlockCopy(ProgramTag, 0, data, 0, ProgramTag.Length); - Buffer.BlockCopy(program, 0, data, ProgramTag.Length, program.Length); - return EncodeAlgorandAddress(Sha512_256(data)); - } - - public static byte[] GetLogicSigProgram(string evmAddress) - { - var hex = evmAddress.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? evmAddress.Substring(2) : evmAddress; - if (hex.Length != 40) - throw new ArgumentException("EVM address must be 20 bytes (40 hex chars)."); - - var evmBytes = HexToBytes(hex); - var program = new byte[ProgramPrefix.Length + evmBytes.Length + ProgramSuffix.Length]; - Buffer.BlockCopy(ProgramPrefix, 0, program, 0, ProgramPrefix.Length); - Buffer.BlockCopy(evmBytes, 0, program, ProgramPrefix.Length, evmBytes.Length); - Buffer.BlockCopy(ProgramSuffix, 0, program, ProgramPrefix.Length + evmBytes.Length, ProgramSuffix.Length); - return program; - } - - public static byte[] ComputeTransactionId(byte[] unsignedTxn) - { - var data = new byte[TxTag.Length + unsignedTxn.Length]; - Buffer.BlockCopy(TxTag, 0, data, 0, TxTag.Length); - Buffer.BlockCopy(unsignedTxn, 0, data, TxTag.Length, unsignedTxn.Length); - return Sha512_256(data); - } - - /// - /// Extracts the Algorand GroupID from an unsigned transaction's msgpack bytes. - /// Returns null if the transaction has no group field (not part of a group). - /// Walks the top-level msgpack map keys structurally (not a brute-force scan) - /// to avoid false matches inside binary field values. - /// - public static byte[] ExtractGroupId(byte[] d) - { - if (d == null || d.Length < 2) return null; - int pos = 0; - - // Read top-level fixmap or map16 entry count - int mapSize; - byte header = d[pos++]; - if ((header & 0xF0) == 0x80) - mapSize = header & 0x0F; - else if (header == 0xDE && pos + 1 < d.Length) - { mapSize = (d[pos] << 8) | d[pos + 1]; pos += 2; } - else - return null; - - for (int entry = 0; entry < mapSize && pos < d.Length; entry++) - { - // Read key (must be fixstr for Algorand canonical msgpack) - if (pos >= d.Length) return null; - byte kh = d[pos]; - if ((kh & 0xE0) != 0xA0) return null; // not a fixstr - int keyLen = kh & 0x1F; - pos++; - if (pos + keyLen > d.Length) return null; - - bool isGrp = keyLen == 3 && d[pos] == 0x67 && d[pos+1] == 0x72 && d[pos+2] == 0x70; - pos += keyLen; - - if (isGrp) - { - // Value must be bin8 with length 32 - if (pos + 34 > d.Length) return null; - if (d[pos] == 0xc4 && d[pos+1] == 0x20) - { - var groupId = new byte[32]; - Buffer.BlockCopy(d, pos + 2, groupId, 0, 32); - return groupId; - } - return null; // grp key found but value format unexpected - } - - // Skip value — need to walk past it to reach next key - if (pos >= d.Length) return null; - pos = SkipMsgpackValue(d, pos); - if (pos < 0) return null; - } - return null; - } - - static int SkipMsgpackValue(byte[] d, int pos) - { - if (pos >= d.Length) return -1; - byte b = d[pos++]; - - // positive fixint (0x00-0x7f) or negative fixint (0xe0-0xff) - if (b <= 0x7f || b >= 0xe0) return pos; - // fixstr (0xa0-0xbf) - if ((b & 0xE0) == 0xA0) return pos + (b & 0x1F); - // fixarray (0x90-0x9f) - if ((b & 0xF0) == 0x90) { int n = b & 0x0F; for (int i = 0; i < n; i++) { pos = SkipMsgpackValue(d, pos); if (pos < 0) return -1; } return pos; } - // fixmap (0x80-0x8f) - if ((b & 0xF0) == 0x80) { int n = b & 0x0F; for (int i = 0; i < n * 2; i++) { pos = SkipMsgpackValue(d, pos); if (pos < 0) return -1; } return pos; } - - switch (b) - { - case 0xc0: return pos; // nil - case 0xc1: return -1; // never used - case 0xc2: case 0xc3: return pos; // bool - case 0xc4: if (pos >= d.Length) return -1; return pos + 1 + d[pos]; // bin8 - case 0xc5: if (pos + 1 >= d.Length) return -1; return pos + 2 + ((d[pos] << 8) | d[pos+1]); // bin16 - case 0xc6: if (pos + 3 >= d.Length) return -1; return pos + 4 + (((int)d[pos] << 24) | (d[pos+1] << 16) | (d[pos+2] << 8) | d[pos+3]); // bin32 - case 0xc7: if (pos >= d.Length) return -1; return pos + 2 + d[pos]; // ext8 - case 0xc8: if (pos + 1 >= d.Length) return -1; return pos + 3 + ((d[pos] << 8) | d[pos+1]); // ext16 - case 0xc9: if (pos + 3 >= d.Length) return -1; return pos + 5 + (((int)d[pos] << 24) | (d[pos+1] << 16) | (d[pos+2] << 8) | d[pos+3]); // ext32 - case 0xca: return pos + 4; // float32 - case 0xcb: return pos + 8; // float64 - case 0xcc: return pos + 1; // uint8 - case 0xcd: return pos + 2; // uint16 - case 0xce: return pos + 4; // uint32 - case 0xcf: return pos + 8; // uint64 - case 0xd0: return pos + 1; // int8 - case 0xd1: return pos + 2; // int16 - case 0xd2: return pos + 4; // int32 - case 0xd3: return pos + 8; // int64 - case 0xd4: return pos + 2; // fixext1 - case 0xd5: return pos + 3; // fixext2 - case 0xd6: return pos + 5; // fixext4 - case 0xd7: return pos + 9; // fixext8 - case 0xd8: return pos + 17; // fixext16 - case 0xd9: if (pos >= d.Length) return -1; return pos + 1 + d[pos]; // str8 - case 0xda: if (pos + 1 >= d.Length) return -1; return pos + 2 + ((d[pos] << 8) | d[pos+1]); // str16 - case 0xdb: if (pos + 3 >= d.Length) return -1; return pos + 4 + (((int)d[pos] << 24) | (d[pos+1] << 16) | (d[pos+2] << 8) | d[pos+3]); // str32 - case 0xdc: // array16 - if (pos + 1 >= d.Length) return -1; - { int n = (d[pos] << 8) | d[pos+1]; pos += 2; for (int i = 0; i < n; i++) { pos = SkipMsgpackValue(d, pos); if (pos < 0) return -1; } return pos; } - case 0xdd: // array32 - if (pos + 3 >= d.Length) return -1; - { int n = (d[pos] << 16) | (d[pos+1] << 8) | d[pos+2]; pos += 4; for (int i = 0; i < n; i++) { pos = SkipMsgpackValue(d, pos); if (pos < 0) return -1; } return pos; } - case 0xde: // map16 - if (pos + 1 >= d.Length) return -1; - { int n = (d[pos] << 8) | d[pos+1]; pos += 2; for (int i = 0; i < n * 2; i++) { pos = SkipMsgpackValue(d, pos); if (pos < 0) return -1; } return pos; } - case 0xdf: // map32 - if (pos + 3 >= d.Length) return -1; - { int n = (d[pos] << 16) | (d[pos+1] << 8) | d[pos+2]; pos += 4; for (int i = 0; i < n * 2; i++) { pos = SkipMsgpackValue(d, pos); if (pos < 0) return -1; } return pos; } - default: return -1; - } - } - - public static string BuildEip712TypedData(byte[] txnId) - { - var hex = "0x" + BytesToHex(txnId); - return "{" + - "\"types\":{" + - "\"EIP712Domain\":[" + - "{\"name\":\"name\",\"type\":\"string\"}," + - "{\"name\":\"version\",\"type\":\"string\"}" + - "]," + - "\"Algorand Transaction\":[" + - "{\"name\":\"Transaction ID\",\"type\":\"bytes32\"}" + - "]" + - "}," + - "\"primaryType\":\"Algorand Transaction\"," + - "\"domain\":{" + - "\"name\":\"Algorand x EVM\"," + - "\"version\":\"1\"" + - "}," + - "\"message\":{" + - "\"Transaction ID\":\"" + hex + "\"" + - "}" + - "}"; - } - - /// - /// Converts a 65-byte EVM signature (r+s+v) into the 66-byte xChain LogicSig arg. - /// Format: [0x01, r(32), s(32), v_normalized(1)] - /// - public static byte[] ParseEvmSignature(string hexSig) - { - var hex = hexSig.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? hexSig.Substring(2) : hexSig; - if (hex.Length != 130) - throw new ArgumentException("Expected 65-byte (130 hex char) EVM signature."); - - var sig = HexToBytes(hex); - - // Extract r, s, v from the 65-byte signature - var r = new byte[32]; - var s = new byte[32]; - Buffer.BlockCopy(sig, 0, r, 0, 32); - Buffer.BlockCopy(sig, 32, s, 0, 32); - byte v = sig[64]; - - // Lower-S normalization (prevents ECDSA malleability). - // If s > secp256k1_n/2, flip to s = n - s and toggle v. - // Matches the official xchain-accounts SDK's normalizeLowerS(). - if (IsHighS(s)) - { - s = SubtractFromN(s); - v = (byte)(v == 27 ? 28 : v == 28 ? 27 : v == 0 ? 1 : v == 1 ? 0 : v); - } - - // Normalize v to 27/28 — the TEAL LogicSig subtracts 27 on-chain. - // Some wallets return 0/1 (non-EIP-155), others return 27/28. - if (v == 0) v = 27; - else if (v == 1) v = 28; - - if (v != 27 && v != 28) - throw new ArgumentException("Invalid EVM signature recovery id"); - - var arg = new byte[66]; - arg[0] = 0x01; - Buffer.BlockCopy(r, 0, arg, 1, 32); - Buffer.BlockCopy(s, 0, arg, 33, 32); - arg[65] = v; - return arg; - } - - /// - /// Wraps an unsigned Algorand transaction with a LogicSig to produce - /// the final signed transaction bytes (canonical msgpack). - /// - public static byte[] BuildSignedTransaction(byte[] unsignedTxn, byte[] program, byte[] sigArg) - { - using (var ms = new MemoryStream()) - { - ms.WriteByte(0x82); // fixmap(2): "lsig", "txn" - - MsgpackFixStr(ms, "lsig"); - ms.WriteByte(0x82); // fixmap(2): "arg", "l" - - MsgpackFixStr(ms, "arg"); - ms.WriteByte(0x91); // fixarray(1) - MsgpackBin(ms, sigArg); - - MsgpackFixStr(ms, "l"); - MsgpackBin(ms, program); - - MsgpackFixStr(ms, "txn"); - ms.Write(unsignedTxn, 0, unsignedTxn.Length); - - return ms.ToArray(); - } - } - - // ── Msgpack helpers ─────────────────────────────────────────────────────── - - static void MsgpackFixStr(Stream s, string str) - { - var b = Encoding.ASCII.GetBytes(str); - if (b.Length > 31) - throw new ArgumentException($"MsgpackFixStr only supports strings up to 31 bytes, got {b.Length}."); - s.WriteByte((byte)(0xa0 | b.Length)); - s.Write(b, 0, b.Length); - } - - static void MsgpackBin(Stream s, byte[] data) - { - if (data.Length > 65535) - throw new ArgumentException("MsgpackBin data exceeds maximum size"); - if (data.Length <= 255) - { - s.WriteByte(0xc4); - s.WriteByte((byte)data.Length); - } - else - { - s.WriteByte(0xc5); - s.WriteByte((byte)(data.Length >> 8)); - s.WriteByte((byte)data.Length); - } - s.Write(data, 0, data.Length); - } - - // ── Algorand address encoding ───────────────────────────────────────────── - - static string EncodeAlgorandAddress(byte[] publicKey) - { - var checksum = Sha512_256(publicKey); - var raw = new byte[36]; - Buffer.BlockCopy(publicKey, 0, raw, 0, 32); - Buffer.BlockCopy(checksum, 28, raw, 32, 4); - return Base32Encode(raw); - } - - // ── Base32 (RFC 4648, no padding) ───────────────────────────────────────── - - const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - - static string Base32Encode(byte[] data) - { - var sb = new StringBuilder((data.Length * 8 + 4) / 5); - int buf = 0, bits = 0; - for (int i = 0; i < data.Length; i++) - { - buf = (buf << 8) | data[i]; - bits += 8; - while (bits >= 5) { bits -= 5; sb.Append(Base32Alphabet[(buf >> bits) & 0x1F]); } - } - if (bits > 0) - sb.Append(Base32Alphabet[(buf << (5 - bits)) & 0x1F]); - return sb.ToString(); - } - - internal static byte[] Base32Decode(string encoded) - { - int totalBits = encoded.Length * 5; - byte[] result = new byte[totalBits / 8]; - int buf = 0, bits = 0, idx = 0; - for (int i = 0; i < encoded.Length; i++) - { - char c = encoded[i]; - int val; - if (c >= 'A' && c <= 'Z') val = c - 'A'; - else if (c >= '2' && c <= '7') val = c - '2' + 26; - else throw new ArgumentException($"Invalid Base32 character: {c}"); - buf = (buf << 5) | val; - bits += 5; - if (bits >= 8) - { - bits -= 8; - result[idx++] = (byte)((buf >> bits) & 0xFF); - } - } - return result; - } - - // ── secp256k1 lower-S normalization ─────────────────────────────────────── - - // secp256k1 curve order n and n/2 (big-endian, 32 bytes) - static readonly byte[] Secp256k1N = new byte[] { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE, - 0xBA,0xAE,0xDC,0xE6,0xAF,0x48,0xA0,0x3B,0xBF,0xD2,0x5E,0x8C,0xD0,0x36,0x41,0x41 }; - static readonly byte[] Secp256k1HalfN = new byte[] { - 0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0x5D,0x57,0x6E,0x73,0x57,0xA4,0x50,0x1D,0xDF,0xE9,0x2F,0x46,0x68,0x1B,0x20,0xA0 }; - - static bool IsHighS(byte[] s) - { - for (int i = 0; i < 32; i++) - { - if (s[i] < Secp256k1HalfN[i]) return false; - if (s[i] > Secp256k1HalfN[i]) return true; - } - return false; // equal to halfN is not high - } - - static byte[] SubtractFromN(byte[] s) - { - var result = new byte[32]; - int borrow = 0; - for (int i = 31; i >= 0; i--) - { - int diff = Secp256k1N[i] - s[i] - borrow; - if (diff < 0) { diff += 256; borrow = 1; } - else borrow = 0; - result[i] = (byte)diff; - } - return result; - } - - // ── Hex utilities ───────────────────────────────────────────────────────── - - static byte[] HexToBytes(string hex) - { - var b = new byte[hex.Length / 2]; - for (int i = 0; i < b.Length; i++) - b[i] = (byte)((HexVal(hex[i * 2]) << 4) | HexVal(hex[i * 2 + 1])); - return b; - } - - static int HexVal(char c) - { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - throw new ArgumentException($"Invalid hex character: {c}"); - } - - static string BytesToHex(byte[] data) - { - var sb = new StringBuilder(data.Length * 2); - for (int i = 0; i < data.Length; i++) - sb.Append(data[i].ToString("x2")); - return sb.ToString(); - } - - // ── SHA-512/256 (FIPS 180-4) ────────────────────────────────────────────── - - static readonly ulong[] K = { - 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, - 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, - 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, - 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694, - 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, - 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, - 0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, - 0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70, - 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df, - 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, - 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, - 0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, - 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, - 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3, - 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, - 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, - 0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, - 0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b, - 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, - 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 - }; - - internal static byte[] Sha512_256(byte[] msg) - { - ulong h0 = 0x22312194FC2BF72C, h1 = 0x9F555FA3C84C64C2; - ulong h2 = 0x2393B86B6F53B151, h3 = 0x963877195940EABD; - ulong h4 = 0x96283EE2A88EFFE3, h5 = 0xBE5E1E2553863992; - ulong h6 = 0x2B0199FC2C85B8AA, h7 = 0x0EB72DDC81C52CA2; - - ulong bitLen = (ulong)msg.Length * 8; - int padded = msg.Length + 1; - int rem = padded % 128; - padded += (rem <= 112 ? 112 - rem : 240 - rem) + 16; - - var buf = new byte[padded]; - Buffer.BlockCopy(msg, 0, buf, 0, msg.Length); - buf[msg.Length] = 0x80; - for (int i = 0; i < 8; i++) - buf[padded - 1 - i] = (byte)(bitLen >> (i * 8)); - - var W = new ulong[80]; - for (int blk = 0; blk < padded; blk += 128) - { - for (int t = 0; t < 16; t++) - { - int o = blk + t * 8; - W[t] = ((ulong)buf[o] << 56) | ((ulong)buf[o+1] << 48) | - ((ulong)buf[o+2] << 40) | ((ulong)buf[o+3] << 32) | - ((ulong)buf[o+4] << 24) | ((ulong)buf[o+5] << 16) | - ((ulong)buf[o+6] << 8) | (ulong)buf[o+7]; - } - for (int t = 16; t < 80; t++) - { - ulong s0 = RotR(W[t-15], 1) ^ RotR(W[t-15], 8) ^ (W[t-15] >> 7); - ulong s1 = RotR(W[t-2], 19) ^ RotR(W[t-2], 61) ^ (W[t-2] >> 6); - W[t] = W[t-16] + s0 + W[t-7] + s1; - } - - ulong a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; - - for (int t = 0; t < 80; t++) - { - ulong S1 = RotR(e, 14) ^ RotR(e, 18) ^ RotR(e, 41); - ulong ch = (e & f) ^ (~e & g); - ulong t1 = h + S1 + ch + K[t] + W[t]; - ulong S0 = RotR(a, 28) ^ RotR(a, 34) ^ RotR(a, 39); - ulong maj = (a & b) ^ (a & c) ^ (b & c); - ulong t2 = S0 + maj; - h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; - } - - h0 += a; h1 += b; h2 += c; h3 += d; h4 += e; h5 += f; h6 += g; h7 += h; - } - - var result = new byte[32]; - StoreBE(result, 0, h0); StoreBE(result, 8, h1); - StoreBE(result, 16, h2); StoreBE(result, 24, h3); - return result; - } - - static ulong RotR(ulong x, int n) => (x >> n) | (x << (64 - n)); - - static void StoreBE(byte[] b, int o, ulong v) - { - b[o] = (byte)(v >> 56); b[o+1] = (byte)(v >> 48); - b[o+2] = (byte)(v >> 40); b[o+3] = (byte)(v >> 32); - b[o+4] = (byte)(v >> 24); b[o+5] = (byte)(v >> 16); - b[o+6] = (byte)(v >> 8); b[o+7] = (byte)v; - } - } - -} \ No newline at end of file diff --git a/Core/XChainAddressDeriver.cs.meta b/Core/XChainAddressDeriver.cs.meta deleted file mode 100644 index 0662cb8..0000000 --- a/Core/XChainAddressDeriver.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3bec2fa8f724f4806991516c091a39ff \ No newline at end of file diff --git a/WebGL~/EMAIL_SETUP.md b/Documentation~/email-setup.md similarity index 99% rename from WebGL~/EMAIL_SETUP.md rename to Documentation~/email-setup.md index 24ea5fa..2e0f82e 100644 --- a/WebGL~/EMAIL_SETUP.md +++ b/Documentation~/email-setup.md @@ -58,7 +58,7 @@ floor require their own supported migration; enrollment never downgrades it. ## Provider UI and recovery The provider owns email, verification and recovery forms. Keep them visible and -interactive in document-root fullscreen as described in the [integration guide](README.md). +interactive in document-root fullscreen as described in the [integration guide](integration.md). Handle popup close/cancellation normally, and let the player return to the game. Do not create another wallet after an authentication failure. No database restore or Blockmaker password reset can recreate a missing wallet key. diff --git a/Documentation~/integration.md b/Documentation~/integration.md new file mode 100644 index 0000000..f163585 --- /dev/null +++ b/Documentation~/integration.md @@ -0,0 +1,107 @@ +# Integrating Blockmaker Wallets + +Install version 2 through Unity Package Manager as described in the [README](../README.md). +The package contains the complete current wallet runtime; no manually added CDN +script tags or separately copied vendor files are needed. Keep one wallet manager per page. + +## Custom game UI + +Create one `BlockmakerClient` and one `BlockmakerWalletPackageWebGL`, initialize +while loading, then open a presentation view on the player's Sign in click: + +```csharp +using Blockmaker; + +var client = new BlockmakerClient(apiOrigin, publicGameId); +var wallet = gameObject.AddComponent(); +wallet.Initialize(client, publicEmailClientIdOrEmpty, appName, result => { + signInButton.SetEnabled(result.Success); +}); + +// In the Sign in button callback. Keep this view alive until the result arrives. +view = new BlockmakerUnityWalletView(document.rootVisualElement, wallet, + new BlockmakerUnityWalletAppearance { AppName = appName, Font = gameFont }); +view.OpenAccount(result => { + bool ready = result.Success && wallet.HasAcknowledgedPlayerSession + && wallet.CanSignTransactions; + // Update the game UI, then dispose the view. +}); +``` + +These are integration fragments; the complete working component is +[`BlockmakerWalletDemo`](../Runtime/BlockmakerWalletDemo.cs). Only initialize once, +keep the receiving GameObject's name unique, and dispose the view at teardown. +`RuntimeSupported` lets the game show a friendly message in Editor/native builds. +A blank public email client ID selects `pera_lute`; a configured ID selects +`pera_lute_txnlab_web3auth`. The Unity presentation shows Pera and optional email; +`OpenAccount` is available for the broader account UI. The older +`web3auth_avm_email` authentication-only broker is separate and remains restricted. + +Use `HasAcknowledgedPlayerSession` and `CanSignTransactions` for readiness. +An address hint or JavaScript-only success is insufficient. `PLAYER_CANCELLED` +is informational. Use the package cancellation/logout methods, honour a required +reload after uncertain cleanup, and never start a hidden recovery flow mid-signing. +Blockmaker session tokens stay in memory, including across refresh acknowledgment. + +## Fullscreen and deployment + +The included template is installed through **Blockmaker → Install Fullscreen Web +Template**. This creates a project-owned template; updates never overwrite a +locally changed template. With a custom host, fullscreen +`document.documentElement` from a user gesture. Do not subsequently call +`unityInstance.SetFullscreen(1)`, which fullscreens only the canvas and can make +provider controls outside that subtree unusable. + +The package uses Unity's supported build callback to add its five browser files +under `StreamingAssets/Blockmaker`. Source assets stay in the package; no copies +are written into your Assets folder. The `.jslib` plugin is enabled for WebGL +only. Builds verify all nine canonical members first and reject conflicting old +StreamingAssets copies. Native builds are rejected because this integration is +WebGL-only. Build from an actual Unity project with this UPM package installed. + +Serve `.js` and `.mjs` as JavaScript, and preserve Unity's content/encoding headers. +Allow the Blockmaker API, Pera connections and the provider's Web3Auth/hCaptcha +frames, scripts and connections in your hosting policy. This non-threaded wallet +integration uses popup-compatible `Cross-Origin-Opener-Policy: +same-origin-allow-popups`; do not impose cross-origin isolation without separately +validating provider frames. Keep provider iframes where the provider mounts them. +The presentation releases Unity's keyboard capture for email inputs. A provider +popup may still change fullscreen according to browser rules. + +## Shared profiles and actions + +`GetGameProfile` reads the canonical shared username, NFD/display-name choice, +default avatar and NFT picture. Use supported profile update methods, distinguish +NFD names from registered usernames, preserve deliberate clears and never save a +placeholder while loading. Identity follows the connected owner wallet, +including rekeyed accounts, rather than the signing authority. Progress, loadouts +and statistics remain game-specific. See [email setup](email-setup.md) for wallet +identity across games. + +For economic operations, use the game's validated prepare → Unity review → +explicit Sign → exact one-time submission/recovery flow. Review the action, +assets, costs and fees before signing. `PrepareTransactionGroup` and +`SignPreparedTransactionGroup` preserve the exact prepared group; they do not +submit it. Preserve signer indexes, operation IDs and recovery records. Never +automatically re-sign/rebroadcast an ambiguous submission. + +Show the exact receiving address, balances and funding guidance. Authentication +adds no ALGO or other assets. Keep credentials, OTPs, private keys and recovery +material inside the provider adapter, outside C#, PlayerPrefs, API bodies and logs. + +## Package maintenance + +`Packages/packages-lock.json` pins the UPM revision. `package-manifest.json` pins +the same nine canonical runtime members used by the earlier complete package; +its original `installPath` fields describe the Assets-based distribution, while +the UPM build hook maps those members to package paths. Their bytes, content ID, +network and provider policy are unchanged. UPM installation does not emit a +separate game-bound Assets installation receipt or activate provider policy. +The old `sync-unity-webgl-sdk` command targets the Assets distribution and must +not be run over a UPM installation. Any operator tooling that assumes that old +layout needs a UPM-aware check; the menu **Verify Installed Package** verifies +this package locally without claiming provider or game qualification. + +Commit scene/template changes and Unity metadata. Use complete tagged releases, +not individual vendor-file patches. Required setup and attended acceptance remain +in [email setup](email-setup.md) and [validation](validation.md). diff --git a/Documentation~/upgrading.md b/Documentation~/upgrading.md new file mode 100644 index 0000000..2244702 --- /dev/null +++ b/Documentation~/upgrading.md @@ -0,0 +1,47 @@ +# Upgrading to version 2 + +Version 2 is now the repository's root Package Manager SDK. It replaces the old +`BlockmakerAuth`, Reown/Magic/server-OTP and native-wallet implementation with the +current WebGL Pera/Lute and optional TxnLab/MetaMask email package. This is a major +API change. Previous releases remain in Git tags for reference; they are not the +current installation route. + +## From the previous v1 UPM package + +1. Commit or back up your project. Preserve profile/player history and exact + pending-operation recovery records. +2. Migrate old `BlockmakerAuth`/`BlockmakerConfig`/identity and profile consumers to + the client, wallet facade and presentation shown in the [integration guide](integration.md). + Remove obsolete scene objects after wiring their replacements. Existing API + callers will need changes; the package does not rewrite gameplay code. +3. Install `https://github.com/blockmaker-ai/blockmaker-unity.git#v2.0.0` through + Package Manager. Keep one wallet manager. Old Reown/Nethereum dependencies can + be removed through Package Manager only if no other project code uses them. +4. Confirm the same owner wallet/profile, cancellation and pending-operation + recovery before resuming live game actions. + +## From the September Assets/ZIP package + +The nine wallet runtime files are unchanged. The new package manages their paths +and WebGL build inclusion. Close Unity and back up the complete existing SDK +files and their `.meta` files outside Assets before removing their old copies +from compilation. Preserve your own game UI, configuration and recovery data. + +Remove only the previous vendor SDK files and its lock/installation receipt, +including its `.jslib` and five `Assets/StreamingAssets/Blockmaker` files. Do not +delete the entire Blockmaker folder if it contains your own work. Install the +UPM package, reopen Unity and check scene references. The canonical runtime +script/plugin GUIDs are retained from the existing integration to support this +move. Assembly placement changes, so custom assembly definitions may need an +explicit reference to the `Blockmaker` assembly. + +The drop-in installer refuses to overwrite an SDK still in Assets. The build +hook also refuses old browser-file collisions. It never automatically deletes +files, migrates email wallets, merges profiles or clears recovery state. + +## Returning email players + +Changing email provider/project/login connection can change the wallet address. +Do not assume the same email restores a legacy address. Keep existing provider +identity pinned and use a reviewed proof-of-control recovery/linking arrangement +where needed. No account migration is performed by updating this package. diff --git a/Documentation~/validation.md b/Documentation~/validation.md new file mode 100644 index 0000000..13e4604 --- /dev/null +++ b/Documentation~/validation.md @@ -0,0 +1,56 @@ +# Validation — Unity Package Manager SDK 2.0.0 + +The package contains canonical wallet runtime +`unity-webgl-package-sha256-cicBnjQf1cPzH5Sz3Gt7wX8seHXG9Cj1VTf6eRIAFlQ`, from +runtime revision `226bd9136b268cd1c7005efdb3bccf23be50e607`. All nine runtime files +are unchanged from the package used by NFTURBO build `20260915-6f70eb687b39`. +Version 2.0.0 describes the public UPM distribution and setup tools; the canonical +runtime manifest retains backend release version `0.2.5`. + +## Checked for this distribution + +- The root package resolves in Unity 6000.3.15f1 using Unity Package Manager. + It compiles without Reown or separately installed wallet dependencies. +- The setup menu creates a scene with a UI document, panel, camera and wallet + demo. Repeating setup does not create a second demo. The `.jslib` plugin is + enabled for WebGL and disabled for Editor. +- A small actual WebGL build succeeded with zero errors. Its five browser files + match the canonical hashes, and no vendor copies were written into the + project's Assets/StreamingAssets folder. The full NFTURBO game was not rebuilt. +- A headless browser loaded the demo and all three executable wallet modules + from the built StreamingAssets with HTTP 200. The fullscreen template entered + document-root fullscreen; Pera's in-game QR and cancellation were checked. +- Five focused checks cover package/dependency layout, the full canonical + runtime hashes, Unity metadata/plugin settings, removal of the old active + runtime, documentation links and sample/template presence. + +The same unchanged core previously completed NFTURBO's desktop, ASTC and ETC2 +builds and focused authentication/session/recovery-boundary tests. Its email +form opening/focus, close/retry and fullscreen behavior were checked in that +integration. These earlier checks do not prove every game or device works. + +## Still requires attended player testing + +Actual email/OTP and Pera wallet approval, C# acknowledgment after a real login, +returning login/recovery and stable identity across two actual game origins +remain player acceptance checks. The packaging test did not enter an email or +create a wallet. Opening a provider form is not proof of a completed login. + +No live transaction was signed or submitted for this release. Shop delivery, +ASA opt-ins, Garage groups, Loadout ownership and Marketplace operations need +their own game-specific acceptance under existing backend policy. The sample +makes no economic requests. Preserve ambiguous-operation recovery and never +retry a submission automatically. + +Physical mobile devices, provider popups and the complete verification/recovery +screens still need checks. The package supports Unity WebGL only; native exports +and unrelated email-provider configurations have not been implemented here. + +## Recheck locally + +Use **Blockmaker → Verify Installed Package** in Unity to check the nine runtime +hashes. Build with the included template and inspect its StreamingAssets output. +For contributors, `node --test 'Tools~/package.test.mjs'` runs the lightweight +packaging checks; Node.js is not required to install or use the SDK in Unity. +Installation and file verification do not activate a provider, qualify a game or +prove completed wallet/economic flows. diff --git a/Editor/Blockmaker.Editor.asmdef b/Editor/Blockmaker.Editor.asmdef index 87c203d..a42d401 100644 --- a/Editor/Blockmaker.Editor.asmdef +++ b/Editor/Blockmaker.Editor.asmdef @@ -1,18 +1,11 @@ { - "name": "Blockmaker.Editor", - "rootNamespace": "", - "references": [ - "Blockmaker" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": false, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false + "name": "Blockmaker.Editor", + "rootNamespace": "Blockmaker.Editor", + "references": [ + "Blockmaker" + ], + "includePlatforms": [ + "Editor" + ], + "autoReferenced": true } diff --git a/Editor/BlockmakerBootSceneSetup.cs b/Editor/BlockmakerBootSceneSetup.cs deleted file mode 100644 index 70c4913..0000000 --- a/Editor/BlockmakerBootSceneSetup.cs +++ /dev/null @@ -1,155 +0,0 @@ -#if UNITY_EDITOR -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -namespace Blockmaker -{ - -/// -/// Editor utility: Blockmaker > Setup Scene -/// Creates all required GameObjects with components and assets pre-wired. -/// Run this once in any scene. It's idempotent — safe to run again. -/// -public static class BlockmakerBootSceneSetup -{ - private const string PackagePath = "Packages/com.blockmaker.sdk"; - - [MenuItem("Blockmaker/Setup Scene")] - public static void RunSetup() - { - // ── 1. Find or create BlockmakerConfig ───────────────────────────── - var config = FindOrCreateConfig(); - - // ── Fix default bundle ID (Reown relay rejects Unity template IDs) - var currentId = PlayerSettings.applicationIdentifier ?? ""; - if (string.IsNullOrEmpty(currentId) || currentId.Contains("unity.template") || currentId.Contains("Unity-Technologies")) - { - var newId = $"com.{PlayerSettings.companyName.ToLower().Replace(" ", "")}.{PlayerSettings.productName.ToLower().Replace(" ", "")}"; - if (string.IsNullOrEmpty(newId) || newId == "com.." || newId.Length < 8) - newId = "com.blockmaker.game"; - PlayerSettings.SetApplicationIdentifier(UnityEditor.Build.NamedBuildTarget.Standalone, newId); - PlayerSettings.SetApplicationIdentifier(UnityEditor.Build.NamedBuildTarget.Android, newId); - PlayerSettings.SetApplicationIdentifier(UnityEditor.Build.NamedBuildTarget.iOS, newId); - Debug.Log($"[Blockmaker] Set bundle ID to {newId}"); - } - - // ── 2. _Blockmaker GameObject ────────────────────────────────────── - var blockmakerGo = GameObject.Find("_Blockmaker"); - if (blockmakerGo == null) - blockmakerGo = new GameObject("_Blockmaker"); - - var auth = blockmakerGo.GetComponent(); - if (auth == null) - auth = blockmakerGo.AddComponent(); - - if (auth.blockmakerConfig == null && config != null) - auth.blockmakerConfig = config; - - // ── 3. _AuthUI GameObject ────────────────────────────────────────── - var authUiGo = GameObject.Find("_AuthUI"); - if (authUiGo == null) - authUiGo = new GameObject("_AuthUI"); - - // Panel Settings - var panelSettings = FindOrCreatePanelSettings(); - - // UIDocument - var uiDoc = authUiGo.GetComponent(); - if (uiDoc == null) - uiDoc = authUiGo.AddComponent(); - - uiDoc.sortingOrder = 100; - - if (uiDoc.panelSettings == null && panelSettings != null) - uiDoc.panelSettings = panelSettings; - - // Assign AuthPrompt UXML as the source asset (the compact modal) - var authPromptUxml = LoadAsset("UI/AuthPrompt.uxml"); - if (authPromptUxml != null && uiDoc.visualTreeAsset == null) - uiDoc.visualTreeAsset = authPromptUxml; - - // AuthPromptController component - var authPrompt = authUiGo.GetComponent(); - if (authPrompt == null) - authPrompt = authUiGo.AddComponent(); - - // Wire up the Pera/Defly/X-Chain modal UXML - if (authPrompt.peraConnectAsset == null) - authPrompt.peraConnectAsset = LoadAsset("UI/PeraConnectModal.uxml"); - - // ── 4. _ConnectButton (example UI) ────────────────────────────────── - var connectGo = GameObject.Find("_ConnectButton"); - if (connectGo == null) - { - connectGo = new GameObject("_ConnectButton"); - - var btnDoc = connectGo.AddComponent(); - btnDoc.sortingOrder = 200; - if (btnDoc.panelSettings == null && panelSettings != null) - btnDoc.panelSettings = panelSettings; - - connectGo.AddComponent(); - } - - // UI Toolkit handles its own input — no EventSystem needed. - - // ── Done ─────────────────────────────────────────────────────────── - UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty( - UnityEngine.SceneManagement.SceneManager.GetActiveScene()); - - Debug.Log("[Blockmaker] Scene setup complete. Save the scene and hit Play."); - EditorGUIUtility.PingObject(authUiGo); - Selection.activeGameObject = authUiGo; - } - - static BlockmakerConfig FindOrCreateConfig() - { - // Check if one already exists in the project - var guids = AssetDatabase.FindAssets("t:BlockmakerConfig"); - if (guids.Length > 0) - return AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0])); - - // Create one - var config = ScriptableObject.CreateInstance(); - string dir = "Assets"; - string path = $"{dir}/BlockmakerConfig.asset"; - AssetDatabase.CreateAsset(config, path); - AssetDatabase.SaveAssets(); - Debug.Log($"[Blockmaker] Created config at {path}"); - return config; - } - - static PanelSettings FindOrCreatePanelSettings() - { - // Check if one exists - var guids = AssetDatabase.FindAssets("t:PanelSettings"); - if (guids.Length > 0) - return AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0])); - - // Create one - var ps = ScriptableObject.CreateInstance(); - string path = "Assets/BlockmakerPanelSettings.asset"; - AssetDatabase.CreateAsset(ps, path); - AssetDatabase.SaveAssets(); - Debug.Log($"[Blockmaker] Created PanelSettings at {path}"); - return ps; - } - - static T LoadAsset(string relativePath) where T : Object - { - var asset = AssetDatabase.LoadAssetAtPath($"{PackagePath}/{relativePath}"); - if (asset == null) - Debug.LogWarning($"[Blockmaker] Could not find {relativePath} in package."); - return asset; - } - - [MenuItem("Blockmaker/Setup Scene", validate = true)] - public static bool RunSetupValidate() - { - return UnityEngine.SceneManagement.SceneManager.GetActiveScene().IsValid(); - } -} - -} // namespace Blockmaker -#endif diff --git a/Editor/BlockmakerBootSceneSetup.cs.meta b/Editor/BlockmakerBootSceneSetup.cs.meta deleted file mode 100644 index 720cf9e..0000000 --- a/Editor/BlockmakerBootSceneSetup.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 084ed6c3b92d1428ea9222d51056d0a8 \ No newline at end of file diff --git a/Editor/BlockmakerBuildKeyStripper.cs b/Editor/BlockmakerBuildKeyStripper.cs deleted file mode 100644 index ab5d337..0000000 --- a/Editor/BlockmakerBuildKeyStripper.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Collections.Generic; -using UnityEditor; -using UnityEditor.Build; -using UnityEditor.Build.Reporting; -using UnityEngine; - -namespace Blockmaker -{ - /// - /// Build safety: strips the developer API key out of every BlockmakerConfig - /// during a player build so it NEVER ships inside a distributed game, then - /// restores it afterwards so editor development keeps working. - /// - /// Keep your dev key in your local (gitignored) BlockmakerConfig for editor - /// testing; shipped builds authenticate via player JWTs and don't need it. - /// This makes it impossible to accidentally ship the key — no discipline - /// required. Editor-only. - /// - public class BlockmakerBuildKeyStripper : IPreprocessBuildWithReport, IPostprocessBuildWithReport - { - public int callbackOrder => 0; - - // Distinct from any game-local stripper so the two can coexist safely - // (each captures/restores only its own SessionState). - private const string StateKey = "blockmaker.sdk.strippedApiKeys"; - - [System.Serializable] private class Entry { public string path; public string key; } - [System.Serializable] private class Entries { public List items = new List(); } - - public void OnPreprocessBuild(BuildReport report) - { - var saved = new Entries(); - foreach (var guid in AssetDatabase.FindAssets("t:BlockmakerConfig")) - { - var path = AssetDatabase.GUIDToAssetPath(guid); - var cfg = AssetDatabase.LoadAssetAtPath(path); - if (cfg == null || string.IsNullOrEmpty(cfg.apiKey)) continue; - saved.items.Add(new Entry { path = path, key = cfg.apiKey }); - cfg.apiKey = ""; - EditorUtility.SetDirty(cfg); - } - - if (saved.items.Count > 0) - { - AssetDatabase.SaveAssets(); - SessionState.SetString(StateKey, JsonUtility.ToJson(saved)); - Debug.LogWarning($"[Blockmaker] Stripped the dev apiKey from {saved.items.Count} BlockmakerConfig(s) for this build — it will not ship. Restored automatically after the build."); - } - else - { - SessionState.EraseString(StateKey); - } - } - - public void OnPostprocessBuild(BuildReport report) => RestoreKeys(); - - // Safety net: if a build is cancelled/fails before OnPostprocessBuild runs, - // restore on the next domain reload — but never mid-build (that would - // re-bake the key into the player). - [InitializeOnLoadMethod] private static void RestoreOnLoad() - { - if (!BuildPipeline.isBuildingPlayer) RestoreKeys(); - } - - private static void RestoreKeys() - { - var json = SessionState.GetString(StateKey, ""); - if (string.IsNullOrEmpty(json)) return; - - var saved = JsonUtility.FromJson(json); - if (saved?.items != null) - { - foreach (var e in saved.items) - { - var cfg = AssetDatabase.LoadAssetAtPath(e.path); - if (cfg != null && string.IsNullOrEmpty(cfg.apiKey)) - { - cfg.apiKey = e.key; - EditorUtility.SetDirty(cfg); - } - } - AssetDatabase.SaveAssets(); - } - SessionState.EraseString(StateKey); - } - } -} diff --git a/Editor/BlockmakerBuildKeyStripper.cs.meta b/Editor/BlockmakerBuildKeyStripper.cs.meta deleted file mode 100644 index 8c963c9..0000000 --- a/Editor/BlockmakerBuildKeyStripper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6cc4d16164444181af1863e2c97f0431 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Editor/BlockmakerSetup.cs b/Editor/BlockmakerSetup.cs new file mode 100644 index 0000000..ba04383 --- /dev/null +++ b/Editor/BlockmakerSetup.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Blockmaker.Editor +{ + public static class BlockmakerSetup + { + [MenuItem("Blockmaker/Setup Wallet Demo")] + public static void SetupWalletDemo() + { + if (UnityEngine.Object.FindFirstObjectByType() != null) { + Debug.Log("A Blockmaker Wallet Demo is already in this scene. Configure its public Game ID in the Inspector."); return; + } + if (UnityEngine.Object.FindFirstObjectByType() != null) + throw new InvalidOperationException("This scene already contains a wallet receiver. Use that integration instead of adding a second manager."); + var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(BlockmakerSetup).Assembly); + var panel = ScriptableObject.CreateInstance(); + panel.name = "Blockmaker Wallet Panel"; + panel.scaleMode = PanelScaleMode.ScaleWithScreenSize; + panel.referenceResolution = new Vector2Int(1280, 720); + panel.themeStyleSheet = AssetDatabase.LoadAssetAtPath(package.assetPath + "/Runtime/WalletDemo.tss"); + if (panel.themeStyleSheet == null) throw new InvalidOperationException("The wallet demo theme could not be loaded. Reimport the package."); + string asset = AssetDatabase.GenerateUniqueAssetPath("Assets/BlockmakerWalletPanel.asset"); + AssetDatabase.CreateAsset(panel, asset); + var go = new GameObject("Blockmaker Wallet Demo"); + Undo.RegisterCreatedObjectUndo(go, "Create Blockmaker Wallet Demo"); + var document = go.AddComponent(); document.panelSettings = panel; + var demo = go.AddComponent(); demo.Document = document; + if (Camera.main == null) { + var camera = new GameObject("Wallet Demo Camera"); + Undo.RegisterCreatedObjectUndo(camera, "Create Wallet Demo Camera"); + camera.tag = "MainCamera"; + var component = camera.AddComponent(); component.clearFlags = CameraClearFlags.SolidColor; + component.backgroundColor = new Color(0.08f, 0.11f, 0.17f); + } + Selection.activeGameObject = go; + EditorSceneManager.MarkSceneDirty(go.scene); + AssetDatabase.SaveAssets(); + Debug.Log("Wallet demo created. Set Game ID, optional public email client ID and branding in the Inspector. Use Blockmaker > Install Fullscreen Web Template before a browser test."); + } + + [MenuItem("Blockmaker/Install Fullscreen Web Template")] + public static void InstallFullscreenWebTemplate() + { + string source = Path.Combine(BlockmakerWebGLBuild.PackageRoot, "Templates~/Blockmaker/index.html"); + string directory = "Assets/WebGLTemplates/Blockmaker"; + string target = directory + "/index.html"; + if (File.Exists(target) && File.ReadAllText(target) != File.ReadAllText(source)) + throw new InvalidOperationException("The Blockmaker WebGL template has local changes. Preserve it and merge the new template manually; nothing was overwritten."); + Directory.CreateDirectory(directory); + if (!File.Exists(target)) File.Copy(source, target); + AssetDatabase.Refresh(); + PlayerSettings.WebGL.template = "PROJECT:Blockmaker"; + Debug.Log("Blockmaker fullscreen WebGL template selected. It fullscreens the document root so provider forms can receive input."); + } + } +} diff --git a/Editor/BlockmakerSetup.cs.meta b/Editor/BlockmakerSetup.cs.meta new file mode 100644 index 0000000..8944aba --- /dev/null +++ b/Editor/BlockmakerSetup.cs.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: 90d6bf6e9af247fba7817236b08f26f2 +MonoImporter: + externalObjects: {} diff --git a/Editor/BlockmakerWebGLBuild.cs b/Editor/BlockmakerWebGLBuild.cs new file mode 100644 index 0000000..3a040be --- /dev/null +++ b/Editor/BlockmakerWebGLBuild.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.PackageManager; +using UnityEngine; + +namespace Blockmaker.Editor +{ + /// Adds the package's exact browser payload without modifying Assets. + public sealed class BlockmakerWebGLBuild : BuildPlayerProcessor + { + public const string CanonicalPackageId = "unity-webgl-package-sha256-cicBnjQf1cPzH5Sz3Gt7wX8seHXG9Cj1VTf6eRIAFlQ"; + public override int callbackOrder => 0; + private static readonly Dictionary Paths = new Dictionary { + { "BlockmakerClient.cs", "Runtime/BlockmakerClient.cs" }, + { "BlockmakerWalletPackageWebGL.cs", "Runtime/BlockmakerWalletPackageWebGL.cs" }, + { "BlockmakerUnityPresentation.cs", "Runtime/BlockmakerUnityPresentation.cs" }, + { "BlockmakerWalletPackageWebGL.jslib", "Plugins/WebGL/BlockmakerWalletPackageWebGL.jslib" }, + { "blockmaker.js", "Browser~/blockmaker.js" }, + { "blockmaker-unity-webgl-wallet-host.mjs", "Browser~/blockmaker-unity-webgl-wallet-host.mjs" }, + { "blockmaker-txnlab-wallet.mjs", "Browser~/blockmaker-txnlab-wallet.mjs" }, + { "blockmaker-txnlab-wallet.d.mts", "Browser~/blockmaker-txnlab-wallet.d.mts" }, + { "blockmaker-txnlab-wallet.NOTICES.txt", "Browser~/blockmaker-txnlab-wallet.NOTICES.txt" }, + }; + [Serializable] private sealed class Manifest { public string packageId; public Member[] members; } + [Serializable] private sealed class Member { public string file, integrity; public int rawBytes; } + + public static string PackageRoot { + get { + var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(BlockmakerWebGLBuild).Assembly); + if (package == null) throw new BuildFailedException("Install Blockmaker with Unity Package Manager before building."); + return package.resolvedPath; + } + } + + public static void VerifyPackage(string root) + { + var manifest = JsonUtility.FromJson(File.ReadAllText(Path.Combine(root, "package-manifest.json"))); + if (manifest == null || manifest.packageId != CanonicalPackageId || manifest.members == null || manifest.members.Length != Paths.Count) + throw new BuildFailedException("Blockmaker's package is incomplete. Reinstall the pinned package version."); + var seen = new HashSet(); + foreach (var member in manifest.members) { + if (member == null || string.IsNullOrEmpty(member.file) || !Paths.TryGetValue(member.file, out var path) || !seen.Add(member.file)) + throw new BuildFailedException("Blockmaker's package member list is invalid."); + var bytes = File.ReadAllBytes(Path.Combine(root, path)); + string integrity; + using (var hash = SHA256.Create()) integrity = "sha256-" + Convert.ToBase64String(hash.ComputeHash(bytes)); + if (bytes.Length != member.rawBytes || integrity != member.integrity) + throw new BuildFailedException("Blockmaker package verification failed for " + member.file + ". Reinstall the complete package; do not patch individual vendor files."); + } + } + + public override void PrepareForBuild(BuildPlayerContext context) + { + if (context.BuildPlayerOptions.target != BuildTarget.WebGL) + throw new BuildFailedException("Blockmaker Wallets 2 supports Unity WebGL only. Choose Web in Build Profiles. Native wallet exports need a separate integration."); + string root = PackageRoot; + VerifyPackage(root); + foreach (var pair in Paths) { + if (!pair.Value.StartsWith("Browser~/", StringComparison.Ordinal)) continue; + string destination = "Blockmaker/" + pair.Key; + if (File.Exists(Path.Combine(Application.streamingAssetsPath, destination))) + throw new BuildFailedException("A previous Blockmaker browser file remains in Assets/StreamingAssets: " + pair.Key + ". Follow the migration guide; the package will not overwrite it."); + context.AddAdditionalPathToStreamingAssets(Path.Combine(root, pair.Value), destination); + } + } + + [MenuItem("Blockmaker/Verify Installed Package")] + public static void VerifyInstalledPackage() + { + VerifyPackage(PackageRoot); + Debug.Log("Blockmaker Wallets: all nine canonical package files verified."); + } + } +} diff --git a/Editor/BlockmakerWebGLBuild.cs.meta b/Editor/BlockmakerWebGLBuild.cs.meta new file mode 100644 index 0000000..72b2475 --- /dev/null +++ b/Editor/BlockmakerWebGLBuild.cs.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: 635418cef2da4696925f40ed6335eb00 +MonoImporter: + externalObjects: {} diff --git a/Identity/EmailIdentity.cs b/Identity/EmailIdentity.cs deleted file mode 100644 index 3c2965f..0000000 --- a/Identity/EmailIdentity.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Tier 2 identity. Player authenticated with email + OTP. - /// Blockmaker server holds an encrypted Algorand wallet on their behalf. - /// Transaction signing is transparent — the server signs, no wallet app needed. - /// - public class EmailIdentity : ServerSignedIdentity - { - private static string SessionKey => BlockmakerPrefs.Key("email_session"); - - public override string Address { get; } - public override string DisplayName { get; } - public override IdentityTier Tier => IdentityTier.Email; - public override string ProviderName => "Email"; - public override bool HasWallet => true; - public override bool CanSign => true; - - public string Email { get; } - public override string SessionToken { get; protected set; } - public override string RefreshToken { get; protected set; } - - public EmailIdentity(string email, string walletAddress, string sessionToken, string refreshToken = null) - { - if (string.IsNullOrEmpty(email)) - throw new ArgumentException("Email is required.", nameof(email)); - if (!email.Contains('@')) - throw new ArgumentException("Invalid email format", nameof(email)); - if (string.IsNullOrEmpty(walletAddress)) - throw new ArgumentException("Wallet address is required.", nameof(walletAddress)); - - Email = email; - Address = walletAddress; - SessionToken = sessionToken; - RefreshToken = refreshToken; - DisplayName = email.Split('@')[0]; - } - - public override void SaveSession() - { - var data = new EmailSessionData - { - email = Email, - walletAddress = Address, - sessionToken = SessionToken, - refreshToken = RefreshToken - }; - SecurePrefs.SetString(SessionKey, JsonUtility.ToJson(data)); - SecurePrefs.Save(); - BlockmakerLog.Info($"[EmailIdentity] Session saved for {Email}"); - } - - public override void ClearSession() - { - SecurePrefs.DeleteKey(SessionKey); - SecurePrefs.Save(); - BlockmakerLog.Info("[EmailIdentity] Session cleared."); - } - - public static EmailIdentity TryLoadSession() - { - string json = SecurePrefs.GetString(SessionKey, string.Empty); - if (string.IsNullOrEmpty(json)) return null; - - try - { - var data = JsonUtility.FromJson(json); - - if (string.IsNullOrEmpty(data.walletAddress)) - return null; - if (string.IsNullOrEmpty(data.sessionToken) && string.IsNullOrEmpty(data.refreshToken)) - return null; - - BlockmakerLog.Info($"[EmailIdentity] Restored session for {data.email}"); - return new EmailIdentity(data.email, data.walletAddress, data.sessionToken, data.refreshToken); - } - catch (Exception e) - { - BlockmakerLog.Warning($"[EmailIdentity] Corrupt session data, clearing: {e.Message}"); - SecurePrefs.DeleteKey(SessionKey); - SecurePrefs.Save(); - return null; - } - } - - [Serializable] - private class EmailSessionData - { - public string email; - public string walletAddress; - public string sessionToken; - public string refreshToken; - } - } - -} \ No newline at end of file diff --git a/Identity/EmailIdentity.cs.meta b/Identity/EmailIdentity.cs.meta deleted file mode 100644 index 9bb7982..0000000 --- a/Identity/EmailIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 276ee79fcc1b94196a2883930995ee8a \ No newline at end of file diff --git a/Identity/EvmXChainIdentity.cs b/Identity/EvmXChainIdentity.cs deleted file mode 100644 index f0f17b7..0000000 --- a/Identity/EvmXChainIdentity.cs +++ /dev/null @@ -1,682 +0,0 @@ -using System; -using System.Collections; -using UnityEngine; - -namespace Blockmaker -{ - - public class EvmXChainIdentity : IBlockmakerIdentity - { - private static string SessionKey => BlockmakerPrefs.Key("evm_xchain_session"); - - public string Address { get; } - public string DisplayName { get; } - public IdentityTier Tier => IdentityTier.SelfCustody; - public string ProviderName => "EvmXChain"; - public bool HasWallet => true; - - public bool CanSign => - #if UNITY_WEBGL && !UNITY_EDITOR - true; - #else - ReownWalletConnector.Instance != null && ReownWalletConnector.Instance.IsConnected; - #endif - - public bool SupportsAtomicGroupSign => true; - - public string EvmAddress { get; } - - /// Player JWT obtained via wallet-signature login (see ). - public string SessionToken { get; private set; } - /// Long-lived refresh token paired with . - public string RefreshToken { get; private set; } - - internal void UpdateTokens(string sessionToken, string refreshToken) - { - if (!string.IsNullOrEmpty(sessionToken)) SessionToken = sessionToken; - if (!string.IsNullOrEmpty(refreshToken)) RefreshToken = refreshToken; - } - - /// - /// Clear only the in-memory JWT/refresh tokens, leaving the wallet identity and the - /// live WalletConnect relay/session intact. Used when a token refresh fails but the - /// wallet can still re-sign for a fresh JWT (see BlockmakerAuth.VerifyRestoredSession). - /// - internal void ClearTokens() - { - SessionToken = null; - RefreshToken = null; - } - - public EvmXChainIdentity(string algorandAddress, string evmAddress) - { - if (string.IsNullOrEmpty(algorandAddress)) - throw new ArgumentException("Invalid Algorand address", nameof(algorandAddress)); - if (string.IsNullOrEmpty(evmAddress)) - throw new ArgumentException("Invalid EVM address", nameof(evmAddress)); - - Address = algorandAddress; - EvmAddress = evmAddress; - DisplayName = evmAddress.Length >= 10 - ? $"ETH · {evmAddress[..6]}...{evmAddress[^4..]}" - : $"ETH · {evmAddress}"; - } - - public IEnumerator SignTransaction( - string unsignedTxnBase64, - Action onSigned, - Action onError) - { - if (string.IsNullOrEmpty(unsignedTxnBase64)) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - string result = null; - string error = null; - - #if UNITY_WEBGL && !UNITY_EDITOR - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - // Zero-bundle WebGL path — same shape as the native Reown branch below: - // C# builds the EIP-712 payload over the txn's raw TxID, the browser - // wallet signs it via eth_signTypedData_v4 (thin jslib transport), and - // C# parses the signature and assembles the LogicSig-signed bytes. - byte[] unsignedBytes = null; - byte[] program = null; - string typedData = null; - try - { - unsignedBytes = Convert.FromBase64String(unsignedTxnBase64); - var txnId = XChainAddressDeriver.ComputeTransactionId(unsignedBytes); - typedData = XChainAddressDeriver.BuildEip712TypedData(txnId); - program = XChainAddressDeriver.GetLogicSigProgram(EvmAddress); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Sign setup error: {ex.Message}"); - onError?.Invoke("Something went wrong while preparing the transaction. Please try again."); - yield break; - } - - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - BlockmakerWalletBridge.EvmSignTypedData( - EvmAddress, - typedData, - BlockmakerAuth.Instance.gameObject.name, - nameof(BlockmakerAuth.Instance.OnTxnSignedFromJS), - nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - - float elapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null && - elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (BlockmakerAuth.Instance != null && !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - var hexSig = BlockmakerAuth.Instance.ConsumePendingSignedTxn(); - error = BlockmakerAuth.Instance.ConsumePendingSignError(); - - if (hexSig != null) - { - try - { - var sigArg = XChainAddressDeriver.ParseEvmSignature(hexSig); - var signed = XChainAddressDeriver.BuildSignedTransaction(unsignedBytes, program, sigArg); - result = Convert.ToBase64String(signed); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Build signed txn error: {ex.Message}"); - error = "Something went wrong while completing the signature. Please try again."; - } - } - #else - var connector = ReownWalletConnector.Instance; - if (connector == null || !connector.IsConnected) - { - onError?.Invoke("Your wallet session has ended. Please connect your wallet again to continue."); - yield break; - } - - var unsignedBytes = Convert.FromBase64String(unsignedTxnBase64); - var txnId = XChainAddressDeriver.ComputeTransactionId(unsignedBytes); - var typedData = XChainAddressDeriver.BuildEip712TypedData(txnId); - var program = XChainAddressDeriver.GetLogicSigProgram(EvmAddress); - - bool signDone = false; - - connector.SignEvmTypedData( - EvmAddress, - typedData, - onSigned: hexSig => - { - try - { - var sigArg = XChainAddressDeriver.ParseEvmSignature(hexSig); - var signed = XChainAddressDeriver.BuildSignedTransaction(unsignedBytes, program, sigArg); - result = Convert.ToBase64String(signed); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Build signed txn error: {ex.Message}"); - error = "Something went wrong while completing the signature. Please try again."; - } - signDone = true; - }, - onError: err => - { - error = err; - signDone = true; - } - ); - - float elapsed = 0f; - while (!signDone && elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!signDone) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - #endif - - if (result != null) onSigned?.Invoke(result); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - } - - public IEnumerator SignTransactions( - string[] unsignedTxnsBase64, - Action onSigned, - Action onError) - { - if (unsignedTxnsBase64 == null || unsignedTxnsBase64.Length == 0) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - // Single transaction — delegate to SignTransaction - if (unsignedTxnsBase64.Length == 1) - { - string signed = null; - string err = null; - yield return SignTransaction(unsignedTxnsBase64[0], s => { signed = s; }, e => { err = e; }); - if (err != null) { onError?.Invoke(err); yield break; } - onSigned?.Invoke(new[] { signed }); - yield break; - } - -#if UNITY_WEBGL && !UNITY_EDITOR - // Zero-bundle WebGL group signing — mirrors the native Reown branch below: - // ONE eth_signTypedData_v4 signature over the 32-byte GROUP ID, then the - // SAME parsed signature arg + LogicSig program attach to EVERY txn in the - // group. (Proven against the on-chain LogicSig via mainnet simulate.) - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - // Extract GroupID (non-yielding setup — catch sets error flag instead of yielding) - byte[] groupId = null; - string typedData = null; - byte[] program = null; - string setupError = null; - try - { - var firstTxnBytes = Convert.FromBase64String(unsignedTxnsBase64[0]); - groupId = XChainAddressDeriver.ExtractGroupId(firstTxnBytes); - - if (groupId != null) - { - typedData = XChainAddressDeriver.BuildEip712TypedData(groupId); - program = XChainAddressDeriver.GetLogicSigProgram(EvmAddress); - } - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Group sign setup error: {ex.Message}"); - setupError = "Something went wrong while signing the transactions. Please try again."; - } - - if (setupError != null) - { - onError?.Invoke(setupError); - yield break; - } - - if (groupId == null) - { - // No group field — sign individually (not an atomic group) - var results = new string[unsignedTxnsBase64.Length]; - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - string signed = null; - string err = null; - yield return SignTransaction(unsignedTxnsBase64[i], s => { signed = s; }, e => { err = e; }); - if (err != null) { onError?.Invoke(err); yield break; } - results[i] = signed; - } - onSigned?.Invoke(results); - yield break; - } - - // Sign the GroupID with the browser EVM wallet (yields outside try-catch) - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - BlockmakerWalletBridge.EvmSignTypedData( - EvmAddress, - typedData, - BlockmakerAuth.Instance.gameObject.name, - nameof(BlockmakerAuth.Instance.OnTxnSignedFromJS), - nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - - float elapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null && - elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (BlockmakerAuth.Instance == null || - !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - var hexSig = BlockmakerAuth.Instance.ConsumePendingSignedTxn(); - var signError = BlockmakerAuth.Instance.ConsumePendingSignError(); - - if (signError != null) - { - onError?.Invoke(signError); - yield break; - } - if (string.IsNullOrEmpty(hexSig)) - { - onError?.Invoke("The transaction was not approved in your wallet. Please try again."); - yield break; - } - - // Build all signed transactions with the SAME signature arg (non-yielding) - try - { - var sigArg = XChainAddressDeriver.ParseEvmSignature(hexSig); - var signedResults = new string[unsignedTxnsBase64.Length]; - - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - var txnBytes = Convert.FromBase64String(unsignedTxnsBase64[i]); - var signedBytes = XChainAddressDeriver.BuildSignedTransaction(txnBytes, program, sigArg); - signedResults[i] = Convert.ToBase64String(signedBytes); - } - - onSigned?.Invoke(signedResults); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Group sign error: {ex.Message}"); - onError?.Invoke("Something went wrong while signing the transactions. Please try again."); - } -#else - // Native: atomic group signing via GroupID - var connector = ReownWalletConnector.Instance; - if (connector == null || !connector.IsConnected) - { - onError?.Invoke("Your wallet session has ended. Please connect your wallet again to continue."); - yield break; - } - - // Extract GroupID (non-yielding setup — catch sets error flag instead of yielding) - byte[] groupId = null; - string typedData = null; - byte[] program = null; - string setupError = null; - try - { - var firstTxnBytes = Convert.FromBase64String(unsignedTxnsBase64[0]); - groupId = XChainAddressDeriver.ExtractGroupId(firstTxnBytes); - - if (groupId != null) - { - typedData = XChainAddressDeriver.BuildEip712TypedData(groupId); - program = XChainAddressDeriver.GetLogicSigProgram(EvmAddress); - } - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Group sign setup error: {ex.Message}"); - setupError = "Something went wrong while signing the transactions. Please try again."; - } - - if (setupError != null) - { - onError?.Invoke(setupError); - yield break; - } - - if (groupId == null) - { - // No group field — sign individually (not an atomic group) - var results = new string[unsignedTxnsBase64.Length]; - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - string signed = null; - string err = null; - yield return SignTransaction(unsignedTxnsBase64[i], s => { signed = s; }, e => { err = e; }); - if (err != null) { onError?.Invoke(err); yield break; } - results[i] = signed; - } - onSigned?.Invoke(results); - yield break; - } - - // Sign GroupID with EVM wallet (yield statements outside try-catch) - string hexSig = null; - string signError = null; - bool signDone = false; - - connector.SignEvmTypedData( - EvmAddress, - typedData, - onSigned: sig => { hexSig = sig; signDone = true; }, - onError: err => { signError = err; signDone = true; } - ); - - float elapsed = 0f; - while (!signDone && elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!signDone) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (signError != null) - { - onError?.Invoke(signError); - yield break; - } - - // Build all signed transactions (non-yielding, in try-catch) - try - { - var sigArg = XChainAddressDeriver.ParseEvmSignature(hexSig); - var signedResults = new string[unsignedTxnsBase64.Length]; - - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - var txnBytes = Convert.FromBase64String(unsignedTxnsBase64[i]); - var signedBytes = XChainAddressDeriver.BuildSignedTransaction(txnBytes, program, sigArg); - signedResults[i] = Convert.ToBase64String(signedBytes); - } - - onSigned?.Invoke(signedResults); - } - catch (Exception ex) - { - BlockmakerLog.Error($"[EvmXChainIdentity] Group sign error: {ex.Message}"); - onError?.Invoke("Something went wrong while signing the transactions. Please try again."); - } -#endif - } - - public void SaveSession() - { - var data = new EvmSessionData - { - algorandAddress = Address, - evmAddress = EvmAddress, - sessionToken = SessionToken, - refreshToken = RefreshToken - }; - SecurePrefs.SetString(SessionKey, JsonUtility.ToJson(data)); - SecurePrefs.Save(); - BlockmakerLog.Info($"[EvmXChainIdentity] Session saved: {EvmAddress} → {Address}"); - } - - public void ClearSession() - { - SecurePrefs.DeleteKey(SessionKey); - SecurePrefs.Save(); - - var connector = ReownWalletConnector.Instance; - if (connector != null && - (string.Equals(connector.ConnectedAddress, EvmAddress, StringComparison.OrdinalIgnoreCase) || - string.Equals(connector.ConnectedAddress, Address, StringComparison.OrdinalIgnoreCase))) - { - connector.Disconnect(); - } - - #if UNITY_WEBGL && !UNITY_EDITOR - BlockmakerWalletBridge.EvmDisconnect(); - #endif - BlockmakerLog.Info("[EvmXChainIdentity] Session cleared."); - } - - public static EvmSessionData TryLoadSessionData() - { - string json = SecurePrefs.GetString(SessionKey, string.Empty); - if (string.IsNullOrEmpty(json)) return null; - - try - { - var data = JsonUtility.FromJson(json); - if (string.IsNullOrEmpty(data.algorandAddress) || - string.IsNullOrEmpty(data.evmAddress)) - return null; - return data; - } - catch (Exception e) - { - BlockmakerLog.Warning($"[EvmXChainIdentity] Corrupt session data, clearing: {e.Message}"); - SecurePrefs.DeleteKey(SessionKey); - SecurePrefs.Save(); - return null; - } - } - - [Serializable] - public class EvmSessionData - { - public string algorandAddress; - public string evmAddress; - public string sessionToken; - public string refreshToken; - } - - // ── Wallet-signature login (challenge → personal_sign → verify → store JWT) ─ - - /// - /// Acquire a player session token by signing a server challenge with the - /// connected EVM wallet (personal_sign). The signature is verified against - /// , but the account/JWT address is the derived - /// Algorand (per the wallet-auth contract). - /// - public IEnumerator Login(Action onSuccess = null, Action onError = null) - { - var client = BlockmakerClient.Instance; - if (client == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - // 1) Challenge — walletAddress is the DERIVED Algorand address; evmAddress is the signer. - WalletChallengeResult challenge = null; - string challengeError = null; - bool challengeDone = false; - client.StartCoroutine(client.RequestWalletChallenge( - Address, "evm", EvmAddress, - r => { challenge = r; challengeDone = true; }, - e => { challengeError = e; challengeDone = true; })); - - float elapsed = 0f; - while (!challengeDone && elapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!challengeDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - if (challenge == null || !challenge.success || string.IsNullOrEmpty(challenge.message)) - { - onError?.Invoke(challengeError ?? "Could not start wallet sign-in. Please try again."); - yield break; - } - - // 2) Sign with personal_sign over the EXACT message - string signatureHex = null; - string signError = null; - bool signDone = false; - - #if UNITY_WEBGL && !UNITY_EDITOR - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - BlockmakerWalletBridge.EvmSignPersonal( - challenge.message, - EvmAddress, - BlockmakerAuth.Instance.gameObject.name, - nameof(BlockmakerAuth.Instance.OnTxnSignedFromJS), - nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS)); - - float jsElapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null && - jsElapsed < BlockmakerAuth.WalletSignTimeout) - { - jsElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (BlockmakerAuth.Instance == null || - !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - if (BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - signatureHex = BlockmakerAuth.Instance.ConsumePendingSignedTxn(); - signError = BlockmakerAuth.Instance.ConsumePendingSignError(); - #else - var connector = ReownWalletConnector.Instance; - if (connector == null || !connector.IsConnected) - { - onError?.Invoke("Your wallet session has ended. Please connect your wallet again to continue."); - yield break; - } - - connector.SignEvmPersonalMessage( - EvmAddress, challenge.message, - onSignedHex: sig => { signatureHex = sig; signDone = true; }, - onError: err => { signError = err; signDone = true; }); - - float sElapsed = 0f; - while (!signDone && sElapsed < BlockmakerAuth.WalletSignTimeout) - { - sElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!signDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - #endif - - if (!string.IsNullOrEmpty(signError)) { onError?.Invoke(signError); yield break; } - if (string.IsNullOrEmpty(signatureHex)) - { - onError?.Invoke("The sign-in request was not approved in your wallet. Please try again."); - yield break; - } - - // 3) Verify → mint JWT (walletAddress = derived Algorand address; evmAddress = signer) - EmailVerifyResult verify = null; - string verifyError = null; - bool verifyDone = false; - client.StartCoroutine(client.VerifyWalletSignature( - Address, "evm", signatureHex, null, challenge.nonce, EvmAddress, - r => { verify = r; verifyDone = true; }, - e => { verifyError = e; verifyDone = true; })); - - float vElapsed = 0f; - while (!verifyDone && vElapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - vElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!verifyDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - if (verify == null || !verify.success || string.IsNullOrEmpty(verify.sessionToken)) - { - onError?.Invoke(verifyError ?? "Wallet sign-in failed. Please try again."); - yield break; - } - - // 4) Store - UpdateTokens(verify.sessionToken, verify.refreshToken); - SaveSession(); - BlockmakerLog.Info("[EvmXChainIdentity] Wallet sign-in complete — session token acquired."); - onSuccess?.Invoke(); - } - } - -} \ No newline at end of file diff --git a/Identity/EvmXChainIdentity.cs.meta b/Identity/EvmXChainIdentity.cs.meta deleted file mode 100644 index 028b8d2..0000000 --- a/Identity/EvmXChainIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3b7f60a6b8e2d4fbab1d57f4a2f08bf8 \ No newline at end of file diff --git a/Identity/GuestIdentity.cs b/Identity/GuestIdentity.cs deleted file mode 100644 index 234872d..0000000 --- a/Identity/GuestIdentity.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Tier 1 identity. No wallet, no email. Uses a stable device ID so a guest's - /// local progress (race points, settings) persists across sessions on the same - /// device. Cannot sign transactions. - /// - /// When the game wants to prompt an upgrade: - /// if (identity.Tier == IdentityTier.Guest) ShowUpgradeUI(); - /// - public class GuestIdentity : IBlockmakerIdentity - { - private static string DeviceIdKey => BlockmakerPrefs.Key("guest_device_id"); - - // ── IBlockmakerIdentity ──────────────────────────────────────────────────── - - public string Address { get; } - public string DisplayName { get; } - public IdentityTier Tier => IdentityTier.Guest; - public string ProviderName => "Guest"; - public bool HasWallet => false; - public bool CanSign => false; - - // ── Constructor ──────────────────────────────────────────────────────────── - - public GuestIdentity() - { - Address = LoadOrCreateDeviceId(); - DisplayName = Address.Length >= 6 ? $"Guest_{Address[..6]}" : $"Guest_{Address}"; - } - - // ── Signing ──────────────────────────────────────────────────────────────── - - public IEnumerator SignTransaction( - string unsignedTxnBase64, - Action onSigned, - Action onError) - { - onError?.Invoke("Guest accounts cannot sign transactions. Please connect a wallet or sign in with email."); - yield break; - } - - public IEnumerator SignTransactions( - string[] unsignedTxnsBase64, - Action onSigned, - Action onError) - { - onError?.Invoke("Guest accounts cannot sign transactions. Please connect a wallet or sign in with email."); - yield break; - } - - // ── Session ──────────────────────────────────────────────────────────────── - - public void SaveSession() - { - // Device ID is already persisted in PlayerPrefs — nothing extra to save - } - - public void ClearSession() - { - // Guests don't have a session to clear, but we keep the device ID - // so local progress (offline race points etc.) isn't lost on "logout" - } - - // ── Helpers ──────────────────────────────────────────────────────────────── - - private static string LoadOrCreateDeviceId() - { - string id = PlayerPrefs.GetString(DeviceIdKey, string.Empty); - - if (string.IsNullOrEmpty(id)) - { - id = Guid.NewGuid().ToString("N"); - PlayerPrefs.SetString(DeviceIdKey, id); - PlayerPrefs.Save(); - BlockmakerLog.Info($"[GuestIdentity] Created new device ID: {id[..8]}…"); - } - - return id; - } - } -} \ No newline at end of file diff --git a/Identity/GuestIdentity.cs.meta b/Identity/GuestIdentity.cs.meta deleted file mode 100644 index 8c0ce8f..0000000 --- a/Identity/GuestIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 48118ea8333014c709d897539e6de7f7 \ No newline at end of file diff --git a/Identity/MagicIdentity.cs b/Identity/MagicIdentity.cs deleted file mode 100644 index 76e97eb..0000000 --- a/Identity/MagicIdentity.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Collections; -using System.Text.RegularExpressions; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Email identity backed by Magic SDK. The player's private key lives on - /// their device (inside Magic's encrypted iframe sandbox) — never on the server. - /// - /// Signing is client-side via the Magic JS bridge, unlike the legacy - /// EmailIdentity which delegates to the Blockmaker server. - /// - /// Session flow: - /// 1. Player enters email → Magic handles OTP verification in its own UI - /// 2. Magic creates or recovers the Algorand wallet on the device - /// 3. We send Magic's DID token to the Blockmaker server to get a JWT - /// 4. JWT stored in SecurePrefs for future API calls - /// - public class MagicIdentity : IBlockmakerIdentity - { - private static readonly Regex Base64Regex = new Regex(@"^[A-Za-z0-9+/]*=*$", RegexOptions.Compiled); - - private static bool IsValidBase64(string value) - { - return !string.IsNullOrEmpty(value) && Base64Regex.IsMatch(value); - } - - private static string SessionKey => BlockmakerPrefs.Key("magic_session"); - - public string Address { get; } - public string DisplayName { get; } - public IdentityTier Tier => IdentityTier.Email; - public string ProviderName => "Magic"; - public bool HasWallet => true; - - public bool CanSign => - #if UNITY_WEBGL && !UNITY_EDITOR - true; - #else - false; - #endif - - public string Email { get; } - public string SessionToken { get; private set; } - public string RefreshToken { get; private set; } - - public MagicIdentity(string email, string walletAddress, string sessionToken, string refreshToken = null) - { - if (string.IsNullOrEmpty(email)) - throw new ArgumentException("Invalid email", nameof(email)); - if (string.IsNullOrEmpty(walletAddress)) - throw new ArgumentException("Invalid wallet address", nameof(walletAddress)); - - Email = email; - Address = walletAddress; - SessionToken = sessionToken; - RefreshToken = refreshToken; - DisplayName = email.Split('@')[0]; - } - - internal void UpdateTokens(string sessionToken, string refreshToken) - { - if (!string.IsNullOrEmpty(sessionToken)) SessionToken = sessionToken; - if (!string.IsNullOrEmpty(refreshToken)) RefreshToken = refreshToken; - } - - public IEnumerator SignTransaction( - string unsignedTxnBase64, - Action onSigned, - Action onError) - { - if (string.IsNullOrEmpty(unsignedTxnBase64)) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - string result = null; - string error = null; - - #if UNITY_WEBGL && !UNITY_EDITOR - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - BlockmakerWalletBridge.MagicSignTransaction( - unsignedTxnBase64, - BlockmakerAuth.Instance.gameObject.name, - nameof(BlockmakerAuth.Instance.OnTxnSignedFromJS), - nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - - float elapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null && - elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (BlockmakerAuth.Instance != null && !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - result = BlockmakerAuth.Instance.ConsumePendingSignedTxn(); - error = BlockmakerAuth.Instance.ConsumePendingSignError(); - #else - BlockmakerLog.Warning("[MagicIdentity] SignTransaction called outside WebGL."); - error = "Transaction signing with email accounts is only available in the web browser version of this game."; - yield return null; - #endif - - if (result != null) onSigned?.Invoke(result); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - } - - public IEnumerator SignTransactions( - string[] unsignedTxnsBase64, - Action onSigned, - Action onError) - { - if (unsignedTxnsBase64 == null || unsignedTxnsBase64.Length == 0) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - #if UNITY_WEBGL && !UNITY_EDITOR - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - if (!IsValidBase64(unsignedTxnsBase64[i])) - { - onError?.Invoke("Something went wrong preparing the transaction. Please try again."); - yield break; - } - } - - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - - var txnsJson = "[" + string.Join(",", System.Array.ConvertAll(unsignedTxnsBase64, t => "\"" + t + "\"")) + "]"; - BlockmakerWalletBridge.MagicSignGroupTransaction( - txnsJson, - BlockmakerAuth.Instance.gameObject.name, - nameof(BlockmakerAuth.Instance.OnGroupTxnSignedFromJS), - nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - - float elapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxns == null && - BlockmakerAuth.Instance.PendingSignError == null && - elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (BlockmakerAuth.Instance == null || - !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance.PendingSignedTxns == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - var results = BlockmakerAuth.Instance.ConsumePendingSignedTxns(); - var error = BlockmakerAuth.Instance.ConsumePendingSignError(); - - if (results != null) - onSigned?.Invoke(results); - else if (error != null) - onError?.Invoke(error); - else - onError?.Invoke("The request could not be completed. Please try again."); - #else - BlockmakerLog.Warning("[MagicIdentity] SignTransactions called outside WebGL."); - onError?.Invoke("Transaction signing with email accounts is only available in the web browser version of this game."); - yield return null; - #endif - } - - public void SaveSession() - { - var data = new MagicSessionData - { - email = Email, - walletAddress = Address, - sessionToken = SessionToken, - refreshToken = RefreshToken - }; - SecurePrefs.SetString(SessionKey, JsonUtility.ToJson(data)); - SecurePrefs.Save(); - BlockmakerLog.Info($"[MagicIdentity] Session saved for {Email}"); - } - - public void ClearSession() - { - SecurePrefs.DeleteKey(SessionKey); - SecurePrefs.Save(); - - #if UNITY_WEBGL && !UNITY_EDITOR - BlockmakerWalletBridge.MagicLogout(); - #endif - BlockmakerLog.Info("[MagicIdentity] Session cleared."); - } - - public static MagicIdentity TryLoadSession() - { - string json = SecurePrefs.GetString(SessionKey, string.Empty); - if (string.IsNullOrEmpty(json)) return null; - - try - { - var data = JsonUtility.FromJson(json); - - if (string.IsNullOrEmpty(data.walletAddress)) - return null; - if (string.IsNullOrEmpty(data.sessionToken) && string.IsNullOrEmpty(data.refreshToken)) - return null; - - BlockmakerLog.Info($"[MagicIdentity] Restored session for {data.email}"); - return new MagicIdentity(data.email, data.walletAddress, data.sessionToken, data.refreshToken); - } - catch (Exception e) - { - BlockmakerLog.Warning($"[MagicIdentity] Corrupt session data, clearing: {e.Message}"); - SecurePrefs.DeleteKey(SessionKey); - SecurePrefs.Save(); - return null; - } - } - - [Serializable] - private class MagicSessionData - { - public string email; - public string walletAddress; - public string sessionToken; - public string refreshToken; - } - } - -} \ No newline at end of file diff --git a/Identity/MagicIdentity.cs.meta b/Identity/MagicIdentity.cs.meta deleted file mode 100644 index d39127e..0000000 --- a/Identity/MagicIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f11073d4347ad4d4798f96667af6a533 \ No newline at end of file diff --git a/Identity/ServerSignedIdentity.cs b/Identity/ServerSignedIdentity.cs deleted file mode 100644 index b1ef1c3..0000000 --- a/Identity/ServerSignedIdentity.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections; -using UnityEngine; - -namespace Blockmaker -{ - - /// - /// Base class for identities that sign transactions via the Blockmaker server - /// (Email, Magic-fallback). Eliminates duplicate signing coroutine logic. - /// - public abstract class ServerSignedIdentity : IBlockmakerIdentity - { - - public abstract string Address { get; } - public abstract string DisplayName { get; } - public abstract IdentityTier Tier { get; } - public abstract string ProviderName { get; } - public abstract bool HasWallet { get; } - public abstract bool CanSign { get; } - - public abstract string SessionToken { get; protected set; } - public abstract string RefreshToken { get; protected set; } - - internal void UpdateTokens(string sessionToken, string refreshToken) - { - if (!string.IsNullOrEmpty(sessionToken)) SessionToken = sessionToken; - if (!string.IsNullOrEmpty(refreshToken)) RefreshToken = refreshToken; - } - - public virtual IEnumerator SignTransaction( - string unsignedTxnBase64, - Action onSigned, - Action onError) - { - if (string.IsNullOrEmpty(unsignedTxnBase64)) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - var client = BlockmakerClient.Instance; - if (client == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - bool done = false; - string result = null; - string error = null; - bool gotAuthError = false; - - client.StartCoroutine( - client.SignTransactionServerSide( - unsignedTxnBase64, - SessionToken, - r => { result = r; done = true; }, - e => { error = e; done = true; }, - bmErr => { if (bmErr != null && bmErr.IsAuthError) gotAuthError = true; } - ) - ); - - float elapsed = 0f; - while (!done && elapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!done) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (gotAuthError && !string.IsNullOrEmpty(RefreshToken)) - { - var oldToken = SessionToken; - done = false; result = null; error = null; gotAuthError = false; - bool refreshDone = false; - client.RefreshToken(RefreshToken, refreshResult => - { - if (refreshResult != null && !string.IsNullOrEmpty(refreshResult.sessionToken)) - { - UpdateTokens(refreshResult.sessionToken, refreshResult.refreshToken); - SaveSession(); - } - refreshDone = true; - }, _ => { refreshDone = true; }); - - float rElapsed = 0f; - while (!refreshDone && rElapsed < 10f) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - rElapsed += Time.unscaledDeltaTime; yield return null; - } - - if (SessionToken != oldToken && !string.IsNullOrEmpty(SessionToken)) - { - client.StartCoroutine( - client.SignTransactionServerSide( - unsignedTxnBase64, SessionToken, - r => { result = r; done = true; }, - e => { error = e; done = true; }, - _ => {} - ) - ); - elapsed = 0f; - while (!done && elapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - elapsed += Time.unscaledDeltaTime; yield return null; - } - } - else - { - onError?.Invoke("Your session has expired. Please sign in again."); - yield break; - } - } - - if (!done) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (result != null) onSigned?.Invoke(result); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - } - - public virtual IEnumerator SignTransactions( - string[] unsignedTxnsBase64, - Action onSigned, - Action onError) - { - if (unsignedTxnsBase64 == null || unsignedTxnsBase64.Length == 0) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - var client = BlockmakerClient.Instance; - if (client == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - bool done = false; - string[] result = null; - string error = null; - bool gotAuthError = false; - - client.StartCoroutine( - client.SignTransactionsServerSide( - unsignedTxnsBase64, - SessionToken, - r => { result = r; done = true; }, - e => { error = e; done = true; }, - bmErr => { if (bmErr != null && bmErr.IsAuthError) gotAuthError = true; } - ) - ); - - float elapsed = 0f; - while (!done && elapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!done) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (gotAuthError && !string.IsNullOrEmpty(RefreshToken)) - { - var oldToken = SessionToken; - done = false; result = null; error = null; gotAuthError = false; - bool refreshDone = false; - client.RefreshToken(RefreshToken, refreshResult => - { - if (refreshResult != null && !string.IsNullOrEmpty(refreshResult.sessionToken)) - { - UpdateTokens(refreshResult.sessionToken, refreshResult.refreshToken); - SaveSession(); - } - refreshDone = true; - }, _ => { refreshDone = true; }); - - float rElapsed = 0f; - while (!refreshDone && rElapsed < 10f) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - rElapsed += Time.unscaledDeltaTime; yield return null; - } - - if (SessionToken != oldToken && !string.IsNullOrEmpty(SessionToken)) - { - client.StartCoroutine( - client.SignTransactionsServerSide( - unsignedTxnsBase64, SessionToken, - r => { result = r; done = true; }, - e => { error = e; done = true; }, - bmErr => { if (bmErr != null && bmErr.IsAuthError) gotAuthError = true; } - ) - ); - elapsed = 0f; - while (!done && elapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - elapsed += Time.unscaledDeltaTime; yield return null; - } - } - else - { - onError?.Invoke("Your session has expired. Please sign in again."); - yield break; - } - } - - if (!done) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (result != null) onSigned?.Invoke(result); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - } - - public abstract void SaveSession(); - public abstract void ClearSession(); - } - -} \ No newline at end of file diff --git a/Identity/ServerSignedIdentity.cs.meta b/Identity/ServerSignedIdentity.cs.meta deleted file mode 100644 index 7366775..0000000 --- a/Identity/ServerSignedIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9adf352b717344a7698a74eb107fe2ce \ No newline at end of file diff --git a/Identity/WalletConnectIdentity.cs b/Identity/WalletConnectIdentity.cs deleted file mode 100644 index acb01fd..0000000 --- a/Identity/WalletConnectIdentity.cs +++ /dev/null @@ -1,770 +0,0 @@ -using System; -using System.Collections; -using System.Text.RegularExpressions; -using UnityEngine; - -namespace Blockmaker -{ - - public abstract class WalletConnectIdentity : IBlockmakerIdentity - { - private static readonly Regex Base64Regex = new Regex(@"^[A-Za-z0-9+/]*=*$", RegexOptions.Compiled); - - private static bool IsValidBase64(string value) - { - return !string.IsNullOrEmpty(value) && Base64Regex.IsMatch(value); - } - - private static string SessionKeyPrefix => BlockmakerPrefs.Key("wc_session_"); - - public string Address { get; } - public string DisplayName { get; } - public IdentityTier Tier => IdentityTier.SelfCustody; - public abstract string ProviderName { get; } - public bool HasWallet => true; - public bool CanSign - { - get - { - #if UNITY_WEBGL && !UNITY_EDITOR - return true; - #else - // Require the WCv1 client to be genuinely CONNECTED, not merely present. A restored - // session has OwnWCv1Client != null (and !IsDisposed) the instant it's rebuilt, but its - // relay isn't live until Reconnect() finishes — so checking existence alone made CanSign - // report "signable" while a sign would hang/fail. IsConnected reflects the live relay. - if (OwnWCv1Client != null && !OwnWCv1Client.IsDisposed && OwnWCv1Client.IsConnected) return true; - var connector = ReownWalletConnector.Instance; - return connector != null && connector.IsConnected; - #endif - } - } - - internal WalletConnectV1Client OwnWCv1Client { get; set; } - - /// Player JWT obtained via wallet-signature login (see ). - public string SessionToken { get; private set; } - /// Long-lived refresh token paired with . - public string RefreshToken { get; private set; } - - internal void UpdateTokens(string sessionToken, string refreshToken) - { - if (!string.IsNullOrEmpty(sessionToken)) SessionToken = sessionToken; - if (!string.IsNullOrEmpty(refreshToken)) RefreshToken = refreshToken; - } - - /// - /// Clear only the in-memory JWT/refresh tokens, leaving the wallet identity and the - /// live WalletConnect relay/session intact. Used when a token refresh fails but the - /// wallet can still re-sign for a fresh JWT (see BlockmakerAuth.VerifyRestoredSession). - /// - internal void ClearTokens() - { - SessionToken = null; - RefreshToken = null; - } - - protected WalletConnectIdentity(string address) - { - if (!IsValidAlgorandAddress(address)) - throw new ArgumentException("Invalid Algorand wallet address", nameof(address)); - - Address = address; - DisplayName = $"{address[..6]}...{address[^4..]}"; - } - - public static bool IsValidAlgorandAddress(string address) - { - if (string.IsNullOrEmpty(address) || address.Length != 58) - return false; - for (int i = 0; i < address.Length; i++) - { - char c = address[i]; - if (!((c >= 'A' && c <= 'Z') || (c >= '2' && c <= '7'))) - return false; - } - - try - { - byte[] decoded = XChainAddressDeriver.Base32Decode(address); - if (decoded.Length != 36) return false; - - byte[] publicKey = new byte[32]; - byte[] checksum = new byte[4]; - Buffer.BlockCopy(decoded, 0, publicKey, 0, 32); - Buffer.BlockCopy(decoded, 32, checksum, 0, 4); - - byte[] hash = XChainAddressDeriver.Sha512_256(publicKey); - if (hash[28] != checksum[0] || hash[29] != checksum[1] || - hash[30] != checksum[2] || hash[31] != checksum[3]) - return false; - } - catch - { - return false; - } - - return true; - } - - private WalletConnectV1Client GetWCv1Client() - { - if (OwnWCv1Client != null && !OwnWCv1Client.IsDisposed) - return OwnWCv1Client; - - var authClient = BlockmakerAuth.Instance?.WCv1Client; - if (authClient != null && !authClient.IsDisposed) - { - OwnWCv1Client = authClient; - return authClient; - } - - var savedSession = TryLoadWCv1Session(ProviderName); - if (savedSession != null) - { - var restored = WalletConnectV1Client.FromSession(savedSession); - OwnWCv1Client = restored; - return restored; - } - - return null; - } - - public IEnumerator SignTransaction( - string unsignedTxnBase64, - Action onSigned, - Action onError) - { - if (string.IsNullOrEmpty(unsignedTxnBase64)) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - string result = null; - string error = null; - bool done = false; - - var wcv1 = GetWCv1Client(); - if (wcv1 != null && - string.Equals(wcv1.Address, Address, StringComparison.OrdinalIgnoreCase)) - { - if (!wcv1.IsConnected) - { - BlockmakerLog.Info($"[{ProviderName}Identity] WCv1 session exists but disconnected — reconnecting..."); - var reconnectTask = wcv1.Reconnect(); - float rcElapsed = 0f; - while (!reconnectTask.IsCompleted && rcElapsed < 10f) - { - rcElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (reconnectTask.IsFaulted || !wcv1.IsConnected) - { - BlockmakerLog.Warning($"[{ProviderName}Identity] Reconnect failed: {reconnectTask.Exception?.InnerException?.Message}"); - wcv1.Dispose(); - OwnWCv1Client = null; - onError?.Invoke("Your wallet connection was lost. Please connect your wallet again to continue."); - yield break; - } - BlockmakerLog.Info($"[{ProviderName}Identity] Reconnected to WCv1 bridge."); - } - - if (!IsValidBase64(unsignedTxnBase64)) - { - onError?.Invoke("Something went wrong preparing the transaction. Please try again."); - yield break; - } - - string paramsJson = $"[[{{\"txn\":\"{unsignedTxnBase64}\"}}]]"; - var signTask = wcv1.SendCustomRequest("algo_signTxn", paramsJson); - - float elapsed = 0f; - while (!signTask.IsCompleted && elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!signTask.IsCompleted) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (signTask.IsFaulted) - { - BlockmakerLog.Error($"[{ProviderName}Identity] Sign error: {signTask.Exception?.InnerException?.Message}"); - onError?.Invoke("Your wallet could not complete the request. Please try again."); - yield break; - } - - string responseJson = signTask.Result; - var signResult = JsonUtility.FromJson(responseJson); - if (signResult.result != null && signResult.result.Length > 0 && !string.IsNullOrEmpty(signResult.result[0])) - { - onSigned?.Invoke(signResult.result[0]); - } - else - { - onError?.Invoke("The transaction was not approved in your wallet. Please try again."); - } - yield break; - } - - // Try Reown WC v2 (Defly, EVM) - var connector = ReownWalletConnector.Instance; - if (connector != null && connector.IsConnected && - string.Equals(connector.ConnectedAddress, Address, StringComparison.OrdinalIgnoreCase)) - { - connector.SignAlgorandTransaction( - unsignedTxnBase64, - onSigned: signed => { result = signed; done = true; }, - onError: err => { error = err; done = true; } - ); - - float elapsed = 0f; - while (!done && elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!done) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (result != null) onSigned?.Invoke(result); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - yield break; - } - - #if UNITY_WEBGL && !UNITY_EDITOR - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - if (ProviderName == BlockmakerAuth.ProviderPera && - BlockmakerWalletBridge.PeraJsHasSession() == 1) - { - // Pera on WebGL: sign through the official @perawallet/connect JS - // session (established by PeraJsConnect / restored by PeraJsReconnect). - // Same pending-sign plumbing and callbacks as the generic bridge below. - BlockmakerWalletBridge.PeraJsSignTransaction( - txnBase64: unsignedTxnBase64, - gameObjectName: BlockmakerAuth.Instance.gameObject.name, - successCallback: nameof(BlockmakerAuth.Instance.OnTxnSignedFromJS), - errorCallback: nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - } - else - BlockmakerWalletBridge.SignTransaction( - provider: ProviderName, - txnBase64: unsignedTxnBase64, - gameObjectName: BlockmakerAuth.Instance.gameObject.name, - successCallback: nameof(BlockmakerAuth.Instance.OnTxnSignedFromJS), - errorCallback: nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - - float jsElapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null && - jsElapsed < BlockmakerAuth.WalletSignTimeout) - { - jsElapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (BlockmakerAuth.Instance != null && !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance.PendingSignedTxn == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - result = BlockmakerAuth.Instance.ConsumePendingSignedTxn(); - error = BlockmakerAuth.Instance.ConsumePendingSignError(); - #else - BlockmakerLog.Warning($"[{ProviderName}Identity] No active WC v1/v2 session and no JS bridge available."); - error = "Your wallet is not connected. Please connect your wallet again to continue."; - yield return null; - #endif - - if (result != null) onSigned?.Invoke(result); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - } - - public IEnumerator SignTransactions( - string[] unsignedTxnsBase64, - Action onSigned, - Action onError) - { - if (unsignedTxnsBase64 == null || unsignedTxnsBase64.Length == 0) - { - onError?.Invoke("Something went wrong. Please try again."); - yield break; - } - - string[] results = null; - string error = null; - bool done = false; - - var wcv1 = GetWCv1Client(); - if (wcv1 != null && - string.Equals(wcv1.Address, Address, StringComparison.OrdinalIgnoreCase)) - { - if (!wcv1.IsConnected) - { - BlockmakerLog.Info($"[{ProviderName}Identity] WCv1 session exists but disconnected — reconnecting..."); - var reconnectTask = wcv1.Reconnect(); - float rcElapsed = 0f; - while (!reconnectTask.IsCompleted && rcElapsed < 10f) - { - rcElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (reconnectTask.IsFaulted || !wcv1.IsConnected) - { - BlockmakerLog.Warning($"[{ProviderName}Identity] Reconnect failed: {reconnectTask.Exception?.InnerException?.Message}"); - wcv1.Dispose(); - OwnWCv1Client = null; - onError?.Invoke("Your wallet connection was lost. Please connect your wallet again to continue."); - yield break; - } - BlockmakerLog.Info($"[{ProviderName}Identity] Reconnected to WCv1 bridge."); - } - - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - if (!IsValidBase64(unsignedTxnsBase64[i])) - { - onError?.Invoke("Something went wrong preparing the transaction. Please try again."); - yield break; - } - } - - var txnObjects = new System.Text.StringBuilder("["); - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - if (i > 0) txnObjects.Append(","); - txnObjects.Append($"{{\"txn\":\"{unsignedTxnsBase64[i]}\"}}"); - } - txnObjects.Append("]"); - string paramsJson = $"[{txnObjects}]"; - - var signTask = wcv1.SendCustomRequest("algo_signTxn", paramsJson); - - float elapsed = 0f; - while (!signTask.IsCompleted && elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!signTask.IsCompleted) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (signTask.IsFaulted) - { - BlockmakerLog.Error($"[{ProviderName}Identity] Sign error: {signTask.Exception?.InnerException?.Message}"); - onError?.Invoke("Your wallet could not complete the request. Please try again."); - yield break; - } - - var signResult = JsonUtility.FromJson(signTask.Result); - if (signResult.result != null && signResult.result.Length == unsignedTxnsBase64.Length) - { - bool allSigned = true; - for (int j = 0; j < signResult.result.Length; j++) - { - if (string.IsNullOrEmpty(signResult.result[j])) - { allSigned = false; break; } - } - if (allSigned) - onSigned?.Invoke(signResult.result); - else - onError?.Invoke("Not all transactions were approved. Please approve all of them in your wallet and try again."); - } - else - { - onError?.Invoke("The transactions were not approved in your wallet. Please try again."); - } - yield break; - } - - var connector = ReownWalletConnector.Instance; - if (connector != null && connector.IsConnected && - string.Equals(connector.ConnectedAddress, Address, StringComparison.OrdinalIgnoreCase)) - { - connector.SignAlgorandTransactions( - unsignedTxnsBase64, - onSigned: signed => { results = signed; done = true; }, - onError: err => { error = err; done = true; } - ); - - float elapsed = 0f; - while (!done && elapsed < BlockmakerAuth.WalletSignTimeout) - { - elapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (!done) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - if (results != null) onSigned?.Invoke(results); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - yield break; - } - - #if UNITY_WEBGL && !UNITY_EDITOR - if (BlockmakerAuth.Instance == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - if (!IsValidBase64(unsignedTxnsBase64[i])) - { - onError?.Invoke("Something went wrong preparing the transaction. Please try again."); - yield break; - } - } - - string txnsJson = "["; - for (int i = 0; i < unsignedTxnsBase64.Length; i++) - { - if (i > 0) txnsJson += ","; - txnsJson += $"\"{unsignedTxnsBase64[i]}\""; - } - txnsJson += "]"; - - int signGen = BlockmakerAuth.Instance.BeginPendingSign(); - if (ProviderName == BlockmakerAuth.ProviderPera && - BlockmakerWalletBridge.PeraJsHasSession() == 1) - { - // Pera on WebGL: sign through the official @perawallet/connect JS - // session. One atomic group, original order preserved; every txn is - // presented for signing (all-or-nothing), matching the WCv1/WC v2/ - // bridge group paths. - BlockmakerWalletBridge.PeraJsSignGroupTransaction( - txnsJson: txnsJson, - gameObjectName: BlockmakerAuth.Instance.gameObject.name, - successCallback: nameof(BlockmakerAuth.Instance.OnGroupTxnSignedFromJS), - errorCallback: nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - } - else - BlockmakerWalletBridge.SignGroupTransaction( - provider: ProviderName, - txnsJson: txnsJson, - gameObjectName: BlockmakerAuth.Instance.gameObject.name, - successCallback: nameof(BlockmakerAuth.Instance.OnGroupTxnSignedFromJS), - errorCallback: nameof(BlockmakerAuth.Instance.OnTxnErrorFromJS) - ); - - float jsElapsed = 0f; - while (BlockmakerAuth.Instance != null && - BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen) && - BlockmakerAuth.Instance.PendingSignedTxns == null && - BlockmakerAuth.Instance.PendingSignError == null && - jsElapsed < BlockmakerAuth.WalletSignTimeout) - { - jsElapsed += Time.unscaledDeltaTime; - yield return null; - } - - if (BlockmakerAuth.Instance == null || - !BlockmakerAuth.Instance.IsSignGenerationCurrent(signGen)) - { - onError?.Invoke("The request was interrupted. Please try again."); - yield break; - } - - if (BlockmakerAuth.Instance.PendingSignedTxns == null && - BlockmakerAuth.Instance.PendingSignError == null) - { - onError?.Invoke("The request timed out. Please try again."); - yield break; - } - - results = BlockmakerAuth.Instance.ConsumePendingSignedTxns(); - error = BlockmakerAuth.Instance.ConsumePendingSignError(); - - if (results != null) onSigned?.Invoke(results); - else if (error != null) onError?.Invoke(error); - else onError?.Invoke("The request could not be completed. Please try again."); - yield break; - #endif - - onError?.Invoke("Your wallet is not connected. Please connect your wallet again to continue."); - yield return null; - } - - public void SaveSession() - { - var data = new WalletSessionData - { - providerName = ProviderName, - address = Address, - sessionToken = SessionToken, - refreshToken = RefreshToken - }; - string key = SessionKeyPrefix + ProviderName.ToLower(); - SecurePrefs.SetString(key, JsonUtility.ToJson(data)); - - var wcv1 = GetWCv1Client(); - if (wcv1 != null) - { - var sessionData = wcv1.GetSessionData(); - if (sessionData != null) - { - string wcKey = SessionKeyPrefix + ProviderName.ToLower() + "_wcv1"; - SecurePrefs.SetString(wcKey, JsonUtility.ToJson(sessionData)); - } - } - - SecurePrefs.Save(); - } - - public void ClearSession() - { - string key = SessionKeyPrefix + ProviderName.ToLower(); - SecurePrefs.DeleteKey(key); - SecurePrefs.DeleteKey(key + "_wcv1"); - SecurePrefs.Save(); - - if (OwnWCv1Client != null && !OwnWCv1Client.IsDisposed) - { - OwnWCv1Client.Dispose(); - } - OwnWCv1Client = null; - - var connector = ReownWalletConnector.Instance; - if (connector != null && - string.Equals(connector.ConnectedAddress, Address, StringComparison.OrdinalIgnoreCase)) - { - connector.Disconnect(); - } - - #if UNITY_WEBGL && !UNITY_EDITOR - BlockmakerWalletBridge.Disconnect(ProviderName); - #endif - BlockmakerLog.Info($"[{ProviderName}Identity] Session cleared."); - } - - public static WalletSessionData TryLoadSessionData(string providerName) - { - string key = SessionKeyPrefix + providerName.ToLower(); - string json = SecurePrefs.GetString(key, string.Empty); - if (string.IsNullOrEmpty(json)) return null; - - try - { - var data = JsonUtility.FromJson(json); - if (string.IsNullOrEmpty(data.address)) - return null; - return data; - } - catch (Exception e) - { - BlockmakerLog.Warning($"[WalletConnectIdentity] Corrupt session data for {providerName}, clearing: {e.Message}"); - SecurePrefs.DeleteKey(key); - SecurePrefs.Save(); - return null; - } - } - - public static WalletConnectV1Client.SavedSession TryLoadWCv1Session(string providerName) - { - string key = SessionKeyPrefix + providerName.ToLower() + "_wcv1"; - string json = SecurePrefs.GetString(key, string.Empty); - if (string.IsNullOrEmpty(json)) return null; - try { return JsonUtility.FromJson(json); } - catch (Exception e) - { - BlockmakerLog.Warning($"[WalletConnectIdentity] Corrupt WCv1 session for {providerName}, clearing: {e.Message}"); - SecurePrefs.DeleteKey(key); - SecurePrefs.Save(); - return null; - } - } - - [Serializable] - public class WalletSessionData - { - public string providerName; - public string address; - public string sessionToken; - public string refreshToken; - } - - // ── Wallet-signature login (challenge → sign → verify → store JWT) ───────── - - /// - /// Acquire a player session token by proving wallet ownership to the server. - /// Runs: challenge → build a 0-amount self-payment whose note == the nonce → - /// sign it with the wallet via algo_signTxn (the universal signing method - /// both Pera WCv1 and Defly WCv2 support) → verify → store SessionToken/RefreshToken. - /// The unsigned txn is built server-side (same path as the shop/reward flows), - /// so suggested params come from the server's algod. - /// Idempotent-ish: safe to call again to re-acquire a token. - /// - public IEnumerator Login(Action onSuccess = null, Action onError = null) - { - var client = BlockmakerClient.Instance; - if (client == null) - { - onError?.Invoke("Something went wrong. Please restart the game and try again."); - yield break; - } - - // 1) Challenge - WalletChallengeResult challenge = null; - string challengeError = null; - bool challengeDone = false; - client.StartCoroutine(client.RequestWalletChallenge( - Address, "algorand", null, - r => { challenge = r; challengeDone = true; }, - e => { challengeError = e; challengeDone = true; })); - - float elapsed = 0f; - while (!challengeDone && elapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - elapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!challengeDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - if (challenge == null || !challenge.success || string.IsNullOrEmpty(challenge.message)) - { - onError?.Invoke(challengeError ?? "Could not start wallet sign-in. Please try again."); - yield break; - } - - // 2a) Build a 0-amount self-payment whose note == the challenge nonce. - // Built server-side (same path as the shop/reward flows) so suggested - // params come from the server's algod. The server's verify then checks - // pay-type, sender==receiver==this wallet, amount 0, and note==nonce. - string unsignedTxnBase64 = null; - string buildError = null; - bool buildDone = false; - client.BuildPayment( - recipient: Address, amountMicroAlgo: 0, note: challenge.nonce, - onSuccess: r => - { - if (r != null && r.success && !string.IsNullOrEmpty(r.unsignedTxnBase64)) - unsignedTxnBase64 = r.unsignedTxnBase64; - else - buildError = (r != null ? r.error : null) ?? "Could not start wallet sign-in. Please try again."; - buildDone = true; - }, - onError: e => { buildError = e; buildDone = true; }); - - float bElapsed = 0f; - while (!buildDone && bElapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - bElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!buildDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - if (!string.IsNullOrEmpty(buildError) || string.IsNullOrEmpty(unsignedTxnBase64)) - { - onError?.Invoke(buildError ?? "Could not start wallet sign-in. Please try again."); - yield break; - } - - // 2b) Sign it via algo_signTxn (universal — Pera WCv1, Defly WCv2, WebGL bridge). - // Reuses SignTransaction, the exact same signing path the shop/reward flows - // use, so no new wallet-signing code and no arbitrary-bytes (signData) path. - string signedTxn = null; - string signError = null; - bool signDone = false; - yield return SignTransaction( - unsignedTxnBase64, - signed => { signedTxn = signed; signDone = true; }, - err => { signError = err; signDone = true; }); - - if (!signDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - if (!string.IsNullOrEmpty(signError)) { onError?.Invoke(signError); yield break; } - if (string.IsNullOrEmpty(signedTxn)) - { - onError?.Invoke("The sign-in request was not approved in your wallet. Please try again."); - yield break; - } - - // 3) Verify → mint JWT - EmailVerifyResult verify = null; - string verifyError = null; - bool verifyDone = false; - client.StartCoroutine(client.VerifyWalletSignature( - Address, "algorand", null, signedTxn, challenge.nonce, null, - r => { verify = r; verifyDone = true; }, - e => { verifyError = e; verifyDone = true; })); - - float vElapsed = 0f; - while (!verifyDone && vElapsed < BlockmakerAuth.WalletSignTimeout) - { - if (client == null) { onError?.Invoke("Something went wrong. Please restart the game and try again."); yield break; } - vElapsed += Time.unscaledDeltaTime; - yield return null; - } - if (!verifyDone) { onError?.Invoke("The request timed out. Please try again."); yield break; } - if (verify == null || !verify.success || string.IsNullOrEmpty(verify.sessionToken)) - { - onError?.Invoke(verifyError ?? "Wallet sign-in failed. Please try again."); - yield break; - } - - // 4) Store - UpdateTokens(verify.sessionToken, verify.refreshToken); - SaveSession(); - BlockmakerLog.Info($"[{ProviderName}Identity] Wallet sign-in complete — session token acquired."); - onSuccess?.Invoke(); - } - } - - public class PeraIdentity : WalletConnectIdentity - { - public override string ProviderName => "Pera"; - public PeraIdentity(string address) : base(address) { } - } - - public class DeflyIdentity : WalletConnectIdentity - { - public override string ProviderName => "Defly"; - public DeflyIdentity(string address) : base(address) { } - } - -} \ No newline at end of file diff --git a/Identity/WalletConnectIdentity.cs.meta b/Identity/WalletConnectIdentity.cs.meta deleted file mode 100644 index dbcc594..0000000 --- a/Identity/WalletConnectIdentity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3dd04abfa3ece4c56b89080dd19c6f65 \ No newline at end of file diff --git a/Installer~/BlockmakerInstaller.cs b/Installer~/BlockmakerInstaller.cs index 1a934a0..2af6951 100644 --- a/Installer~/BlockmakerInstaller.cs +++ b/Installer~/BlockmakerInstaller.cs @@ -1,93 +1,50 @@ #if UNITY_EDITOR using System.IO; using UnityEditor; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; using UnityEngine; -/// -/// Drop this file into your Unity project's Assets folder. -/// Legacy v1.2.0 installer. New WebGL games should use WebGL~/README.md. -/// You can delete this file after installation. -/// +/// Optional drop-in installer for the same pinned UPM release. [InitializeOnLoad] public static class BlockmakerInstaller { + private const string PackageUrl = "https://github.com/blockmaker-ai/blockmaker-unity.git#v2.0.0"; + private static AddRequest request; static BlockmakerInstaller() { - var manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); - if (!File.Exists(manifestPath)) return; - - var content = File.ReadAllText(manifestPath); - if (content.Contains("com.blockmaker.sdk")) return; // Already installed - - EditorApplication.delayCall += () => - { - if (EditorUtility.DisplayDialog( - "Install Blockmaker SDK", - "This will install the Blockmaker wallet SDK and the Reown dependency.\n\nYour project will reload after installation.", - "Install", - "Cancel")) - { - Install(manifestPath); - } + // Offer once per Editor session. A cancelled dialog never repeats on reload. + if (SessionState.GetBool("BlockmakerInstaller.offered", false)) return; + SessionState.SetBool("BlockmakerInstaller.offered", true); + EditorApplication.delayCall += () => { + if (HasPackage()) return; + if (EditorUtility.DisplayDialog("Install Blockmaker Wallets", "Install the current Pera and optional email wallet package for Unity WebGL?", "Install", "Cancel")) Install(); }; } - - [MenuItem("Blockmaker/Install SDK")] - public static void InstallFromMenu() + private static bool HasPackage() { - var manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); - if (!File.Exists(manifestPath)) - { - EditorUtility.DisplayDialog("Error", "Could not find Packages/manifest.json", "OK"); - return; - } - - var content = File.ReadAllText(manifestPath); - if (content.Contains("com.blockmaker.sdk")) - { - EditorUtility.DisplayDialog("Blockmaker SDK", "Already installed!", "OK"); - return; - } - - Install(manifestPath); + var manifest = Path.Combine(Application.dataPath, "../Packages/manifest.json"); + return File.Exists(manifest) && File.ReadAllText(manifest).Contains("\"com.blockmaker.sdk\""); } - - static void Install(string manifestPath) + [MenuItem("Blockmaker/Install or Update Wallet Package")] + public static void Install() { - var content = File.ReadAllText(manifestPath); - - // Add scoped registry if not present - if (!content.Contains("package.openupm.com")) - { - var lastBrace = content.LastIndexOf('}'); - var registry = @", - ""scopedRegistries"": [ - { - ""name"": ""OpenUPM"", - ""url"": ""https://package.openupm.com"", - ""scopes"": [""com.reown"", ""com.nethereum"", ""com.cysharp""] - } - ] -}"; - content = content.Substring(0, lastBrace) + registry; - } - - // Add dependencies - var depsIdx = content.IndexOf("\"dependencies\""); - if (depsIdx >= 0) - { - var braceIdx = content.IndexOf('{', depsIdx); - if (braceIdx >= 0) - { - var insert = "\n \"com.blockmaker.sdk\": \"https://github.com/blockmaker-ai/blockmaker-unity.git#v1.2.0\",\n \"com.nethereum.unity\": \"4.19.2\",\n \"com.reown.sign.nethereum\": \"1.6.0\",\n \"com.reown.sign.unity\": \"1.6.0\","; - content = content.Substring(0, braceIdx + 1) + insert + content.Substring(braceIdx + 1); - } + if (request != null) return; + if (Directory.GetFiles(Application.dataPath, "BlockmakerWalletPackageWebGL.cs", SearchOption.AllDirectories).Length > 0 || + Directory.GetFiles(Application.dataPath, "BlockmakerAuth.cs", SearchOption.AllDirectories).Length > 0) { + EditorUtility.DisplayDialog("Existing Blockmaker files", "Blockmaker scripts already exist in Assets. Follow the README migration steps before installing the package. Your files were not changed.", "OK"); return; } - - File.WriteAllText(manifestPath, content); - Debug.Log("[Blockmaker] SDK installed! Unity is resolving packages..."); - - UnityEditor.PackageManager.Client.Resolve(); + request = Client.Add(PackageUrl); + EditorApplication.update += Progress; + } + private static void Progress() + { + if (request == null || !request.IsCompleted) return; + EditorApplication.update -= Progress; + if (request.Status == StatusCode.Success) + Debug.Log("Blockmaker Wallets installed. Use Blockmaker > Setup Wallet Demo. You can delete this installer file."); + else Debug.LogError("Blockmaker installation failed: " + request.Error.message); + request = null; } } #endif diff --git a/LEGACY_V1.md b/LEGACY_V1.md deleted file mode 100644 index 4acd1dc..0000000 --- a/LEGACY_V1.md +++ /dev/null @@ -1,304 +0,0 @@ -# Legacy v1 documentation - -This is the historical v1.2.0 guide. Its server OTP/Magic and native-platform -claims do not describe the current WebGL package. Do not use these email -paths for new integrations. Start with the [current SDK guide](README.md). -Existing projects can pin v1.2.0 while planning an explicit migration. - -# Blockmaker Unity SDK - -Add Algorand wallet auth and transaction signing to your Unity game. Open-source, free, built for Unity 6+. - -## Supported Wallets - -| Wallet | How it works | -|--------|-------------| -| **Pera** | QR code scan (WalletConnect v1). On WebGL the SDK uses Pera's official [@perawallet/connect](https://github.com/perawallet/connect); mobile browsers get an "Open in wallet app" deep link | -| **Defly** | QR code scan (WalletConnect v2), mobile deep link per Defly's official SDK | -| **X-Chain** *(beta)* | Any EVM wallet via [xChain Accounts](https://github.com/algorandfoundation/xchain-accounts). On WebGL the SDK discovers every wallet installed in the player's browser (EIP-6963) and shows it in the picker with its real icon — MetaMask, Rainbow, Coinbase Wallet, Trust + more; native builds connect via WalletConnect QR. Off by default — set `enableEvmXChain` in your BlockmakerConfig | -| **Email** | Server-managed OTP out of the box (all platforms); optionally Magic SDK on WebGL with your own key | - -> Currently Algorand **mainnet** only. - -## Installation - -### One-click install - -1. **[Download BlockmakerInstaller.cs](https://raw.githubusercontent.com/blockmaker-ai/blockmaker-unity/main/Installer~/BlockmakerInstaller.cs)** (right-click → Save As) -2. Drag it into your Unity project (anywhere in the Assets folder) -3. A dialog will appear — click **Install** -4. Unity installs everything automatically. Done. - -You can delete the installer file after installation. - -
-Manual install (if you prefer) - -Open `Packages/manifest.json` in your project folder and add: - -**In `"dependencies"`:** -```json -"com.blockmaker.sdk": "https://github.com/blockmaker-ai/blockmaker-unity.git#v1.2.0", -"com.nethereum.unity": "4.19.2", -"com.reown.sign.nethereum": "1.6.0", -"com.reown.sign.unity": "1.6.0", -``` - -**At the bottom of the file, before the last `}`:** -```json -, -"scopedRegistries": [ - { - "name": "OpenUPM", - "url": "https://package.openupm.com", - "scopes": ["com.reown", "com.nethereum", "com.cysharp"] - } -] -``` - -Save and reopen Unity. - -
- -## Setup - -Go to **Blockmaker > Setup Scene** in the menu bar. This automatically: - -- Creates a BlockmakerConfig asset -- Adds BlockmakerAuth with the config wired up -- Adds the auth UI with all UXML assets pre-assigned -- Creates PanelSettings if needed -- Adds a Connect Wallet button - -Hit Play — the wallet system is ready. No API keys or signups needed. - -### Sample scene - -Want to see it working first? In **Package Manager > Blockmaker SDK > Samples**, import the **Wallet Demo**. Add the `WalletDemo` script to an empty GameObject and hit Play — includes wallet connection and transaction signing. - -## Usage - -### Connect a wallet - -```csharp -using Blockmaker; - -// Pera (QR code) -BlockmakerAuth.Instance.ConnectWallet("Pera", - identity => Debug.Log($"Connected: {identity.Address}"), - error => Debug.Log(error) -); - -// Defly (QR code) -BlockmakerAuth.Instance.ConnectWallet("Defly", - identity => Debug.Log($"Connected: {identity.Address}"), - error => Debug.Log(error) -); - -// X-Chain — any EVM wallet (beta). On WebGL this auto-picks the player's -// last-used browser wallet; native builds show a WalletConnect QR. -// Off by default — set enableEvmXChain in your BlockmakerConfig. -BlockmakerAuth.Instance.ConnectEvm( - identity => Debug.Log($"Connected: {identity.Address}"), // Algorand address, derived from the EVM key - error => Debug.Log(error) -); - -// Email login — built-in server OTP (works everywhere, no keys needed) -BlockmakerAuth.Instance.RequestEmailOTP("player@example.com", - () => Debug.Log("Code sent — check your inbox"), - error => Debug.Log(error) -); -BlockmakerAuth.Instance.VerifyEmailOTP("player@example.com", "123456", - identity => Debug.Log($"Signed in: {identity.Address}"), - error => Debug.Log(error) -); - -// Email login via Magic SDK — WebGL only, requires magicPublishableKey in your config -// (without a key the built-in auth UI automatically uses the OTP flow above) -BlockmakerAuth.Instance.ConnectMagicEmail("player@example.com", - identity => Debug.Log($"Signed in: {identity.Address}"), - error => Debug.Log(error) -); -``` - -### Async/await - -```csharp -var identity = await BlockmakerAuth.Instance.ConnectWalletAsync("Pera"); -var evmIdentity = await BlockmakerAuth.Instance.ConnectEvmAsync(); -``` - -### Sign transactions - -```csharp -var identity = BlockmakerAuth.Instance.Identity; - -// Single transaction -yield return identity.SignTransaction(unsignedTxnBase64, - signed => Debug.Log("Signed!"), - error => Debug.Log(error) -); - -// Atomic group -yield return identity.SignTransactions(unsignedTxnsBase64, - signed => Debug.Log($"Signed {signed.Length} transactions"), - error => Debug.Log(error) -); -``` - -### Check state - -```csharp -BlockmakerAuth.Instance.HasWallet // true if a wallet is connected -BlockmakerAuth.Instance.Address // Algorand address -BlockmakerAuth.Instance.CanSign // true if the wallet can sign right now -BlockmakerAuth.Instance.IsLoggedIn // true for Email or SelfCustody tier -BlockmakerAuth.Instance.Tier // Guest, Email, or SelfCustody -``` - -### Listen for changes - -```csharp -void OnEnable() -{ - BlockmakerAuth.OnIdentityChanged += HandleIdentityChanged; -} - -void OnDisable() -{ - BlockmakerAuth.OnIdentityChanged -= HandleIdentityChanged; -} - -void HandleIdentityChanged(IBlockmakerIdentity identity) -{ - Debug.Log($"Identity changed: {identity.ProviderName} — {identity.Address}"); -} -``` - -### Session restore & sign-in status - -Self-custody sign-in is **two approvals**: connect, then a signature that logs the -player into the backend. The SDK exposes everything a custom UI needs to render that -honestly: - -```csharp -// Boot-time restore: don't guess — wait for the restore to settle. -if (BlockmakerAuth.SessionRestoreSettled) { /* auth state is known */ } -BlockmakerAuth.OnSessionRestoreSettled += () => - Debug.Log($"Restore settled — tier: {BlockmakerAuth.Instance.Tier}"); - -// Progress messages while the player must act in their wallet app. -BlockmakerAuth.OnAuthStatus += msg => statusLabel.text = msg; - -// The step-2 (login signature) window: offer RESEND / CANCEL instead of a dead end. -if (BlockmakerAuth.CanRetryWalletLogin) -{ - BlockmakerAuth.Instance.RetryWalletLogin(); // push a fresh sign-in request - // or: BlockmakerAuth.Instance.CancelWalletLogin(); -} - -// Skip best-effort backend calls that would 401 for guests. -if (BlockmakerClient.Instance.HasBackendSession) { /* safe to call player APIs */ } -``` - -The built-in auth UI already implements all of this (a "STEP 2 OF 2" panel with -RESEND / CANCEL). - -### WebGL: self-hosting the wallet JS (optional) - -On WebGL the SDK loads its wallet libraries from jsDelivr at runtime — zero setup. -For immunity to ad-blockers and CDN outages you can ship the bundles with your game: -build IIFE bundles that assign `window.BmPeraVendor = { PeraWalletConnect, algosdk }` -and/or `window.BmWCVendor = { SignClient, QRCode }`, and load them via ` + + diff --git a/Tools~/package.test.mjs b/Tools~/package.test.mjs new file mode 100644 index 0000000..62a933a --- /dev/null +++ b/Tools~/package.test.mjs @@ -0,0 +1,79 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFile, readdir } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { resolve, dirname, join } from 'node:path' +import { parseWalletPackageManifest, integrityForWalletPackageBytes } from './wallet-package-contract.mjs' +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const text = path => readFile(join(root, path), 'utf8') +const json = async path => JSON.parse(await text(path)) +const location = name => name.endsWith('.cs') ? 'Runtime/' + name : name.endsWith('.jslib') ? 'Plugins/WebGL/' + name : 'Browser~/' + name +async function allFiles(path = '') { + const out = [] + for (const entry of await readdir(join(root, path), { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue + const next = path ? path + '/' + entry.name : entry.name + if (entry.isDirectory()) out.push(...await allFiles(next)); else out.push(next) + } + return out +} + +test('UPM root is version 2 with only Unity module dependencies and current installer', async () => { + const pkg = await json('package.json') + assert.equal(pkg.name, 'com.blockmaker.sdk'); assert.equal(pkg.version, '2.0.0') + assert.ok(Object.keys(pkg.dependencies).every(name => name.startsWith('com.unity.modules.'))) + const runtime = await json('Runtime/Blockmaker.asmdef') + assert.equal(runtime.name, 'Blockmaker'); assert.deepEqual(runtime.references, []) + const editor = await json('Editor/Blockmaker.Editor.asmdef') + assert.deepEqual(editor.includePlatforms, ['Editor']); assert.ok(editor.references.includes('Blockmaker')) + const installer = await text('Installer~/BlockmakerInstaller.cs') + assert.ok(installer.includes('Client.Add(PackageUrl)')) + assert.ok(installer.includes('https://github.com/blockmaker-ai/blockmaker-unity.git#v2.0.0')) +}) + +test('UPM carries the complete unmodified canonical runtime and matching build map', async () => { + const value = await json('package-manifest.json') + const manifest = parseWalletPackageManifest(value, { apiOrigin: new URL(value.members[0].url).origin, target: 'unity_webgl' }) + assert.equal(manifest.packageId, 'unity-webgl-package-sha256-cicBnjQf1cPzH5Sz3Gt7wX8seHXG9Cj1VTf6eRIAFlQ') + assert.equal(manifest.members.length, 9) + const hook = await text('Editor/BlockmakerWebGLBuild.cs') + for (const member of manifest.members) { + const path = location(member.file), bytes = await readFile(join(root, path)) + assert.equal(bytes.length, member.rawBytes); assert.equal(integrityForWalletPackageBytes(bytes), member.integrity) + assert.ok(hook.includes('"' + path + '"'), path) + } + assert.equal((await readdir(join(root, 'Browser~'))).length, 5) + assert.ok(hook.includes('AddAdditionalPathToStreamingAssets')) +}) + +test('all imported assets have unique metadata and the bridge is WebGL-only', async () => { + const files = await allFiles(), guids = new Set() + for (const path of files.filter(path => !path.split('/').some(part => part.endsWith('~')) && !path.endsWith('.meta'))) { + const meta = await text(path + '.meta') + const guid = meta.match(/^guid: ([a-f0-9]{32})$/m)?.[1] + assert.ok(guid, path); assert.ok(!guids.has(guid), path); guids.add(guid) + } + const plugin = await text('Plugins/WebGL/BlockmakerWalletPackageWebGL.jslib.meta') + assert.match(plugin, /WebGL: WebGL\n\s+second:\n\s+enabled: 1/) + assert.match(plugin, /Editor: Editor\n\s+second:\n\s+enabled: 0/) +}) + +test('active runtime has one wallet facade and no obsolete implementation', async () => { + const files = await allFiles() + assert.ok(!files.some(path => /^(Core|Identity|UI|WebGL~)\//.test(path))) + assert.ok(!files.some(path => /BlockmakerAuth\.cs|BlockmakerWalletBridge\.jslib|SecurePrefs\.cs/.test(path))) + assert.equal(files.filter(path => path.endsWith('BlockmakerWalletPackageWebGL.cs')).length, 1) +}) + +test('documentation links, sample and fullscreen template resolve', async () => { + for (const path of (await allFiles()).filter(path => path.endsWith('.md') && path !== 'CHANGELOG.md')) { + for (const [, link] of (await text(path)).matchAll(/\]\(([^)]+)\)/g)) { + if (/^https?:|^#/.test(link)) continue + await readFile(resolve(root, dirname(path), link.split('#')[0])) + } + } + const template = await text('Templates~/Blockmaker/index.html') + assert.ok(template.includes('document.documentElement.requestFullscreen()')) + assert.ok(!template.includes('SetFullscreen')) + for (const sample of (await json('package.json')).samples) assert.ok((await readdir(join(root, sample.path))).length > 0) +}) diff --git a/WebGL~/Tools/wallet-package-contract.mjs b/Tools~/wallet-package-contract.mjs similarity index 100% rename from WebGL~/Tools/wallet-package-contract.mjs rename to Tools~/wallet-package-contract.mjs diff --git a/UI.meta b/UI.meta deleted file mode 100644 index f33194e..0000000 --- a/UI.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: cc3379bb9e30e4eeea791109cf89227a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UI/AuthPrompt.uss b/UI/AuthPrompt.uss deleted file mode 100644 index 4292214..0000000 --- a/UI/AuthPrompt.uss +++ /dev/null @@ -1,1180 +0,0 @@ -/* ========================================================================= - AuthPrompt.uss - Blockmaker SDK Auth Modal - ========================================================================= */ - -@import url("BlockmakerTheme.uss"); - -/* ── Overlay ──────────────────────────────────────────────────────────── */ - -.auth-overlay { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - background-color: rgba(4, 4, 10, 0.78); - align-items: center; - justify-content: center; - display: none; -} - -/* ── Card ─────────────────────────────────────────────────────────────── */ - -.auth-card { - width: 92%; - max-width: 440px; /* single-column sign-in — 560 read as too wide */ - flex-direction: column; - align-items: stretch; - background-color: rgb(13, 13, 20); - border-top-left-radius: 20px; - border-top-right-radius: 20px; - border-bottom-left-radius: 20px; - border-bottom-right-radius: 20px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: rgba(255, 255, 255, 0.10); - border-right-color: rgba(255, 255, 255, 0.05); - border-left-color: rgba(255, 255, 255, 0.05); - border-bottom-color: rgba(0, 0, 0, 0.50); - overflow: hidden; - padding-bottom: 12px; -} - -/* ── Close button (cross made from two rotated bars) ──────────────────── */ - -.auth-close-btn { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - border-top-width: 0; - border-right-width: 0; - border-bottom-width: 0; - border-left-width: 0; - background-color: transparent; - padding-top: 0; - padding-bottom: 0; - padding-left: 0; - padding-right: 0; - align-items: center; - justify-content: center; - cursor: link; - color: rgba(255, 255, 255, 0.65); /* 0.25 was invisible on the dark card */ - font-size: 18px; - -unity-font-style: normal; - transition-property: background-color, color; - transition-duration: 0.15s; - transition-timing-function: ease-in-out; -} - -.auth-close-btn:hover { - background-color: rgba(255, 255, 255, 0.10); - color: rgba(255, 255, 255, 1); -} - -.auth-close-btn:active { - background-color: rgba(255, 255, 255, 0.03); - color: rgba(255, 255, 255, 0.35); -} - -/* Two-bar cross geometry lives in BlockmakerTheme.uss; brighten on hover. */ -.auth-close-btn:hover .auth-close-bar { - background-color: rgba(238, 238, 246, 0.90); -} - -/* ── Header ───────────────────────────────────────────────────────────── */ - -.auth-header { - flex-direction: column; - align-items: center; - padding-top: 40px; - padding-bottom: 4px; - padding-left: 40px; - padding-right: 40px; -} - -.auth-title { - font-size: 32px; - color: rgb(238, 238, 246); - -unity-font-style: bold; - -unity-text-align: middle-center; - letter-spacing: -0.5px; -} - -/* ── Pages ─────────────────────────────────────────────────────────────── */ - -.auth-page { - flex-direction: column; - padding-top: 20px; - padding-bottom: 24px; - padding-left: 40px; - padding-right: 40px; - display: none; -} - -.auth-page--center { - align-items: center; -} - -/* ── Page navigation (back arrow + heading) ────────────────────────────── */ - -.auth-page-nav { - flex-direction: row; - align-items: center; - justify-content: center; /* heading centred between the corner back + close buttons */ - margin-bottom: 28px; - align-self: stretch; -} - -.auth-back-btn { - position: absolute; - top: 14px; - left: 14px; - width: 36px; - height: 36px; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: rgba(167, 139, 250, 0.45); - border-right-color: rgba(167, 139, 250, 0.20); - border-left-color: rgba(167, 139, 250, 0.20); - border-bottom-color: rgba(75, 25, 160, 0.70); - background-color: rgb(109, 40, 217); - color: rgba(255, 255, 255, 0.90); - font-size: 22px; - -unity-text-align: middle-center; - align-items: center; - justify-content: center; - padding-top: 0; - padding-bottom: 0; - padding-left: 0; - padding-right: 0; - margin-right: 16px; - flex-shrink: 0; - cursor: link; - transition-property: background-color, border-color, color; - transition-duration: 0.15s; - transition-timing-function: ease-in-out; -} - -.auth-back-btn:hover { - background-color: rgb(124, 58, 237); - border-top-color: rgba(167, 139, 250, 0.60); - color: rgba(255, 255, 255, 1); -} - -.auth-back-btn:active { - scale: 0.90; -} - -.auth-page-heading { - font-size: 24px; - color: rgb(238, 238, 246); - -unity-font-style: bold; - letter-spacing: -0.3px; -} - -.auth-page-heading--standalone { - margin-bottom: 24px; -} - -/* Heading + small subtitle stacked next to the back arrow */ -.auth-page-heading-wrap { - flex-direction: column; -} - -.auth-page-subheading { - font-size: 12px; - color: rgba(238, 238, 246, 0.35); - margin-top: 2px; -} - -/* ═══════════════════════════════════════════════════════════════════════ - BUTTONS - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-btn { - flex-direction: row; - align-items: center; - border-top-left-radius: 14px; - border-top-right-radius: 14px; - border-bottom-left-radius: 14px; - border-bottom-right-radius: 14px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - padding-top: 0; - padding-bottom: 0; - padding-left: 0; - padding-right: 0; - margin-bottom: 12px; - cursor: link; - transition-property: scale, background-color, border-color; - transition-duration: 0.18s; - transition-timing-function: ease-in-out; -} - -/* ── Primary button (Email) ───────────────────────────────────────────── */ - -.auth-btn--primary { - height: 66px; - background-color: rgb(109, 40, 217); - border-top-color: rgba(167, 139, 250, 0.45); - border-right-color: rgba(167, 139, 250, 0.20); - border-left-color: rgba(167, 139, 250, 0.20); - border-bottom-color:rgba(75, 25, 160, 0.70); -} - -.auth-btn--primary:hover { - background-color: rgb(124, 58, 237); - border-top-color: rgba(167, 139, 250, 0.60); - border-right-color: rgba(167, 139, 250, 0.35); - border-left-color: rgba(167, 139, 250, 0.35); - scale: 1.015; -} - -.auth-btn--primary:active { - background-color: rgb(91, 33, 182); - scale: 0.985; -} - -/* ── Wallet buttons ───────────────────────────────────────────────────── */ - -.auth-btn--wallet { - height: 64px; - background-color: rgb(20, 20, 30); - border-top-width: 0; - border-right-width: 0; - border-bottom-width: 0; - border-left-width: 0; -} - -.auth-btn--wallet:hover { - background-color: rgb(28, 28, 42); - scale: 1.015; -} - -.auth-btn--wallet:active { - background-color: rgb(16, 16, 24); - scale: 0.985; -} - -.auth-btn:disabled { - opacity: 0.45; -} - -/* ── Button icon area ─────────────────────────────────────────────────── */ - -.auth-btn-icon { - width: 46px; - height: 46px; - border-top-left-radius: 13px; - border-top-right-radius: 13px; - border-bottom-left-radius: 13px; - border-bottom-right-radius: 13px; - align-items: center; - justify-content: center; - flex-shrink: 0; - margin-left: 10px; -} - -.auth-btn-icon-glyph { - width: 100%; - height: 100%; - font-size: 18px; - -unity-text-align: middle-center; - -unity-font-style: bold; - color: rgba(238, 238, 246, 0.45); -} - -/* ── Provider icon colors ─────────────────────────────────────────────── */ - -.auth-btn-icon--email { - /* dark inset chip — the old faint purple vanished against the purple button */ - background-color: rgba(0, 0, 0, 0.30); -} - -.auth-btn-icon--email .auth-btn-icon-glyph { - color: rgb(255, 255, 255); - font-size: 22px; - translate: 1px -3px; /* optical-centre the @ — the font seats it low + slightly left */ -} - -/* Unified wallet entry (options page) — generic wallet mark, not one brand */ -.auth-btn-icon--wallets { - background-image: url("generic-wallet-icon.png"); - -unity-background-scale-mode: scale-to-fit; - background-size: 100% 100%; - background-position: center; -} - -.auth-btn-icon--wallets .auth-btn-icon-glyph { - display: none; -} - -.auth-btn-icon--pera { - background-image: url("pera-logo.png"); - -unity-background-scale-mode: scale-to-fit; - background-size: 100% 100%; - background-position: center; -} - -/* Curated EVM wallet logos (native/editor rows) — full-bleed square brand tiles - in the rounded 40px chip. */ -.wallet-logo--metamask { background-image: url("metamask-logo.png"); background-size: 100% 100%; } -.wallet-logo--coinbase { background-image: url("coinbase-logo.png"); background-size: 100% 100%; } -.wallet-logo--rainbow { background-image: url("rainbow-logo.png"); background-size: 100% 100%; } -.wallet-logo--trust { background-image: url("trust-logo.png"); background-size: 100% 100%; } - -.auth-btn-icon--pera .auth-btn-icon-glyph { - display: none; -} - -.auth-btn-icon--defly { - background-image: url("defly-logo.png"); - -unity-background-scale-mode: scale-to-fit; - background-size: 100% 100%; - background-position: center; -} - -.auth-btn-icon--defly .auth-btn-icon-glyph { - display: none; -} - -/* Primary button overrides (after provider colors for specificity) */ -.auth-btn--primary .auth-btn-icon-glyph { - color: rgba(255, 255, 255, 0.90); - font-size: 22px; -} - -/* ── Button body (two-line labels) ────────────────────────────────────── */ - -.auth-btn-body { - flex-direction: column; - flex-grow: 1; - align-items: flex-start; - justify-content: center; - padding-left: 16px; -} - -.auth-btn-body .auth-btn-label { - padding-left: 0; - flex-grow: 0; - margin-bottom: 3px; - -unity-text-align: upper-left; -} - -/* ── Button label ─────────────────────────────────────────────────────── */ - -.auth-btn-label { - flex-grow: 1; - font-size: 16px; - color: rgba(238, 238, 246, 0.88); - -unity-text-align: middle-left; - padding-left: 16px; -} - -.auth-btn--primary .auth-btn-label { - color: rgb(255, 255, 255); - font-size: 17px; - -unity-font-style: bold; -} - -.auth-btn-sub { - font-size: 12px; - color: rgba(238, 238, 246, 0.32); -} - -/* ── Button chevron ───────────────────────────────────────────────────── */ - -.auth-btn-chevron { - width: 52px; - align-self: stretch; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -/* Chevron geometry lives in BlockmakerTheme.uss (.auth-chevron). Idle stroke - is the shared 0.10; these lift the border colours on hover. */ -.auth-btn--primary:hover .auth-chevron { - border-right-color: rgba(255, 255, 255, 0.50); - border-bottom-color: rgba(255, 255, 255, 0.50); -} - -.auth-btn--wallet:hover .auth-chevron { - border-right-color: rgba(167, 139, 250, 0.45); - border-bottom-color: rgba(167, 139, 250, 0.45); - translate: 2px 0; /* tactile lean-in */ -} - -.auth-back-btn:hover .auth-chevron { - border-right-color: rgb(255, 255, 255); - border-bottom-color: rgb(255, 255, 255); -} - -/* White chevron on the purple back button (matches the primary email button). */ -.auth-back-btn .auth-chevron { - border-right-color: rgba(255, 255, 255, 0.90); - border-bottom-color: rgba(255, 255, 255, 0.90); -} - -/* ═══════════════════════════════════════════════════════════════════════ - DIVIDER - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-divider { - flex-direction: row; - align-items: center; - margin-top: 10px; - margin-bottom: 20px; -} - -.auth-divider-line { - flex-grow: 1; - height: 1px; - background-color: rgba(255, 255, 255, 0.06); -} - -.auth-divider-text { - font-size: 11px; - color: rgba(238, 238, 246, 0.20); - margin-left: 18px; - margin-right: 18px; - letter-spacing: 1.5px; -} - -/* ═══════════════════════════════════════════════════════════════════════ - OTP STEPS - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-step { - flex-direction: column; - display: none; -} - -.auth-field-label { - font-size: 11px; - color: rgba(238, 238, 246, 0.32); - margin-bottom: 10px; - letter-spacing: 1.5px; -} - -.auth-sent-hint { - font-size: 14px; - color: rgba(238, 238, 246, 0.42); - margin-bottom: 24px; -} - -/* ── Text fields ──────────────────────────────────────────────────────── */ - -.auth-input { - height: 56px; - background-color: rgba(0, 0, 0, 0); - border-top-width: 0; - border-right-width: 0; - border-bottom-width: 0; - border-left-width: 0; - margin-bottom: 20px; - color: rgb(238, 238, 246); - font-size: 16px; - padding-left: 0; - padding-right: 0; -} - -.auth-input > .unity-base-text-field__input { - background-color: rgb(20, 20, 30); - color: rgb(238, 238, 246); - border-top-width: 0; - border-left-width: 0; - border-right-width: 0; - border-bottom-width: 0; - border-top-left-radius: 12px; - border-top-right-radius: 12px; - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; - padding-left: 18px; - padding-right: 18px; - padding-top: 4px; - padding-bottom: 4px; - font-size: 16px; - --unity-cursor-color: rgb(167, 139, 250); - transition-property: border-color, background-color; - transition-duration: 0.2s; -} - -.auth-input > .unity-base-text-field__input:focus { - background-color: rgb(16, 16, 26); -} - -.auth-input--code > .unity-base-text-field__input { - font-size: 36px; - letter-spacing: 16px; - -unity-text-align: middle-center; - color: rgb(167, 139, 250); -} - -/* ── CTA button ───────────────────────────────────────────────────────── */ - -.auth-cta-btn { - height: 60px; - background-color: rgb(109, 40, 217); - border-top-left-radius: 14px; - border-top-right-radius: 14px; - border-bottom-left-radius: 14px; - border-bottom-right-radius: 14px; - border-top-width: 1px; - border-right-width: 0; - border-bottom-width: 0; - border-left-width: 0; - border-top-color: rgba(167, 139, 250, 0.35); - color: rgb(255, 255, 255); - font-size: 17px; - -unity-font-style: bold; - -unity-text-align: middle-center; - align-self: stretch; - margin-bottom: 0; - cursor: link; - transition-property: scale, background-color; - transition-duration: 0.18s; - transition-timing-function: ease-in-out; -} - -.auth-cta-btn:hover { - background-color: rgb(124, 58, 237); - scale: 1.015; -} - -.auth-cta-btn:active { - background-color: rgb(91, 33, 182); - scale: 0.985; -} - -/* ── Link button ──────────────────────────────────────────────────────── */ - -.auth-link-btn { - height: 44px; - background-color: rgba(0, 0, 0, 0); - border-top-width: 0; - border-right-width: 0; - border-bottom-width: 0; - border-left-width: 0; - color: rgba(238, 238, 246, 0.28); - font-size: 13px; - -unity-text-align: middle-center; - cursor: link; - transition-property: color; - transition-duration: 0.15s; - transition-timing-function: ease-in-out; -} - -.auth-link-btn:hover { - color: rgba(238, 238, 246, 0.60); -} - -/* ═══════════════════════════════════════════════════════════════════════ - QR CONNECT PAGE - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-qr-outer { - align-self: center; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 6px; - padding-right: 6px; - border-top-left-radius: 20px; - border-top-right-radius: 20px; - border-bottom-left-radius: 20px; - border-bottom-right-radius: 20px; - background-color: rgba(124, 58, 237, 0.06); - border-top-width: 2px; - border-right-width: 2px; - border-bottom-width: 2px; - border-left-width: 2px; - border-color: rgba(167, 139, 250, 0.16); - margin-bottom: 28px; -} - -.auth-qr-frame { - width: 360px; - height: 360px; - align-items: center; - justify-content: center; - border-top-left-radius: 14px; - border-top-right-radius: 14px; - border-bottom-left-radius: 14px; - border-bottom-right-radius: 14px; - overflow: hidden; -} - -.auth-qr-image { - width: 348px; - height: 348px; - background-color: rgb(255, 255, 255); - background-size: contain; - flex-shrink: 0; - align-self: center; - display: none; -} - -.auth-qr-loading-wrap { - width: 276px; - height: 276px; - background-color: rgba(255, 255, 255, 0.02); - border-top-left-radius: 10px; - border-top-right-radius: 10px; - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-color: rgba(255, 255, 255, 0.05); - align-items: center; - justify-content: center; - flex-direction: column; - align-self: center; -} - -.auth-qr-spinner { - width: 48px; - height: 48px; - border-top-left-radius: 24px; - border-top-right-radius: 24px; - border-bottom-left-radius: 24px; - border-bottom-right-radius: 24px; - border-top-width: 3px; - border-right-width: 3px; - border-bottom-width: 3px; - border-left-width: 3px; - border-top-color: rgb(124, 58, 237); - border-right-color: rgba(124, 58, 237, 0.10); - border-bottom-color: rgba(124, 58, 237, 0.10); - border-left-color: rgba(124, 58, 237, 0.10); - margin-bottom: 16px; -} - -.auth-qr-loading-text { - font-size: 13px; - color: rgba(238, 238, 246, 0.32); - -unity-text-align: middle-center; -} - -.auth-qr-hint { - font-size: 14px; - color: rgba(238, 238, 246, 0.35); - -unity-text-align: middle-center; - margin-bottom: 18px; - padding-left: 16px; - padding-right: 16px; -} - -.auth-qr-steps-hint { - font-size: 13px; - color: rgba(167, 139, 250, 0.80); - -unity-text-align: middle-center; - white-space: normal; - margin-top: -6px; - margin-bottom: 18px; - padding-left: 16px; - padding-right: 16px; -} - -/* ═══════════════════════════════════════════════════════════════════════ - STEP 2 OF 2 — waiting for the wallet sign-in approval - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-step2-badge { - align-self: center; - font-size: 12px; - letter-spacing: 3px; - -unity-font-style: bold; - -unity-text-align: middle-center; - color: rgb(167, 139, 250); - background-color: rgba(124, 58, 237, 0.14); - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-color: rgba(167, 139, 250, 0.35); - border-top-left-radius: 14px; - border-top-right-radius: 14px; - border-bottom-left-radius: 14px; - border-bottom-right-radius: 14px; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 16px; - padding-right: 16px; - margin-top: 6px; - margin-bottom: 22px; -} - -.auth-step2-title { - font-size: 30px; - -unity-font-style: bold; - color: rgb(238, 238, 246); - -unity-text-align: middle-center; - letter-spacing: -0.5px; - margin-bottom: 14px; -} - -.auth-step2-body { - font-size: 16px; - color: rgba(238, 238, 246, 0.85); - white-space: normal; - -unity-text-align: middle-center; - max-width: 340px; - margin-bottom: 26px; -} - -.auth-step2-dots { - flex-direction: row; - justify-content: center; - align-items: center; - height: 24px; - margin-bottom: 20px; -} - -.auth-step2-dot { - width: 12px; - height: 12px; - border-top-left-radius: 6px; - border-top-right-radius: 6px; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - margin-left: 6px; - margin-right: 6px; - background-color: rgba(167, 139, 250, 0.18); - transition-property: background-color, scale; - transition-duration: 0.25s; - transition-timing-function: ease-in-out; -} - -.auth-step2-dot--on { - background-color: rgb(167, 139, 250); - scale: 1.4; -} - -.auth-step2-hint { - font-size: 13px; - color: rgba(238, 238, 246, 0.40); - white-space: normal; - -unity-text-align: middle-center; -} - -/* ── Step 2 recovery controls (resend / cancel) ───────────────────────── */ - -.auth-step2-resend-hint { - font-size: 12px; - color: rgba(238, 238, 246, 0.30); - white-space: normal; - -unity-text-align: middle-center; - margin-top: 20px; - margin-bottom: 12px; -} - -.auth-step2-resend-btn { - height: 48px; - min-width: 260px; - background-color: rgba(124, 58, 237, 0.10); - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-color: rgba(167, 139, 250, 0.35); - border-top-left-radius: 12px; - border-top-right-radius: 12px; - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; - color: rgb(167, 139, 250); - font-size: 14px; - -unity-font-style: bold; - -unity-text-align: middle-center; - padding-left: 24px; - padding-right: 24px; - padding-top: 0; - padding-bottom: 0; - cursor: link; - transition-property: background-color, border-color, color; - transition-duration: 0.15s; - transition-timing-function: ease-in-out; -} - -.auth-step2-resend-btn:hover { - background-color: rgba(124, 58, 237, 0.18); - border-color: rgba(167, 139, 250, 0.55); - color: rgb(196, 181, 253); -} - -.auth-step2-resend-btn:active { - background-color: rgba(124, 58, 237, 0.06); -} - -.auth-step2-resend-btn:disabled { - opacity: 0.5; -} - -.auth-step2-cancel-btn { - margin-top: 6px; - min-width: 260px; - height: 36px; -} - -/* ── Copy-link text button ────────────────────────────────────────────── */ - -.auth-pill-btn { - background-color: rgba(0, 0, 0, 0); - border-top-width: 0; - border-right-width: 0; - border-bottom-width: 0; - border-left-width: 0; - color: rgba(167, 139, 250, 0.55); - font-size: 12px; - -unity-font-style: normal; - -unity-text-align: middle-center; - padding-left: 4px; - padding-right: 4px; - padding-top: 4px; - padding-bottom: 4px; - cursor: link; - display: none; - transition-property: color; - transition-duration: 0.15s; - transition-timing-function: ease-in-out; -} - -.auth-pill-btn:hover { - color: rgba(167, 139, 250, 0.85); -} - -.auth-pill-btn:active { - color: rgba(167, 139, 250, 0.45); -} - -/* ═══════════════════════════════════════════════════════════════════════ - UNIFIED WALLET PICKER (page-evm-wallets) - ═══════════════════════════════════════════════════════════════════════ */ - -/* The two panes are hidden by default; the controller flips exactly one to - flex: the LIST pane (Pera/Defly + the EVM portion) or CONNECTING+error. */ -.evm-state { - flex-direction: column; - display: none; -} - -.evm-state--center { - align-items: center; - padding-top: 10px; - padding-bottom: 6px; -} - -/* Hidden by default — the controller shows the "none found" row only when - discovery came back empty. */ -.evm-substate { - display: none; -} - -/* ── "None found" row (quiet) ─────────────────────────────────────────── */ - -.evm-none-row { - flex-direction: row; - align-items: center; - justify-content: space-between; - height: 48px; - padding-left: 14px; - padding-right: 6px; -} - -.evm-none-text { - font-size: 13px; - color: rgba(238, 238, 246, 0.32); - -unity-text-align: middle-left; -} - -.evm-find-link { - height: 36px; - flex-shrink: 0; - color: rgba(167, 139, 250, 0.60); -} - -.evm-find-link:hover { - color: rgba(167, 139, 250, 0.90); -} - -/* ── Unified wallet list ──────────────────────────────────────────────── */ - -/* Caps at 5 rows + half of the 6th (70px each incl. margin) — the cut row - is the scroll affordance; longer lists scroll. */ -.evm-wallet-list { - max-height: 385px; - flex-grow: 0; -} - -/* Bottom "more below" fade — transparent → card colour. Overlays the list's - lower edge; picking-mode Ignore in UXML keeps it from eating row clicks. - Controller toggles display: shown only when the list overflows. */ -.evm-list-fade { - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 52px; - background-image: url("list-fade.png"); - -unity-background-scale-mode: stretch-to-fill; -} - -/* ONE row class for every wallet — Pera, Defly, discovered EVM, curated EVM, - legacy browser. opacity/translate drive the --enter pop-in; scale/colors - keep the auth-btn hover/press feel. */ -.wallet-row { - height: 60px; - margin-bottom: 10px; - transition-property: opacity, translate, scale, background-color, border-color; - transition-duration: 0.16s, 0.16s, 0.18s, 0.18s, 0.18s; - transition-timing-function: ease-out, ease-out, ease-in-out, ease-in-out, ease-in-out; -} - -/* Rows with a second line need more height */ -.wallet-row--two-line { - height: 64px; -} - -/* Pop-in start state for rows added after discovery (never on Pera/Defly) */ -.wallet-row--enter { - opacity: 0; - translate: 0 6px; -} - -.wallet-row-icon { - width: 40px; - height: 40px; - border-top-left-radius: 11px; - border-top-right-radius: 11px; - border-bottom-left-radius: 11px; - border-bottom-right-radius: 11px; - margin-left: 12px; - overflow: hidden; - -unity-background-scale-mode: scale-to-fit; - background-size: 100% 100%; - background-position: center; -} - -.wallet-row-glyph { - font-size: 16px; - color: rgba(255, 255, 255, 0.90); -} - -.wallet-row-name { - font-size: 15px; - color: rgba(238, 238, 246, 0.88); - padding-left: 14px; - flex-grow: 1; -} - -.wallet-row .auth-btn-chevron { - width: 48px; -} - -/* ── Discovery skeleton (one ghost row while DiscoverEvmWallets runs) ──── */ - -.wallet-skeleton { - height: 60px; - margin-bottom: 10px; - flex-direction: row; - align-items: center; - background-color: rgba(255, 255, 255, 0.03); - border-top-left-radius: 14px; - border-top-right-radius: 14px; - border-bottom-left-radius: 14px; - border-bottom-right-radius: 14px; - opacity: 1; - transition-property: opacity; - transition-duration: 0.6s; - transition-timing-function: ease-in-out; -} - -/* Toggled by the controller every 600ms — the transition does the pulse */ -.wallet-skeleton--pulse { - opacity: 0.55; -} - -.wallet-skeleton-chip { - width: 40px; - height: 40px; - border-top-left-radius: 11px; - border-top-right-radius: 11px; - border-bottom-left-radius: 11px; - border-bottom-right-radius: 11px; - margin-left: 12px; - background-color: rgba(255, 255, 255, 0.05); - flex-shrink: 0; -} - -.wallet-skeleton-bar { - width: 96px; - height: 12px; - border-top-left-radius: 6px; - border-top-right-radius: 6px; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - margin-left: 14px; - background-color: rgba(255, 255, 255, 0.05); -} - -/* ── Native caption (curated rows connect via QR) ─────────────────────── */ - -.evm-native-caption { - font-size: 11px; - color: rgba(238, 238, 246, 0.28); - -unity-text-align: middle-center; - white-space: normal; - margin-top: 2px; -} - -/* ── Badges (one slot per row, between name and chevron) ──────────────── */ - -.evm-badge { - font-size: 10px; - letter-spacing: 1px; - -unity-font-style: bold; - -unity-text-align: middle-center; - color: rgb(167, 139, 250); - background-color: rgba(124, 58, 237, 0.14); - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-color: rgba(167, 139, 250, 0.35); - border-top-left-radius: 9px; - border-top-right-radius: 9px; - border-bottom-left-radius: 9px; - border-bottom-right-radius: 9px; - padding-top: 3px; - padding-bottom: 3px; - padding-left: 9px; - padding-right: 9px; - margin-right: 8px; - flex-shrink: 0; - align-self: center; -} - -/* ── Connecting pane (one wallet) ─────────────────────────────────────── */ - -.evm-connect-icon { - width: 64px; - height: 64px; - border-top-left-radius: 16px; - border-top-right-radius: 16px; - border-bottom-left-radius: 16px; - border-bottom-right-radius: 16px; - background-color: rgb(20, 20, 30); - align-items: center; - justify-content: center; - overflow: hidden; - margin-top: 4px; - margin-bottom: 22px; - -unity-background-scale-mode: scale-to-fit; - background-size: 100% 100%; - background-position: center; -} - -.evm-connect-glyph { - font-size: 26px; - -unity-font-style: bold; - -unity-text-align: middle-center; - color: rgba(255, 255, 255, 0.90); -} - -.evm-connect-title { - font-size: 24px; - -unity-font-style: bold; - color: rgb(238, 238, 246); - -unity-text-align: middle-center; - letter-spacing: -0.3px; - margin-bottom: 12px; -} - -.evm-connect-body { - font-size: 15px; - color: rgba(238, 238, 246, 0.85); - white-space: normal; - -unity-text-align: middle-center; - max-width: 340px; - margin-bottom: 18px; -} - -.evm-connect-body--error { - color: rgb(248, 113, 113); -} - -.evm-retry-btn { - margin-top: 4px; -} - -/* ═══════════════════════════════════════════════════════════════════════ - STATUS - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-status { - font-size: 13px; - color: rgba(238, 238, 246, 0.50); - white-space: normal; - margin-top: 4px; - margin-bottom: 20px; - margin-left: 40px; - margin-right: 40px; - display: none; - -unity-text-align: middle-center; -} - -.auth-status--error { - color: rgb(248, 113, 113); - display: flex; -} - -/* ═══════════════════════════════════════════════════════════════════════ - WALLET ADDRESS CHANGE WARNING - ═══════════════════════════════════════════════════════════════════════ */ - -.auth-wallet-warning { - display: none; - margin-top: 8px; - margin-bottom: 8px; - margin-left: 24px; - margin-right: 24px; - padding-top: 12px; - padding-bottom: 12px; - padding-left: 16px; - padding-right: 16px; - background-color: rgba(251, 191, 36, 0.10); - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-color: rgba(251, 191, 36, 0.30); - border-top-left-radius: 8px; - border-top-right-radius: 8px; - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; -} - -.auth-wallet-warning-text { - font-size: 12px; - color: rgba(251, 191, 36, 0.95); - white-space: normal; - -unity-text-align: upper-left; -} diff --git a/UI/AuthPrompt.uss.meta b/UI/AuthPrompt.uss.meta deleted file mode 100644 index 623743d..0000000 --- a/UI/AuthPrompt.uss.meta +++ /dev/null @@ -1,12 +0,0 @@ -fileFormatVersion: 2 -guid: 9e91f037f34dc4e8b9d7f7d51d9449e1 -ScriptedImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 2 - userData: - assetBundleName: - assetBundleVariant: - script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} - disableValidation: 0 - unsupportedSelectorAction: 0 diff --git a/UI/AuthPrompt.uxml b/UI/AuthPrompt.uxml deleted file mode 100644 index d3c7e72..0000000 --- a/UI/AuthPrompt.uxml +++ /dev/null @@ -1,176 +0,0 @@ - -