@authon/react-native-wallet 0.1.0 → 0.5.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 (73) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +58 -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/appleSignIn.d.ts +26 -0
  22. package/build/appleSignIn.d.ts.map +1 -0
  23. package/build/appleSignIn.js +76 -0
  24. package/build/appleSignIn.js.map +1 -0
  25. package/build/client.d.ts +18 -2
  26. package/build/client.d.ts.map +1 -1
  27. package/build/client.js +88 -6
  28. package/build/client.js.map +1 -1
  29. package/build/errors.d.ts +8 -1
  30. package/build/errors.d.ts.map +1 -1
  31. package/build/errors.js +7 -1
  32. package/build/errors.js.map +1 -1
  33. package/build/index.d.ts +3 -1
  34. package/build/index.d.ts.map +1 -1
  35. package/build/index.js +1 -0
  36. package/build/index.js.map +1 -1
  37. package/build/native.d.ts +11 -1
  38. package/build/native.d.ts.map +1 -1
  39. package/build/native.js.map +1 -1
  40. package/build/types.d.ts +47 -1
  41. package/build/types.d.ts.map +1 -1
  42. package/ios/AuthonReactNativeWallet.podspec +9 -3
  43. package/ios/AuthonWalletModule.swift +72 -3
  44. package/ios/vendor/AuthonAppleSignIn/AppleSignIn.swift +285 -0
  45. package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
  46. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  47. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  48. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  49. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  50. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  51. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  52. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
  53. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
  54. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
  55. package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
  56. package/ios/vendor/CKeccak/README.md +6 -3
  57. package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
  58. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  59. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  60. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  61. package/ios/vendor/CMonocypher/README.md +27 -0
  62. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  63. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  64. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  65. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  66. package/ios/vendor/GENERATED.md +1 -1
  67. package/package.json +3 -2
  68. package/src/appleSignIn.ts +94 -0
  69. package/src/client.ts +90 -6
  70. package/src/errors.ts +7 -1
  71. package/src/index.ts +7 -0
  72. package/src/native.ts +6 -2
  73. package/src/types.ts +52 -1
