@authon/react-native-wallet 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +34 -4
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
  5. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
  6. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +16 -8
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
  21. package/build/client.d.ts +18 -2
  22. package/build/client.d.ts.map +1 -1
  23. package/build/client.js +88 -6
  24. package/build/client.js.map +1 -1
  25. package/build/errors.d.ts +8 -1
  26. package/build/errors.d.ts.map +1 -1
  27. package/build/errors.js +7 -1
  28. package/build/errors.js.map +1 -1
  29. package/build/index.d.ts +1 -1
  30. package/build/index.d.ts.map +1 -1
  31. package/build/native.d.ts +5 -1
  32. package/build/native.d.ts.map +1 -1
  33. package/build/native.js.map +1 -1
  34. package/build/types.d.ts +47 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/ios/AuthonReactNativeWallet.podspec +6 -2
  37. package/ios/AuthonWalletModule.swift +46 -3
  38. package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
  39. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  40. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  41. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  42. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  43. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  44. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  45. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
  46. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
  47. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
  48. package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
  49. package/ios/vendor/CKeccak/README.md +6 -3
  50. package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
  51. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  52. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  53. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  54. package/ios/vendor/CMonocypher/README.md +27 -0
  55. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  56. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  57. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  58. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  59. package/ios/vendor/GENERATED.md +1 -1
  60. package/package.json +3 -2
  61. package/src/client.ts +90 -6
  62. package/src/errors.ts +7 -1
  63. package/src/index.ts +5 -0
  64. package/src/native.ts +4 -2
  65. package/src/types.ts +52 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # @authon/react-native-wallet