@@ -0,0 +1,285 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonAppleSignIn/AppleSignIn.swift. Do not edit: change the source and run the script.
2
+ import AuthenticationServices
3
+ import CryptoKit
4
+ import Foundation
5
+ import Security
6
+ #if canImport(UIKit)
7
+ import UIKit
8
+ #elseif canImport(AppKit)
9
+ import AppKit
10
+ #endif
11
+
12
+ /// Error of the native Apple sign-in. `code` is a sign-in code shared by every Authon SDK
13
+ /// (docs/wallet/NATIVE.md section 10): OAUTH_CANCELLED (show nothing), OAUTH_START_FAILED,
14
+ /// OAUTH_REDIRECT_NOT_ALLOWED (app not registered or approved), OAUTH_PROVIDER_ERROR,
15
+ /// OAUTH_STATE_MISMATCH, OAUTH_EXCHANGE_FAILED, ACCOUNT_LINK_REQUIRED, NETWORK_ERROR,
16
+ /// RATE_LIMITED, API_ERROR.
17
+ public struct AppleSignInError: Error, Equatable, LocalizedError, Sendable {
18
+ public let code: String
19
+ public let message: String
20
+ /// The server's request ID when the error came from an API response.
21
+ public let diagnosticId: String?
22
+ /// The server's own `code`, when it sent one (e.g. APP_NOT_APPROVED).
23
+ public let serverCode: String?
24
+ public let statusCode: Int
25
+ /// Whether the same call can succeed again. False for Apple ID sheet failures: showing the
26
+ /// same sheet again does not help (e.g. no Apple account on the device).
27
+ public let retryable: Bool
28
+
29
+ public var errorDescription: String? { message }
30
+
31
+ init(_ code: String, _ message: String, diagnosticId: String? = nil, serverCode: String? = nil, statusCode: Int = 0, retryable: Bool? = nil) {
32
+ self.code = code
33
+ self.message = message
34
+ self.diagnosticId = diagnosticId
35
+ self.serverCode = serverCode
36
+ self.statusCode = statusCode
37
+ self.retryable = retryable ?? ["OAUTH_START_FAILED", "OAUTH_PROVIDER_ERROR", "OAUTH_STATE_MISMATCH", "OAUTH_EXCHANGE_FAILED", "NETWORK_ERROR", "RATE_LIMITED", "API_ERROR"].contains(code)
38
+ }
39
+ }
40
+
41
+ /// What the Apple ID sheet returned. Credentials only travel to the Authon API.
42
+ public struct AppleIDResult: Sendable {
43
+ public var identityToken: Data?
44
+ public var authorizationCode: Data?
45
+ public var fullName: PersonNameComponents?
46
+
47
+ public init(identityToken: Data?, authorizationCode: Data?, fullName: PersonNameComponents?) {
48
+ self.identityToken = identityToken
49
+ self.authorizationCode = authorizationCode
50
+ self.fullName = fullName
51
+ }
52
+ }
53
+
54
+ /// Shows the Apple ID sheet for a nonce (already hashed: lowercase hex SHA-256 of the server nonce).
55
+ public protocol AppleIDAuthorizing: Sendable {
56
+ func authorize(hashedNonce: String) async throws -> AppleIDResult
57
+ }
58
+
59
+ /// Native Sign in with Apple against Authon's nonce-bound flow:
60
+ /// `POST /v1/auth/oauth/native/apple/start` → Apple ID sheet → `POST /v1/auth/oauth/native/apple/complete`.
61
+ /// There is no browser and no web fallback. An attempt is used once: after any failure, start again.
62
+ /// The identity token, authorization code, nonce and verifier are never logged or returned.
63
+ public final class AuthonAppleSignIn: @unchecked Sendable {
64
+ private let base: String
65
+ private let publishableKey: String
66
+ private let appId: String?
67
+ private let authorizer: AppleIDAuthorizing
68
+ private let session: URLSession
69
+
70
+ /// - Parameters:
71
+ /// - appId: `TEAMID.bundleId` as registered in the Authon console; detected from the default
72
+ /// keychain access group when nil.
73
+ /// - anchor: The window for the Apple ID sheet (the key window when nil).
74
+ public convenience init(publishableKey: String, apiURL: String = "https://api.authon.dev", appId: String? = nil,
75
+ anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
76
+ self.init(publishableKey: publishableKey, apiURL: apiURL, appId: appId, authorizer: SystemAppleIDAuthorizer(anchor: anchor), session: .shared)
77
+ }
78
+
79
+ public init(publishableKey: String, apiURL: String, appId: String?, authorizer: AppleIDAuthorizing, session: URLSession = .shared) {
80
+ var b = apiURL
81
+ while b.hasSuffix("/") { b.removeLast() }
82
+ base = b
83
+ self.publishableKey = publishableKey
84
+ self.appId = appId
85
+ self.authorizer = authorizer
86
+ self.session = session
87
+ }
88
+
89
+ /// Runs the sign-in and returns the API's JSON result: a session
90
+ /// (`accessToken`, `refreshToken`, `expiresIn`, `user`) or an MFA challenge (`mfaRequired`, `mfaToken`).
91
+ public func signIn() async throws -> [String: Any] {
92
+ guard let appId = appId ?? Self.detectAppIdentifier() else {
93
+ throw AppleSignInError("OAUTH_REDIRECT_NOT_ALLOWED", "appId (TEAMID.bundleId) could not be determined")
94
+ }
95
+ var verifierBytes = [UInt8](repeating: 0, count: 32)
96
+ guard SecRandomCopyBytes(kSecRandomDefault, verifierBytes.count, &verifierBytes) == errSecSuccess else {
97
+ throw AppleSignInError("OAUTH_START_FAILED", "random source unavailable")
98
+ }
99
+ let verifier = Self.base64url(Data(verifierBytes))
100
+ let challenge = Self.base64url(Data(SHA256.hash(data: Data(verifier.utf8))))
101
+
102
+ let start: [String: Any]
103
+ do {
104
+ start = try await post("/v1/auth/oauth/native/apple/start", ["appId": appId, "codeChallenge": challenge])
105
+ } catch let e as AppleSignInError where e.code == "API_ERROR" {
106
+ throw AppleSignInError("OAUTH_START_FAILED", e.message, diagnosticId: e.diagnosticId, serverCode: e.serverCode, statusCode: e.statusCode)
107
+ }
108
+ guard let attemptId = start["attemptId"] as? String, let nonce = start["nonce"] as? String, !nonce.isEmpty else {
109
+ throw AppleSignInError("OAUTH_START_FAILED", "Unexpected response from the server")
110
+ }
111
+
112
+ let apple = try await authorizer.authorize(hashedNonce: Self.hashNonce(nonce))
113
+ guard let tokenData = apple.identityToken, let identityToken = String(data: tokenData, encoding: .utf8),
114
+ let codeData = apple.authorizationCode, let authorizationCode = String(data: codeData, encoding: .utf8) else {
115
+ throw AppleSignInError("OAUTH_PROVIDER_ERROR", "Apple returned no identity token or authorization code", retryable: false)
116
+ }
117
+ var body: [String: Any] = ["attemptId": attemptId, "codeVerifier": verifier, "identityToken": identityToken, "authorizationCode": authorizationCode]
118
+ if let name = apple.fullName.map({ PersonNameComponentsFormatter.localizedString(from: $0, style: .default) })?
119
+ .trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty {
120
+ body["displayName"] = name
121
+ }
122
+ // Single use: no retry, whatever the outcome.
123
+ return try await post("/v1/auth/oauth/native/apple/complete", body)
124
+ }
125
+
126
+ /// Lowercase hex SHA-256 of the server nonce (what ASAuthorizationAppleIDRequest.nonce carries).
127
+ public static func hashNonce(_ nonce: String) -> String {
128
+ SHA256.hash(data: Data(nonce.utf8)).map { String(format: "%02x", $0) }.joined()
129
+ }
130
+
131
+ static func base64url(_ data: Data) -> String {
132
+ data.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
133
+ }
134
+
135
+ // MARK: - HTTP
136
+
137
+ private func post(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
138
+ guard let url = URL(string: base + path) else { throw AppleSignInError("API_ERROR", "Invalid URL") }
139
+ var req = URLRequest(url: url)
140
+ req.httpMethod = "POST"
141
+ req.setValue("application/json", forHTTPHeaderField: "content-type")
142
+ req.setValue("application/json", forHTTPHeaderField: "accept")
143
+ req.setValue(publishableKey, forHTTPHeaderField: "x-api-key")
144
+ req.httpShouldHandleCookies = false
145
+ req.httpBody = try JSONSerialization.data(withJSONObject: body)
146
+ let data: Data
147
+ let response: URLResponse
148
+ do {
149
+ (data, response) = try await session.data(for: req)
150
+ } catch {
151
+ throw AppleSignInError("NETWORK_ERROR", "The server could not be reached")
152
+ }
153
+ let http = response as? HTTPURLResponse
154
+ let status = http?.statusCode ?? 0
155
+ let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
156
+ guard (200..<300).contains(status) else {
157
+ let serverCode = json?["code"] as? String
158
+ let id = (json?["requestId"] as? String) ?? http?.value(forHTTPHeaderField: "X-Request-Id")
159
+ let message = (json?["message"] as? String) ?? HTTPURLResponse.localizedString(forStatusCode: status)
160
+ throw AppleSignInError(Self.code(status: status, serverCode: serverCode), message, diagnosticId: id, serverCode: serverCode, statusCode: status)
161
+ }
162
+ guard let json else { throw AppleSignInError("API_ERROR", "Unexpected response from the server", statusCode: status) }
163
+ return json
164
+ }
165
+
166
+ static func code(status: Int, serverCode: String?) -> String {
167
+ switch serverCode {
168
+ case "OAUTH_STATE_INVALID": return "OAUTH_STATE_MISMATCH"
169
+ case "OAUTH_EXCHANGE_FAILED": return "OAUTH_EXCHANGE_FAILED"
170
+ case "OAUTH_REDIRECT_NOT_ALLOWED", "APP_NOT_REGISTERED", "APP_NOT_APPROVED": return "OAUTH_REDIRECT_NOT_ALLOWED"
171
+ case "EXPLICIT_IDENTITY_LINK_REQUIRED": return "ACCOUNT_LINK_REQUIRED"
172
+ case "RATE_LIMITED": return "RATE_LIMITED"
173
+ default: return status == 429 ? "RATE_LIMITED" : "API_ERROR"
174
+ }
175
+ }
176
+
177
+ /// `TEAMID.bundleId` from the app's default keychain access group.
178
+ static func detectAppIdentifier() -> String? {
179
+ var q: [String: Any] = [
180
+ kSecClass as String: kSecClassGenericPassword,
181
+ kSecAttrService as String: "dev.authon.apple-sign-in.app-id",
182
+ kSecAttrAccount as String: "probe",
183
+ kSecReturnAttributes as String: true,
184
+ ]
185
+ #if os(macOS)
186
+ q[kSecUseDataProtectionKeychain as String] = true
187
+ #endif
188
+ var out: AnyObject?
189
+ var status = SecItemCopyMatching(q as CFDictionary, &out)
190
+ if status == errSecItemNotFound {
191
+ var add = q
192
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
193
+ status = SecItemAdd(add as CFDictionary, &out)
194
+ }
195
+ guard status == errSecSuccess, let attrs = out as? [String: Any], let group = attrs[kSecAttrAccessGroup as String] as? String,
196
+ group.contains(".") else { return nil }
197
+ return group
198
+ }
199
+ }
200
+
201
+ /// The system Apple ID sheet (ASAuthorizationController), shown immediately.
202
+ public final class SystemAppleIDAuthorizer: AppleIDAuthorizing, @unchecked Sendable {
203
+ private let anchor: (@MainActor () -> ASPresentationAnchor?)?
204
+
205
+ public init(anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
206
+ self.anchor = anchor
207
+ }
208
+
209
+ @MainActor
210
+ public func authorize(hashedNonce: String) async throws -> AppleIDResult {
211
+ let request = ASAuthorizationAppleIDProvider().createRequest()
212
+ request.requestedScopes = [.fullName, .email]
213
+ request.nonce = hashedNonce
214
+ let delegate = AppleIDDelegate(anchor: anchor?() ?? Self.keyWindow())
215
+ let credential = try await delegate.run(request)
216
+ return AppleIDResult(identityToken: credential.identityToken, authorizationCode: credential.authorizationCode, fullName: credential.fullName)
217
+ }
218
+
219
+ @MainActor
220
+ static func keyWindow() -> ASPresentationAnchor {
221
+ #if canImport(UIKit)
222
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
223
+ return scenes.flatMap(\.windows).first(where: \.isKeyWindow) ?? scenes.first?.windows.first ?? ASPresentationAnchor()
224
+ #else
225
+ return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
226
+ #endif
227
+ }
228
+
229
+ /// ASAuthorizationError → sign-in code. Only `.canceled` is a cancel; everything else (including
230
+ /// 1000 `.unknown` after dismissing "sign in to your Apple account") is a provider error that
231
+ /// retrying the same sheet will not fix.
232
+ static func map(_ error: Error) -> AppleSignInError {
233
+ if let e = error as? ASAuthorizationError, e.code == .canceled {
234
+ return AppleSignInError("OAUTH_CANCELLED", "Sign in with Apple was cancelled")
235
+ }
236
+ return AppleSignInError("OAUTH_PROVIDER_ERROR", "Sign in with Apple failed (\((error as NSError).code))", retryable: false)
237
+ }
238
+ }
239
+
240
+ @MainActor
241
+ private final class AppleIDDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
242
+ private let anchor: ASPresentationAnchor
243
+ private var continuation: CheckedContinuation<ASAuthorizationAppleIDCredential, Error>?
244
+ private var retain: AppleIDDelegate?
245
+
246
+ init(anchor: ASPresentationAnchor) {
247
+ self.anchor = anchor
248
+ }
249
+
250
+ func run(_ request: ASAuthorizationRequest) async throws -> ASAuthorizationAppleIDCredential {
251
+ try await withCheckedThrowingContinuation { cont in
252
+ continuation = cont
253
+ retain = self
254
+ let controller = ASAuthorizationController(authorizationRequests: [request])
255
+ controller.delegate = self
256
+ controller.presentationContextProvider = self
257
+ controller.performRequests()
258
+ }
259
+ }
260
+
261
+ nonisolated func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
262
+ MainActor.assumeIsolated { anchor }
263
+ }
264
+
265
+ nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
266
+ MainActor.assumeIsolated {
267
+ if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
268
+ finish(.success(credential))
269
+ } else {
270
+ finish(.failure(AppleSignInError("OAUTH_PROVIDER_ERROR", "Unexpected Apple credential type", retryable: false)))
271
+ }
272
+ }
273
+ }
274
+
275
+ nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
276
+ let mapped = SystemAppleIDAuthorizer.map(error)
277
+ MainActor.assumeIsolated { finish(.failure(mapped)) }
278
+ }
279
+
280
+ private func finish(_ result: Result<ASAuthorizationAppleIDCredential, Error>) {
281
+ continuation?.resume(with: result)
282
+ continuation = nil
283
+ retain = nil
284
+ }
285
+ }
@@ -61,11 +61,16 @@ public final class AuthonWallet: @unchecked Sendable {
61
61
  /// App registration and wallet settings from the API (cached).
62
62
  public func appConfig() async throws -> WalletAppConfig {
63
63
  if let c = lock.withLock({ cachedConfig }) { return c }
64
+ return try await refreshAppConfig()
65
+ }
66
+
67
+ /// Fetches app-config again (used by capabilities()); a successful answer replaces the cache.
68
+ func refreshAppConfig() async throws -> WalletAppConfig {
64
69
  guard let appId = appId ?? Self.detectAppIdentifier() else {
65
70
  throw WalletError(.originNotAllowed, "appId (TEAMID.bundleId) could not be determined")
66
71
  }
67
72
  let cfg = try await api.appConfig(platform: "ios", appId: appId)
68
- lock.withLock { cachedConfig = cfg }
73
+ if cfg.projectFound { lock.withLock { cachedConfig = cfg } }
69
74
  return cfg
70
75
  }
71
76
 
@@ -135,9 +140,10 @@ public final class AuthonWallet: @unchecked Sendable {
135
140
  try await run { ops, s in try PublicWallet(try await ops.create(s)) }
136
141
  }
137
142
 
138
- /// Addresses of accounts start..<start+count (m/44'/60'/0'/0/n).
139
- public func accounts(start: Int = 0, count: Int = 1) async throws -> [WalletAccount] {
140
- try await run { ops, s in try await ops.accounts(s, start: start, count: count) }
143
+ /// Addresses of accounts start..<start+count on `chain` (EVM: m/44'/60'/0'/0/n; other paths in
144
+ /// `WalletChain.path`). A wallet from before HD wallets has only EVM account 0 (ACCOUNT_UNAVAILABLE).
145
+ public func accounts(start: Int = 0, count: Int = 1, chain: WalletChain = .evm) async throws -> [WalletAccount] {
146
+ try await run { ops, s in try await ops.accounts(s, start: start, count: count, chain: chain) }
141
147
  }
142
148
 
143
149
  /// EIP-191 personal_sign. `account` > 0 must come with the address from `accounts()`.
@@ -166,6 +172,20 @@ public final class AuthonWallet: @unchecked Sendable {
166
172
  }
167
173
  }
168
174
 
175
+ /// Signs a transaction the app built for another chain (EVM uses `signTransaction`: UNSUPPORTED_TYPE).
176
+ /// - Solana: `["message": <serialized Message, base64>, "cluster": "devnet"]` (cluster optional,
177
+ /// only labels the approval) → `["signature": <base58>]`.
178
+ /// - TRON: `["rawDataHex": <raw_data protobuf, hex>]` → `["txID": <hex>, "signature": <r||s||v hex>]`.
179
+ /// - Bitcoin / testnet: `["psbt": <base64>]` → `["psbt": <signed PSBT base64>, "txHex": <raw tx>]`
180
+ /// (`txHex` only when every input was the account's).
181
+ /// - XRP: `["txBlobHex": <encodeForSigning output>]` → `["signature": <DER, uppercase hex>]`.
182
+ /// `address` is the expected address of `account` on that chain (from `accounts(chain:)`).
183
+ public func signChainTransaction(chain: WalletChain, account: Int = 0, address: String, payload: [String: Any]) async throws -> [String: String] {
184
+ try await run { ops, s in
185
+ try await ops.signChainTransaction(s, chain: chain, account: account, address: address, payload: try JSONValue.from(payload))
186
+ }
187
+ }
188
+
169
189
  /// Creates the passkey backup (iOS 18 / macOS 15 for PRF).
170
190
  public func protect() async throws -> ProtectResult {
171
191
  try await run(passkeys: true) { ops, s in try await ops.protect(s) }
@@ -0,0 +1,149 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Capabilities.swift. Do not edit: change the source and run the script.
2
+ import CryptoKit
3
+ import Foundation
4
+ import LocalAuthentication
5
+ import Security
6
+
7
+ /// Pre-sign-in check (docs/wallet/NATIVE.md section 9): whether this app and device can use the
8
+ /// wallet. Needs no session, opens no login or passkey UI and never creates a wallet.
9
+ public struct WalletCapabilities: Equatable, Sendable {
10
+ public enum Status: String, Sendable {
11
+ case available, unavailable, unknown
12
+ }
13
+
14
+ public struct Check: Equatable, Sendable {
15
+ public var status: Status
16
+ /// Section 9 reason code; nil when available.
17
+ public var reason: String?
18
+
19
+ public static let available = Check(status: .available, reason: nil)
20
+ static func unavailable(_ reason: String) -> Check { Check(status: .unavailable, reason: reason) }
21
+ static func unknown(_ reason: String) -> Check { Check(status: .unknown, reason: reason) }
22
+ }
23
+
24
+ /// unavailable if any check is unavailable, else unknown if any is unknown, else available.
25
+ public var ready: Status
26
+ /// App registration and approval (GET /v1/wallet/app-config).
27
+ public var app: Check
28
+ /// The embedded wallet is enabled for the project.
29
+ public var wallet: Check
30
+ /// Hardware-protected key storage (Secure Enclave on a real device).
31
+ public var secureStorage: Check
32
+ /// A device passcode or biometrics is set.
33
+ public var deviceLock: Check
34
+ /// Passkey PRF backup and restore (static OS support only).
35
+ public var passkeyBackup: Check
36
+ /// `X-Request-Id` of the app-config response.
37
+ public var diagnosticId: String?
38
+
39
+ init(app: Check, wallet: Check, secureStorage: Check, deviceLock: Check, passkeyBackup: Check, diagnosticId: String?) {
40
+ self.app = app
41
+ self.wallet = wallet
42
+ self.secureStorage = secureStorage
43
+ self.deviceLock = deviceLock
44
+ self.passkeyBackup = passkeyBackup
45
+ self.diagnosticId = diagnosticId
46
+ let all = [app, wallet, secureStorage, deviceLock, passkeyBackup].map(\.status)
47
+ ready = all.contains(.unavailable) ? .unavailable : all.contains(.unknown) ? .unknown : .available
48
+ }
49
+ }
50
+
51
+ /// Device checks behind capabilities(); replaceable in tests.
52
+ struct CapabilityProbes: Sendable {
53
+ var secureStorage: @Sendable () -> WalletCapabilities.Check
54
+ var deviceLock: @Sendable () -> WalletCapabilities.Check
55
+ var passkeyBackup: @Sendable () -> WalletCapabilities.Check
56
+
57
+ static let device = CapabilityProbes(
58
+ secureStorage: { probeSecureEnclave() },
59
+ deviceLock: { probeDeviceLock() },
60
+ passkeyBackup: { probePasskeyBackup() }
61
+ )
62
+
63
+ /// The simulator never counts as hardware protection, even where it emulates an enclave.
64
+ /// On a device a throwaway enclave key is created and released (never stored).
65
+ static func probeSecureEnclave() -> WalletCapabilities.Check {
66
+ #if targetEnvironment(simulator)
67
+ return .unknown("SIMULATOR")
68
+ #else
69
+ guard SecureEnclave.isAvailable else { return .unavailable("HARDWARE_KEYSTORE_UNAVAILABLE") }
70
+ var error: Unmanaged<CFError>?
71
+ guard let access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, &error) else {
72
+ return .unknown("CHECK_FAILED")
73
+ }
74
+ let attrs: [String: Any] = [
75
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
76
+ kSecAttrKeySizeInBits as String: 256,
77
+ kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
78
+ kSecPrivateKeyAttrs as String: [
79
+ kSecAttrIsPermanent as String: false,
80
+ kSecAttrAccessControl as String: access,
81
+ ] as [String: Any],
82
+ ]
83
+ guard SecKeyCreateRandomKey(attrs as CFDictionary, &error) != nil else { return .unknown("CHECK_FAILED") }
84
+ return .available
85
+ #endif
86
+ }
87
+
88
+ static func probeDeviceLock() -> WalletCapabilities.Check {
89
+ let context = LAContext()
90
+ var error: NSError?
91
+ if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { return .available }
92
+ if let code = error.map({ LAError.Code(rawValue: $0.code) }), code == .passcodeNotSet {
93
+ return .unavailable("DEVICE_LOCK_NOT_SET")
94
+ }
95
+ return .unknown("CHECK_FAILED")
96
+ }
97
+
98
+ static func probePasskeyBackup() -> WalletCapabilities.Check {
99
+ if #available(iOS 18.0, macOS 15.0, *) {
100
+ // The API exists; whether the user's passkey provider evaluates PRF shows on the first protect().
101
+ return .unknown("PRF_PROVIDER_UNVERIFIED")
102
+ }
103
+ return .unavailable("OS_UNSUPPORTED")
104
+ }
105
+ }
106
+
107
+ extension AuthonWallet {
108
+ /// See `WalletCapabilities`. Never throws: failures become `unknown` checks.
109
+ /// `allowSoftwareKeys` does not affect the answer.
110
+ public func capabilities() async -> WalletCapabilities {
111
+ await capabilities(probes: .device)
112
+ }
113
+
114
+ func capabilities(probes: CapabilityProbes) async -> WalletCapabilities {
115
+ let app: WalletCapabilities.Check
116
+ let wallet: WalletCapabilities.Check
117
+ var diagnosticId: String?
118
+ do {
119
+ let cfg = try await refreshAppConfig()
120
+ diagnosticId = cfg.diagnosticId
121
+ if !cfg.projectFound {
122
+ app = .unavailable("APP_NOT_REGISTERED")
123
+ wallet = .unknown("API_ERROR")
124
+ } else {
125
+ app = !cfg.allowed ? .unavailable("APP_NOT_REGISTERED") : cfg.approved ? .available : .unknown("APP_NOT_APPROVED")
126
+ wallet = cfg.enabled ? .available : .unavailable("WALLET_DISABLED")
127
+ }
128
+ } catch let e as WalletError where e.isNetworkFailure {
129
+ app = .unknown("NETWORK_ERROR")
130
+ wallet = .unknown("NETWORK_ERROR")
131
+ } catch let e as WalletError where e.code == .originNotAllowed {
132
+ // The app ID could not be determined, so no registration can match it.
133
+ app = .unavailable("APP_NOT_REGISTERED")
134
+ wallet = .unknown("API_ERROR")
135
+ } catch {
136
+ diagnosticId = (error as? WalletError)?.diagnosticId
137
+ app = .unknown("API_ERROR")
138
+ wallet = .unknown("API_ERROR")
139
+ }
140
+ return WalletCapabilities(
141
+ app: app,
142
+ wallet: wallet,
143
+ secureStorage: probes.secureStorage(),
144
+ deviceLock: probes.deviceLock(),
145
+ passkeyBackup: probes.passkeyBackup(),
146
+ diagnosticId: diagnosticId
147
+ )
148
+ }
149
+ }