2
+
3
+ ## 0.4.0
4
+
5
+ - `signChainTransaction({ chain, account, address, payload })` for app-built transactions on other chains (NATIVE.md section 12): Solana (`{ message }` → `{ signature }`), TRON (`{ rawDataHex }` → `{ txID, signature }`), Bitcoin and Bitcoin testnet (`{ psbt }` → `{ psbt, txHex? }`) and XRP (`{ txBlobHex }` → `{ signature }`). EVM stays on `signTransaction` (`INVALID_PARAMS` for `chain: 'evm'`). The native approval UI summarizes the transaction before the key is assembled.
6
+ - `getAccounts` passes through `publicKey` when the native wallet returns one (XRP).
7
+
8
+ ## 0.3.0
9
+
10
+ - `getAccounts({ start, count, chain })`: account addresses on `evm` (default), `tron`, `xrp`, `bitcoin`, `bitcoin-testnet` and `solana`, derived from the same seed with the standard paths. Each account carries its `chain`; a native wallet that answers without it is refused (`UNSUPPORTED_TYPE`). Signing stays EVM only.
11
+
12
+ ## 0.2.0
13
+
14
+ - `capabilities()`: checks before sign-in whether this device and app can use the wallet (app registration and approval, wallet enabled, hardware key storage, device lock, passkey backup support), with reason codes and the app-config `diagnosticId`. No session needed.
15
+ - `AuthonWalletError` has `diagnosticId` (server request ID) and `retryable`, passed through from the Swift and Kotlin wallets.
16
+ - iOS: a second signature right after the first no longer hangs; the approval sheet fails with `CONFIRM_WINDOW_BLOCKED` when it cannot be shown.
17
+
18
+ ## 0.1.0
19
+
20
+ - First release: Expo module over the Swift and Kotlin embedded wallets (status, create/ensure, accounts, personal_sign, EIP-712, transactions, passkey protect/upgrade/restore, device revoke), `useAuthonWallet()` with `@authon/react-native`.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  > (`AuthonWallet`) and Kotlin (`dev.authon.sdk.wallet`) wallet implementations.
5
5
 
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue)](../../LICENSE)
7
- [![Version](https://img.shields.io/badge/version-0.1.0-blue)]()
7
+ [![Version](https://img.shields.io/badge/version-0.4.0-blue)]()
8
8
 
9
9
  It is the same wallet as the web SDK (`@authon/js`): same wallet row, same `/v1/wallet/me/*` API,
10
10
  same shares. A user gets the same address in your web app and your mobile app, and the app install
@@ -27,8 +27,12 @@ appears as one more device in the wallet's device list.
27
27
  - iOS 16+ (iOS 18+ for passkey backup and restore, which need the passkey PRF extension).
28
28
  - Android minSdk 28, compileSdk 35+. Passkey backup and restore need Google Play services with a
29
29
  passkey provider that supports PRF (Google Password Manager on Android 9+ with current Play services).
30
- - EVM chains: personal_sign, EIP-712 v4, legacy / EIP-2930 / EIP-1559 transactions, HD accounts
30
+ - Signing: EVM (personal_sign, EIP-712 v4, legacy / EIP-2930 / EIP-1559 transactions), HD accounts
31
31
  `m/44'/60'/0'/0/n`.
32
+ - Addresses (receive / display) from the same seed: TRON `m/44'/195'/n'/0/0`, XRP `m/44'/144'/n'/0/0`,
33
+ Bitcoin P2WPKH `m/84'/0'/n'/0/0` (testnet `m/84'/1'/n'/0/0`), Solana `m/44'/501'/n'/0'`. These
34
+ chains sign through `signChainTransaction` (the app builds the transaction; fees, balances and broadcast are up to the app). A wallet created before HD wallets has EVM account 0 only
35
+ (`ACCOUNT_UNAVAILABLE` for other chains).
32
36
 
33
37
  ## Install
34
38
 
@@ -128,14 +132,16 @@ Method names and result shapes follow `@authon/js` (`WalletClient`).
128
132
 
129
133
  | Method | Description |
130
134
  |---|---|
135
+ | `capabilities()` | Pre-sign-in check of this device and app (see below). No session, creates nothing, opens no UI. |
131
136
  | `appConfig()` | App registration and wallet settings: `{ projectId, allowed, approved, enabled, chainIds, confirmSigning, rpId, minFrameProtocol }`. No session needed. |
132
137
  | `get()` / `status()` | The signed-in user's wallet `{ address, protected, needsRestore, backupUpgradeRecommended, devices }`, or `null` when there is none. |
133
138
  | `create()` | Creates the wallet (or returns the existing one). |
134
139
  | `ensure()` | `get()`, then `create()` when there is no wallet. Concurrent calls share one request. |
135
140
  | `isProtected()` | True when the wallet has a passkey backup. |
136
- | `getAccounts({ start, count })` | HD account addresses `{ index, path, address }` (public, no approval). |
141
+ | `getAccounts({ start, count, chain })` | HD account addresses `{ index, path, address, chain }` (public, no approval). `chain`: `evm` (default), `tron`, `xrp`, `bitcoin`, `bitcoin-testnet`, `solana`. |
137
142
  | `signMessage(message, { account })` | EIP-191 personal_sign; `message` is text or `{ raw: '0x...' }`. |
138
143
  | `signTypedData(typedData, { account })` | EIP-712 v4. |
144
+ | `signChainTransaction({ chain, account, address, payload })` | Signs an app-built transaction: Solana `{ message }` (base64 Message) → `{ signature }` (base58); TRON `{ rawDataHex }` → `{ txID, signature }`; Bitcoin / testnet `{ psbt }` (base64, every input with witness_utxo; only our P2WPKH inputs are signed) → `{ psbt, txHex? }` (txHex when every input is ours); XRP `{ txBlobHex }` (encodeForSigning output with SigningPubKey = `getAccounts({ chain: 'xrp' })` `publicKey`) → `{ signature }` (DER). The approval UI summarizes it; unknown programs are shown as high risk. `address` must be the account's address on that chain (`ADDRESS_MISMATCH` otherwise). |
139
145
  | `signTransaction(tx, { account })` | Legacy, EIP-2930 or EIP-1559; returns the serialized signed transaction. `bigint` values are allowed. |
140
146
  | `protect()` | Creates the passkey backup. Do not let users receive funds before the wallet is protected. |
141
147
  | `upgradeBackup()` | Replaces a legacy backup (`backupUpgradeRecommended`) with a verifiable one. |
@@ -159,14 +165,38 @@ Options of `createAuthonWallet` / `useAuthonWallet`:
159
165
  | `requireDeviceAuthentication` | Also ask for Face ID / Touch ID / passcode (iOS) or BiometricPrompt (Android) before each signature. |
160
166
  | `testing` | **Test only**, refused outside debug builds: `{ allowSoftwareKeys, autoApprove }` for simulators and emulators (software device keys instead of Secure Enclave / StrongBox, and no approval UI). Never ship it. |
161
167
 
168
+ ### Before sign-in: `capabilities()`
169
+
170
+ ```ts
171
+ const caps = await wallet.capabilities();
172
+ if (caps.ready === 'unavailable') showWalletUnavailable(caps); // tell the user before sign-in
173
+ // 'unknown': continue, but be ready for that feature to fail
174
+ ```
175
+
176
+ `{ ready, app, wallet, secureStorage, deviceLock, passkeyBackup, diagnosticId }`; each check is
177
+ `{ status: 'available' | 'unavailable' | 'unknown', reason }`. `ready` is `unavailable` if any check is,
178
+ else `unknown` if any is, else `available`. It answers for a release build (the `testing` options are
179
+ ignored).
180
+
181
+ | Check | unavailable reason | unknown reason |
182
+ |---|---|---|
183
+ | `app` | `APP_NOT_REGISTERED`, `APP_SIGNATURE_MISMATCH` (Android: the running app's signing certificate differs from the registered one) | `APP_NOT_APPROVED` (signing works, passkey backup waits for approval), `NETWORK_ERROR`, `API_ERROR` |
184
+ | `wallet` | `WALLET_DISABLED` | `NETWORK_ERROR` |
185
+ | `secureStorage` | `HARDWARE_KEYSTORE_UNAVAILABLE` | `SIMULATOR` (simulators and emulators never count as hardware protected), `CHECK_FAILED` |
186
+ | `deviceLock` | `DEVICE_LOCK_NOT_SET` | `CHECK_FAILED` |
187
+ | `passkeyBackup` | `OS_UNSUPPORTED` (iOS < 18, Android < 9), `PASSKEY_API_UNAVAILABLE` | `PRF_PROVIDER_UNVERIFIED` (the provider's PRF support is confirmed on the first `protect()`) |
188
+
162
189
  ### Errors
163
190
 
164
- Every failure is an `AuthonWalletError` with a `code`. Codes are the web wallet's
191
+ Every failure is an `AuthonWalletError` with `code`, `message`, `diagnosticId` (the server's request ID
192
+ when an API response caused it; log it, never secrets) and `retryable`. Codes are the web wallet's
165
193
  (`NOT_FOUND`, `NEEDS_RESTORE`, `NOT_PROTECTED`, `PRF_UNSUPPORTED`, `ORIGIN_NOT_ALLOWED`,
166
194
  `USER_CANCELLED`, `UNAUTHORIZED`, `ADDRESS_MISMATCH`, `STORAGE_UNAVAILABLE`, `LAST_DEVICE`,
167
195
  `MESSAGE_TOO_LARGE`, `FRAME_OUTDATED`, `BACKUP_KEY_REQUIRED`, `ALREADY_UPGRADED`,
168
196
  `ACCOUNT_UNAVAILABLE`, ...), reported unchanged by the Swift and Kotlin wallets. The JS layer adds
169
197
  `NOT_SIGNED_IN` (no token), `INVALID_PARAMS`, `DISPOSED` and `NOT_CONFIGURED` (native module missing).
198
+ `USER_CANCELLED` means the user rejected the approval or closed the passkey sheet: go back without an
199
+ error message. `CONFIRM_WINDOW_BLOCKED` means the approval UI could not be shown; nothing was signed.
170
200
 
171
201
  ```ts
172
202
  try {
@@ -1,4 +1,8 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
  <uses-permission android:name="android.permission.INTERNET" />
3
3
  <uses-permission android:name="android.permission.USE_BIOMETRIC" />
4
+ <!-- capabilities(): checks whether Google Play services (passkey provider) is installed. -->
5
+ <queries>
6
+ <package android:name="com.google.android.gms" />
7
+ </queries>
4
8
  </manifest>
@@ -2,6 +2,9 @@ package dev.authon.reactnative.wallet
2
2
 
3
3
  import android.content.pm.ApplicationInfo
4
4
  import dev.authon.sdk.wallet.AuthonWallet
5
+ import dev.authon.sdk.wallet.CapabilityCheck
6
+ import dev.authon.sdk.wallet.WalletCapabilities
7
+ import dev.authon.sdk.wallet.WalletChain
5
8
  import dev.authon.sdk.wallet.PublicWallet
6
9
  import dev.authon.sdk.wallet.RestoreMode
7
10
  import dev.authon.sdk.wallet.SignAccount
@@ -15,6 +18,15 @@ import expo.modules.kotlin.functions.Coroutine
15
18
  import expo.modules.kotlin.modules.Module
16
19
  import expo.modules.kotlin.modules.ModuleDefinition
17
20
  import java.util.concurrent.ConcurrentHashMap
21
+ import kotlinx.serialization.json.Json
22
+ import kotlinx.serialization.json.JsonArray
23
+ import kotlinx.serialization.json.JsonElement
24
+ import kotlinx.serialization.json.JsonNull
25
+ import kotlinx.serialization.json.JsonObject
26
+ import kotlinx.serialization.json.JsonPrimitive
27
+ import kotlinx.serialization.json.booleanOrNull
28
+ import kotlinx.serialization.json.doubleOrNull
29
+ import kotlinx.serialization.json.longOrNull
18
30
  import kotlin.coroutines.AbstractCoroutineContextElement
19
31
  import kotlin.coroutines.CoroutineContext
20
32
  import kotlinx.coroutines.CancellationException
@@ -47,6 +59,10 @@ class AuthonWalletModule : Module() {
47
59
  call(id, null) { appConfig(it.appConfig()) }
48
60
  }
49
61
 
62
+ AsyncFunction("capabilities") Coroutine { id: String ->
63
+ call(id, null) { capabilities(it.capabilities()) }
64
+ }
65
+
50
66
  AsyncFunction("status") Coroutine { id: String, token: String ->
51
67
  call(id, token) { wallet(it.status()) }
52
68
  }
@@ -55,9 +71,11 @@ class AuthonWalletModule : Module() {
55
71
  call(id, token) { wallet(it.create()) }
56
72
  }
57
73
 
58
- AsyncFunction("accounts") Coroutine { id: String, token: String, start: Int, count: Int ->
74
+ AsyncFunction("accounts") Coroutine { id: String, token: String, start: Int, count: Int, chain: String ->
59
75
  call(id, token) { w ->
60
- w.accounts(start.toLong(), count).map { mapOf("index" to it.index.toInt(), "path" to it.path, "address" to it.address) }
76
+ w.accounts(start.toLong(), count, WalletChain.fromWire(chain)).map {
77
+ mapOf("index" to it.index.toInt(), "path" to it.path, "address" to it.address, "chain" to it.chain.wire, "publicKey" to it.publicKey)
78
+ }
61
79
  }
62
80
  }
63
81
 
@@ -76,6 +94,10 @@ class AuthonWalletModule : Module() {
76
94
  call(id, token) { it.signTransaction(transactionJson, SignAccount.parse(account.toLong(), address)) }
77
95
  }
78
96
 
97
+ AsyncFunction("signChainTransaction") Coroutine { id: String, token: String, chain: String, account: Int, address: String, payloadJson: String ->
98
+ call(id, token) { it.signChainTransaction(WalletChain.fromWire(chain), account.toLong(), address, parsePayload(payloadJson)) }
99
+ }
100
+
79
101
  AsyncFunction("protect") Coroutine { id: String, token: String ->
80
102
  call(id, token) { mapOf("shareVersion" to it.protect().shareVersion) }
81
103
  }
@@ -149,7 +171,26 @@ class AuthonWalletModule : Module() {
149
171
 
150
172
  private fun failure(e: Throwable): Map<String, Any?> {
151
173
  val w = e as? WalletException ?: WalletException(WalletErrorCode.INTERNAL, "unexpected error")
152
- return mapOf("ok" to false, "code" to w.code.name, "message" to (w.message ?: w.code.name))
174
+ return mapOf(
175
+ "ok" to false,
176
+ "code" to w.code.name,
177
+ "message" to (w.message ?: w.code.name),
178
+ "diagnosticId" to w.diagnosticId,
179
+ "retryable" to w.retryable,
180
+ )
181
+ }
182
+
183
+ private fun parsePayload(json: String): Map<String, Any?> {
184
+ val obj = runCatching { Json.parseToJsonElement(json) }.getOrNull() as? JsonObject
185
+ ?: throw WalletException(WalletErrorCode.INVALID_REQUEST, "payload must be a JSON object")
186
+ return obj.mapValues { (_, v) -> plain(v) }
187
+ }
188
+
189
+ private fun plain(e: JsonElement): Any? = when (e) {
190
+ is JsonNull -> null
191
+ is JsonObject -> e.mapValues { (_, v) -> plain(v) }
192
+ is JsonArray -> e.map { plain(it) }
193
+ is JsonPrimitive -> if (e.isString) e.content else e.booleanOrNull ?: e.longOrNull ?: e.doubleOrNull
153
194
  }
154
195
 
155
196
  // Public data only
@@ -164,6 +205,19 @@ class AuthonWalletModule : Module() {
164
205
  },
165
206
  )
166
207
 
208
+ private fun capabilities(c: WalletCapabilities): Map<String, Any?> {
209
+ fun check(k: CapabilityCheck) = mapOf("status" to k.status.wire, "reason" to k.reason)
210
+ return mapOf(
211
+ "ready" to c.ready.wire,
212
+ "app" to check(c.app),
213
+ "wallet" to check(c.wallet),
214
+ "secureStorage" to check(c.secureStorage),
215
+ "deviceLock" to check(c.deviceLock),
216
+ "passkeyBackup" to check(c.passkeyBackup),
217
+ "diagnosticId" to c.diagnosticId,
218
+ )
219
+ }
220
+
167
221
  private fun appConfig(c: WalletAppConfig): Map<String, Any?> = mapOf(
168
222
  "projectId" to c.projectId,
169
223
  "allowed" to c.allowed,
@@ -0,0 +1,86 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import android.app.KeyguardManager
5
+ import android.content.Context
6
+ import android.content.pm.PackageManager
7
+ import android.os.Build
8
+ import android.security.keystore.KeyGenParameterSpec
9
+ import android.security.keystore.KeyProperties
10
+ import kotlinx.coroutines.Dispatchers
11
+ import kotlinx.coroutines.withContext
12
+ import java.security.KeyPairGenerator
13
+ import java.security.KeyStore
14
+ import java.security.MessageDigest
15
+ import java.security.spec.ECGenParameterSpec
16
+ import java.util.UUID
17
+
18
+ /** Device checks for capabilities() on Android. Test-only software-key settings are ignored. */
19
+ class AndroidCapabilityProbe(context: Context) : CapabilityProbe {
20
+ private val appContext = context.applicationContext
21
+
22
+ override fun signingCertSha256(): String? {
23
+ if (Build.VERSION.SDK_INT < 28) return null
24
+ val info = appContext.packageManager.getPackageInfo(appContext.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
25
+ val cert = info.signingInfo?.apkContentsSigners?.firstOrNull() ?: return null
26
+ return fingerprint(cert.toByteArray())
27
+ }
28
+
29
+ override suspend fun secureStorage(): CapabilityCheck = withContext(Dispatchers.IO) {
30
+ if (isEmulator()) return@withContext CapabilityCheck.unknown(Capabilities.SIMULATOR)
31
+ val alias = "dev.authon.wallet.probe.${UUID.randomUUID()}"
32
+ val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
33
+ try {
34
+ val spec = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN)
35
+ .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
36
+ .setDigests(KeyProperties.DIGEST_SHA256)
37
+ .build()
38
+ val pair = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore").apply { initialize(spec) }.generateKeyPair()
39
+ when (KeystoreSecurity.levelOf(pair.private)) {
40
+ KeySecurityLevel.STRONGBOX, KeySecurityLevel.TRUSTED_ENVIRONMENT, KeySecurityLevel.UNKNOWN_SECURE -> CapabilityCheck.AVAILABLE
41
+ KeySecurityLevel.SOFTWARE -> CapabilityCheck.unavailable(Capabilities.HARDWARE_KEYSTORE_UNAVAILABLE)
42
+ KeySecurityLevel.UNKNOWN -> CapabilityCheck.unknown(Capabilities.CHECK_FAILED)
43
+ }
44
+ } catch (_: Exception) {
45
+ CapabilityCheck.unknown(Capabilities.CHECK_FAILED)
46
+ } finally {
47
+ runCatching { ks.deleteEntry(alias) }
48
+ }
49
+ }
50
+
51
+ override fun deviceLock(): CapabilityCheck {
52
+ val km = appContext.getSystemService(KeyguardManager::class.java) ?: return CapabilityCheck.unknown(Capabilities.CHECK_FAILED)
53
+ return if (km.isDeviceSecure) CapabilityCheck.AVAILABLE else CapabilityCheck.unavailable(Capabilities.DEVICE_LOCK_NOT_SET)
54
+ }
55
+
56
+ override fun passkeyBackup(): CapabilityCheck {
57
+ if (Build.VERSION.SDK_INT < 28) return CapabilityCheck.unavailable(Capabilities.OS_UNSUPPORTED)
58
+ // Android 14+ ships the Credential Manager framework service; older versions need Play services.
59
+ val framework = Build.VERSION.SDK_INT >= 34 && appContext.getSystemService("credential") != null
60
+ val playServices = try {
61
+ appContext.packageManager.getApplicationInfo("com.google.android.gms", 0).enabled
62
+ } catch (_: PackageManager.NameNotFoundException) {
63
+ false
64
+ }
65
+ return if (framework || playServices) {
66
+ CapabilityCheck.unknown(Capabilities.PRF_PROVIDER_UNVERIFIED)
67
+ } else {
68
+ CapabilityCheck.unavailable(Capabilities.PASSKEY_API_UNAVAILABLE)
69
+ }
70
+ }
71
+
72
+ companion object {
73
+ fun fingerprint(der: ByteArray): String =
74
+ MessageDigest.getInstance("SHA-256").digest(der).joinToString(":") { "%02X".format(it.toInt() and 0xff) }
75
+
76
+ /** Conservative emulator detection (goldfish/ranchu hardware, generic/emulator builds). */
77
+ fun isEmulator(): Boolean {
78
+ val hw = Build.HARDWARE.lowercase()
79
+ val fp = Build.FINGERPRINT.lowercase()
80
+ val product = Build.PRODUCT.lowercase()
81
+ return hw.contains("goldfish") || hw.contains("ranchu") || fp.startsWith("generic") ||
82
+ fp.contains("emulator") || fp.contains("/sdk_gphone") || product.startsWith("sdk_gphone") ||
83
+ product.contains("emulator") || product.contains("simulator")
84
+ }
85
+ }
86
+ }
@@ -44,6 +44,7 @@ class AuthonWallet internal constructor(
44
44
  private val passkeysFactory: (rpId: String) -> PasskeyProvider,
45
45
  private val approver: WalletApprover?,
46
46
  private val deviceAuthenticator: DeviceAuthenticator?,
47
+ private val probe: CapabilityProbe?,
47
48
  private val dispatcher: CoroutineContext = Dispatchers.Default,
48
49
  ) {
49
50
  /**
@@ -69,6 +70,7 @@ class AuthonWallet internal constructor(
69
70
  passkeysFactory = { rpId -> options.passkeys ?: CredentialManagerPasskeys(activityProvider, rpId) },
70
71
  approver = options.approver ?: DialogWalletApprover(activityProvider),
71
72
  deviceAuthenticator = if (options.requireDeviceAuthentication) BiometricDeviceAuthenticator(activityProvider) else null,
73
+ probe = AndroidCapabilityProbe(context),
72
74
  )
73
75
 
74
76
  private val mutex = Mutex()
@@ -77,6 +79,17 @@ class AuthonWallet internal constructor(
77
79
  /** This app's wallet configuration from the API (cached after the first success). */
78
80
  suspend fun appConfig(): WalletAppConfig = run { config ?: api.appConfig(packageName).also { config = it } }
79
81
 
82
+ /**
83
+ * Whether this device and app can use the wallet (NATIVE.md section 9). Needs no session,
84
+ * opens no login or passkey UI, creates nothing, and never throws.
85
+ */
86
+ suspend fun capabilities(): WalletCapabilities = withContext(dispatcher) {
87
+ val p = probe ?: throw IllegalStateException("no capability probe")
88
+ val (caps, cfg) = Capabilities.evaluate(api, packageName, p)
89
+ if (cfg != null) config = cfg
90
+ caps
91
+ }
92
+
80
93
  private class Ready(val ops: WalletOps, val session: String, val config: WalletAppConfig)
81
94
 
82
95
  private suspend fun ready(): Ready {
@@ -123,7 +136,9 @@ class AuthonWallet internal constructor(
123
136
  /** Creates the wallet (two-phase; resumes or discards an interrupted attempt), or returns the existing one. */
124
137
  suspend fun create(): PublicWallet = op { it.ops.toPublicWallet(it.ops.createWallet(it.session)) }
125
138
 
126
- suspend fun accounts(start: Long = 0, count: Int = 1): List<WalletAccount> = op { it.ops.accounts(it.session, start, count) }
139
+ /** Addresses of accounts start..start+count-1 on [chain] (NATIVE.md section 11). Signing is EVM only. */
140
+ suspend fun accounts(start: Long = 0, count: Int = 1, chain: WalletChain = WalletChain.EVM): List<WalletAccount> =
141
+ op { it.ops.accounts(it.session, start, count, chain) }
127
142
 
128
143
  suspend fun signMessage(message: String, account: SignAccount = SignAccount.PRIMARY): String =
129
144
  signMessage(WalletMessage.Text(message), account)
@@ -150,6 +165,17 @@ class AuthonWallet internal constructor(
150
165
  suspend fun signTransaction(transactionJson: String, account: SignAccount = SignAccount.PRIMARY): String =
151
166
  op { it.ops.signTransaction(it.session, parseObject(transactionJson), account) }
152
167
 
168
+ /**
169
+ * Signs an app-built transaction on another chain (NATIVE.md section 12). Solana: payload
170
+ * `message` (serialized legacy/v0 Message, base64, at most 1232 bytes) and optional `cluster`
171
+ * (shown as "Solana <cluster>"); returns `signature` (base58). The signer must be the fee payer,
172
+ * and [address] must be the chain address of [account] (from accounts()). TRON: payload
173
+ * `rawDataHex` (raw_data protobuf, at most 4096 bytes); returns `txID` and `signature`
174
+ * (r||s||v hex); the owner must be the signer.
175
+ */
176
+ suspend fun signChainTransaction(chain: WalletChain, account: Long, address: String, payload: Map<String, Any?>): Map<String, String> =
177
+ op { it.ops.signChainTransaction(it.session, chain, account, address, payload) }
178
+
153
179
  /** Creates the passkey backup (B) of an unprotected wallet. */
154
180
  suspend fun protect(): ProtectResult = op {
155
181
  requirePasskeyApproval(it.config)
@@ -186,6 +212,7 @@ class AuthonWallet internal constructor(
186
212
  passkeys: PasskeyProvider,
187
213
  approver: WalletApprover?,
188
214
  deviceAuthenticator: DeviceAuthenticator? = null,
189
- ) = AuthonWallet(api, packageName, sessionProvider, store, { passkeys }, approver, deviceAuthenticator)
215
+ probe: CapabilityProbe? = null,
216
+ ) = AuthonWallet(api, packageName, sessionProvider, store, { passkeys }, approver, deviceAuthenticator, probe)
190
217
  }
191
218
  }