@authon/react-native-wallet 0.0.0-stage → 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 (183) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +251 -2
  3. package/android/build.gradle +40 -0
  4. package/android/consumer-rules.pro +10 -0
  5. package/android/src/main/AndroidManifest.xml +8 -0
  6. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +235 -0
  7. package/android/src/vendor/GENERATED.md +4 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +218 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +86 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +236 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  28. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  29. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  30. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
  31. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
  32. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +361 -0
  33. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
  34. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +74 -0
  35. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +551 -0
  36. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +78 -0
  37. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
  38. package/build/authonContext.d.ts +19 -0
  39. package/build/authonContext.d.ts.map +1 -0
  40. package/build/authonContext.js +20 -0
  41. package/build/authonContext.js.map +1 -0
  42. package/build/client.d.ts +92 -0
  43. package/build/client.d.ts.map +1 -0
  44. package/build/client.js +431 -0
  45. package/build/client.js.map +1 -0
  46. package/build/errors.d.ts +25 -0
  47. package/build/errors.d.ts.map +1 -0
  48. package/build/errors.js +15 -0
  49. package/build/errors.js.map +1 -0
  50. package/build/index.d.ts +7 -0
  51. package/build/index.d.ts.map +1 -0
  52. package/build/index.js +4 -0
  53. package/build/index.js.map +1 -0
  54. package/build/native.d.ts +43 -0
  55. package/build/native.d.ts.map +1 -0
  56. package/build/native.js +12 -0
  57. package/build/native.js.map +1 -0
  58. package/build/types.d.ts +167 -0
  59. package/build/types.d.ts.map +1 -0
  60. package/build/types.js +3 -0
  61. package/build/types.js.map +1 -0
  62. package/build/useAuthonWallet.d.ts +25 -0
  63. package/build/useAuthonWallet.d.ts.map +1 -0
  64. package/build/useAuthonWallet.js +77 -0
  65. package/build/useAuthonWallet.js.map +1 -0
  66. package/expo-module.config.json +9 -0
  67. package/ios/AuthonReactNativeWallet.podspec +41 -0
  68. package/ios/AuthonWalletModule.swift +233 -0
  69. package/ios/vendor/AuthonWallet/AuthonWallet.swift +232 -0
  70. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  71. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  72. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  73. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  74. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  75. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  76. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  77. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  78. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +128 -0
  79. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  80. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  81. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  82. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  83. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  84. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  85. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  86. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  87. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  88. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  89. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  90. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  91. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  92. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  93. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  94. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +361 -0
  95. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +654 -0
  96. package/ios/vendor/AuthonWallet/WalletError.swift +84 -0
  97. package/ios/vendor/CKeccak/LICENSE +22 -0
  98. package/ios/vendor/CKeccak/README.md +30 -0
  99. package/ios/vendor/CKeccak/byte_order.h +59 -0
  100. package/ios/vendor/CKeccak/include/ckeccak.h +13 -0
  101. package/ios/vendor/CKeccak/memzero.c +75 -0
  102. package/ios/vendor/CKeccak/memzero.h +8 -0
  103. package/ios/vendor/CKeccak/options.h +99 -0
  104. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  105. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  106. package/ios/vendor/CKeccak/sha3.c +413 -0
  107. package/ios/vendor/CKeccak/sha3.h +89 -0
  108. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  109. package/ios/vendor/CMonocypher/README.md +27 -0
  110. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  111. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  112. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  113. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  114. package/ios/vendor/CSecp256k1/COPYING +19 -0
  115. package/ios/vendor/CSecp256k1/README.md +35 -0
  116. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  117. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  118. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  119. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  120. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  121. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  122. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  123. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  124. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  125. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  126. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  127. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  128. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  129. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  130. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  131. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  132. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  133. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  134. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  135. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  136. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  137. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  138. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  139. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  140. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  141. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  142. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  143. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  144. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  145. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  146. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  147. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  148. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  149. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  150. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  151. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  152. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  153. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  154. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  155. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  156. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  157. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  158. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  159. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  160. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  161. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  162. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  163. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  164. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  165. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  166. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  167. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  168. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  169. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  170. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  171. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  172. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  173. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  174. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  175. package/ios/vendor/GENERATED.md +4 -0
  176. package/package.json +66 -3
  177. package/src/authonContext.ts +30 -0
  178. package/src/client.ts +459 -0
  179. package/src/errors.ts +58 -0
  180. package/src/index.ts +26 -0
  181. package/src/native.ts +52 -0
  182. package/src/types.ts +171 -0
  183. package/src/useAuthonWallet.ts +103 -0
@@ -0,0 +1,232 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/AuthonWallet.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+ import Security
4
+
5
+ /// The Authon embedded wallet on iOS and macOS. Same wallet, shares and API as the web
6
+ /// frame: the device keeps only its share D; every signature fetches the company share A
7
+ /// with a device-key proof, combines them in memory, signs and wipes the key.
8
+ ///
9
+ /// This module does not depend on the `Authon` auth module; `AuthonWalletAuthon` adds an
10
+ /// initializer that takes an `Authon` instance.
11
+ public final class AuthonWallet: @unchecked Sendable {
12
+ public typealias SessionProvider = @Sendable () async throws -> String
13
+
14
+ private let api: WalletAPIClient
15
+ private let sessionProvider: SessionProvider
16
+ private let appId: String?
17
+ private let customStore: DeviceStore?
18
+ private let customPasskeys: PasskeyProvider?
19
+ private let approver: WalletApprover?
20
+ private let deviceAuthenticator: DeviceAuthenticator?
21
+ private let allowSoftwareKeys: Bool
22
+ private let lock = NSLock()
23
+ private var cachedConfig: WalletAppConfig?
24
+ private var cachedStore: DeviceStore?
25
+
26
+ /// - Parameters:
27
+ /// - sessionProvider: Returns the current Authon session token (refreshed if needed).
28
+ /// - appId: `TEAMID.bundleId` as registered in the Authon console. Detected from the
29
+ /// app's keychain access group when nil.
30
+ /// - approver: Shown before every signature when the project requires confirmation
31
+ /// (server setting). Pass your own to replace the default sheet.
32
+ /// - requireDeviceAuthentication: Ask for Face ID / Touch ID / passcode before each signature.
33
+ /// - deviceStore: Defaults to the Secure Enclave store.
34
+ /// - allowSoftwareKeys: Use software device keys when there is no Secure Enclave (simulator,
35
+ /// tests). Refused in release builds on devices. Never enable it for production.
36
+ public init(
37
+ publishableKey: String,
38
+ apiURL: String = "https://api.authon.dev",
39
+ sessionProvider: @escaping SessionProvider,
40
+ appId: String? = nil,
41
+ approver: WalletApprover? = SheetWalletApprover(),
42
+ requireDeviceAuthentication: Bool = false,
43
+ deviceAuthenticator: DeviceAuthenticator? = nil,
44
+ deviceStore: DeviceStore? = nil,
45
+ passkeys: PasskeyProvider? = nil,
46
+ allowSoftwareKeys: Bool = false,
47
+ urlSession: URLSession = .shared
48
+ ) {
49
+ api = WalletAPIClient(apiURL: apiURL, publishableKey: publishableKey, session: urlSession)
50
+ self.sessionProvider = sessionProvider
51
+ self.appId = appId
52
+ self.approver = approver
53
+ self.deviceAuthenticator = deviceAuthenticator ?? (requireDeviceAuthentication ? LocalDeviceAuthenticator() : nil)
54
+ customStore = deviceStore
55
+ customPasskeys = passkeys
56
+ self.allowSoftwareKeys = allowSoftwareKeys
57
+ }
58
+
59
+ // MARK: - Context
60
+
61
+ /// App registration and wallet settings from the API (cached).
62
+ public func appConfig() async throws -> WalletAppConfig {
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 {
69
+ guard let appId = appId ?? Self.detectAppIdentifier() else {
70
+ throw WalletError(.originNotAllowed, "appId (TEAMID.bundleId) could not be determined")
71
+ }
72
+ let cfg = try await api.appConfig(platform: "ios", appId: appId)
73
+ if cfg.projectFound { lock.withLock { cachedConfig = cfg } }
74
+ return cfg
75
+ }
76
+
77
+ private func store() throws -> DeviceStore {
78
+ if let s = customStore { return s }
79
+ return try lock.withLock {
80
+ if let s = cachedStore { return s }
81
+ let s: DeviceStore
82
+ if let se = try? SecureEnclaveDeviceStore() {
83
+ s = se
84
+ } else if allowSoftwareKeys {
85
+ s = try SoftwareDeviceStore(persistent: true)
86
+ } else {
87
+ throw WalletError(.storageUnavailable, "this device has no Secure Enclave")
88
+ }
89
+ cachedStore = s
90
+ return s
91
+ }
92
+ }
93
+
94
+ private func context(passkeys needed: Bool = false) async throws -> (WalletOps, String) {
95
+ // Like the frame, a request without a session is refused before anything else.
96
+ let session = try await sessionProvider()
97
+ guard !session.isEmpty else { throw WalletError(.unauthorized, "token required") }
98
+ let cfg = try await appConfig()
99
+ guard cfg.allowed else { throw WalletError(.originNotAllowed, "this app is not registered for the project") }
100
+ if let min = cfg.minFrameProtocol, min > WalletAPIClient.frameProtocol { throw WalletError(.frameOutdated) }
101
+ if needed && !cfg.approved {
102
+ throw WalletError(.originNotAllowed, "the app registration is not approved yet, so wallet passkeys are unavailable")
103
+ }
104
+ let ctx = WalletContext(
105
+ api: api,
106
+ store: try store(),
107
+ passkeys: customPasskeys ?? PlatformPasskeyProvider(relyingPartyIdentifier: cfg.rpId),
108
+ projectId: cfg.projectId ?? api.publishableKey,
109
+ confirmSigning: cfg.confirmSigning,
110
+ backupVersion: cfg.prfSaltVersion,
111
+ approver: approver,
112
+ deviceAuthenticator: deviceAuthenticator
113
+ )
114
+ return (WalletOps(ctx: ctx), session)
115
+ }
116
+
117
+ private func run<T>(passkeys: Bool = false, _ body: (WalletOps, String) async throws -> T) async throws -> T {
118
+ do {
119
+ let (ops, session) = try await context(passkeys: passkeys)
120
+ return try await body(ops, session)
121
+ } catch {
122
+ throw WalletError.wrap(error)
123
+ }
124
+ }
125
+
126
+ // MARK: - Operations
127
+
128
+ /// The wallet of the signed-in user. Throws NOT_FOUND when there is none yet.
129
+ public func status() async throws -> PublicWallet {
130
+ try await run { ops, s in try PublicWallet(try await ops.status(s)) }
131
+ }
132
+
133
+ /// Full status including a pending (unconfirmed) wallet; does not throw NOT_FOUND.
134
+ public func detailedStatus() async throws -> WalletStatus {
135
+ try await run { ops, s in try await ops.status(s) }
136
+ }
137
+
138
+ /// Creates the wallet (two-phase), or returns the existing one.
139
+ public func create() async throws -> PublicWallet {
140
+ try await run { ops, s in try PublicWallet(try await ops.create(s)) }
141
+ }
142
+
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) }
147
+ }
148
+
149
+ /// EIP-191 personal_sign. `account` > 0 must come with the address from `accounts()`.
150
+ public func signMessage(_ message: WalletMessage, account: Int = 0, address: String? = nil) async throws -> String {
151
+ try await run { ops, s in try await ops.signMessage(s, message, account: try SignAccount.parse(index: account, address: address)) }
152
+ }
153
+
154
+ public func signMessage(_ text: String, account: Int = 0, address: String? = nil) async throws -> String {
155
+ try await signMessage(.text(text), account: account, address: address)
156
+ }
157
+
158
+ /// EIP-712 v4. `typedData` is a JSON string, a `[String: Any]` dictionary or a JSONValue.
159
+ public func signTypedData(_ typedData: Any, account: Int = 0, address: String? = nil) async throws -> String {
160
+ try await run { ops, s in
161
+ let value: JSONValue = try (typedData as? String).map { try JSONValue.parse($0) } ?? JSONValue.from(typedData)
162
+ return try await ops.signTypedData(s, value, account: try SignAccount.parse(index: account, address: address))
163
+ }
164
+ }
165
+
166
+ /// Signs a legacy, EIP-2930 or EIP-1559 transaction and returns the serialized signed
167
+ /// transaction. Numeric fields may be numbers, decimal strings or 0x hex strings.
168
+ public func signTransaction(_ transaction: Any, account: Int = 0, address: String? = nil) async throws -> String {
169
+ try await run { ops, s in
170
+ let value: JSONValue = try (transaction as? String).map { try JSONValue.parse($0) } ?? JSONValue.from(transaction)
171
+ return try await ops.signTransaction(s, value, account: try SignAccount.parse(index: account, address: address))
172
+ }
173
+ }
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
+
189
+ /// Creates the passkey backup (iOS 18 / macOS 15 for PRF).
190
+ public func protect() async throws -> ProtectResult {
191
+ try await run(passkeys: true) { ops, s in try await ops.protect(s) }
192
+ }
193
+
194
+ /// Replaces a legacy backup with a verifiable one, from a device that holds the wallet.
195
+ public func upgradeBackup() async throws -> ProtectResult {
196
+ try await run(passkeys: true) { ops, s in try await ops.upgradeBackup(s) }
197
+ }
198
+
199
+ /// Restores the wallet on this device with the backup passkey.
200
+ public func restore(mode: RestoreMode = .add) async throws -> RestoreResult {
201
+ try await run(passkeys: true) { ops, s in try await ops.restore(s, mode: mode) }
202
+ }
203
+
204
+ public func revokeDevice(_ deviceId: String) async throws {
205
+ try await run { ops, s in try await ops.revokeDevice(s, deviceId: deviceId) }
206
+ }
207
+
208
+ // MARK: - App identifier
209
+
210
+ /// `TEAMID.bundleId` from the default keychain access group (the app identifier prefix).
211
+ static func detectAppIdentifier() -> String? {
212
+ var q: [String: Any] = [
213
+ kSecClass as String: kSecClassGenericPassword,
214
+ kSecAttrService as String: "dev.authon.wallet.app-id",
215
+ kSecAttrAccount as String: "probe",
216
+ kSecReturnAttributes as String: true,
217
+ ]
218
+ #if os(macOS)
219
+ q[kSecUseDataProtectionKeychain as String] = true
220
+ #endif
221
+ var out: AnyObject?
222
+ var status = SecItemCopyMatching(q as CFDictionary, &out)
223
+ if status == errSecItemNotFound {
224
+ var add = q
225
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
226
+ status = SecItemAdd(add as CFDictionary, &out)
227
+ }
228
+ guard status == errSecSuccess, let attrs = out as? [String: Any], let group = attrs[kSecAttrAccessGroup as String] as? String,
229
+ group.contains(".") else { return nil }
230
+ return group
231
+ }
232
+ }
@@ -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
+ }
@@ -0,0 +1,296 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Chains/Bitcoin.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+
4
+ /// Bitcoin PSBT signing (docs/wallet/NATIVE.md section 12, `apps/wallet/src/bitcoin.ts`). Inputs
5
+ /// whose previous output is the signing account's P2WPKH script are signed (BIP143,
6
+ /// SIGHASH_ALL); other inputs are left alone. Every input must carry witness_utxo.
7
+ enum Bitcoin {
8
+ static let maxPsbt = 100_000
9
+
10
+ private static func invalid(_ why: String = "not a valid PSBT") -> WalletError { WalletError(.invalidRequest, why) }
11
+
12
+ struct KeyValue {
13
+ var key: [UInt8]
14
+ var value: [UInt8]
15
+ }
16
+
17
+ struct Input {
18
+ var prevout: [UInt8] // txid (32, internal order) + vout (4)
19
+ var sequence: [UInt8]
20
+ var map: [KeyValue]
21
+
22
+ var witnessUtxo: (amount: UInt64, script: [UInt8])? {
23
+ guard let v = map.first(where: { $0.key == [0x01] })?.value else { return nil }
24
+ return try? Bitcoin.decodeTxOut(v)
25
+ }
26
+ }
27
+
28
+ struct Output {
29
+ var amount: UInt64
30
+ var script: [UInt8]
31
+ }
32
+
33
+ /// A version 0 PSBT. Key-value pairs are kept as read and written back in the same order.
34
+ struct Psbt {
35
+ var global: [KeyValue]
36
+ var version: [UInt8]
37
+ var locktime: [UInt8]
38
+ var inputs: [Input]
39
+ var outputs: [Output]
40
+ var outputMaps: [[KeyValue]]
41
+ }
42
+
43
+ // MARK: - Reading
44
+
45
+ private struct Reader {
46
+ let b: [UInt8]
47
+ var pos = 0
48
+ var done: Bool { pos == b.count }
49
+
50
+ mutating func take(_ n: Int) throws -> [UInt8] {
51
+ guard n >= 0, n <= b.count - pos else { throw invalid() }
52
+ pos += n
53
+ return Array(b[(pos - n)..<pos])
54
+ }
55
+
56
+ mutating func compact() throws -> Int {
57
+ let first = try take(1)[0]
58
+ switch first {
59
+ case 0xfd: return Int(le(try take(2)))
60
+ case 0xfe: return Int(le(try take(4)))
61
+ case 0xff:
62
+ let v = le(try take(8))
63
+ guard v <= UInt64(Int32.max) else { throw invalid() }
64
+ return Int(v)
65
+ default: return Int(first)
66
+ }
67
+ }
68
+
69
+ mutating func varBytes() throws -> [UInt8] { try take(try compact()) }
70
+
71
+ /// One map; ends at the 0x00 separator. Duplicate keys are invalid.
72
+ mutating func map() throws -> [KeyValue] {
73
+ var out = [KeyValue]()
74
+ while true {
75
+ let key = try varBytes()
76
+ if key.isEmpty { return out }
77
+ let value = try varBytes()
78
+ guard !out.contains(where: { $0.key == key }) else { throw invalid() }
79
+ out.append(KeyValue(key: key, value: value))
80
+ }
81
+ }
82
+ }
83
+
84
+ static func le(_ b: [UInt8]) -> UInt64 { b.enumerated().reduce(UInt64(0)) { $0 | UInt64($1.element) << (8 * UInt64($1.offset)) } }
85
+ static func le64(_ v: UInt64) -> [UInt8] { (0..<8).map { UInt8(truncatingIfNeeded: v >> (8 * UInt64($0))) } }
86
+
87
+ static func compact(_ n: Int) -> [UInt8] {
88
+ if n < 0xfd { return [UInt8(n)] }
89
+ if n <= 0xffff { return [0xfd, UInt8(n & 0xff), UInt8(n >> 8)] }
90
+ return [0xfe] + (0..<4).map { UInt8(truncatingIfNeeded: n >> (8 * $0)) }
91
+ }
92
+
93
+ static func decodeTxOut(_ b: [UInt8]) throws -> (amount: UInt64, script: [UInt8]) {
94
+ var r = Reader(b: b)
95
+ let amount = le(try r.take(8))
96
+ let script = try r.varBytes()
97
+ guard r.done else { throw invalid() }
98
+ return (amount, script)
99
+ }
100
+
101
+ static func parse(_ bytes: [UInt8]) throws -> Psbt {
102
+ guard !bytes.isEmpty, bytes.count <= maxPsbt else { throw invalid() }
103
+ var r = Reader(b: bytes)
104
+ guard try r.take(5) == [0x70, 0x73, 0x62, 0x74, 0xff] else { throw invalid() }
105
+ let global = try r.map()
106
+ if let v = global.first(where: { $0.key == [0xfb] })?.value, le(v) != 0 { throw invalid("only PSBT version 0 is supported") }
107
+ guard let unsigned = global.first(where: { $0.key == [0x00] })?.value else { throw invalid() }
108
+ var t = Reader(b: unsigned)
109
+ let version = try t.take(4)
110
+ let inCount = try t.compact()
111
+ guard inCount > 0 else { throw invalid() } // also rejects a segwit marker
112
+ var inputs = [Input]()
113
+ for _ in 0..<inCount {
114
+ let prevout = try t.take(36)
115
+ guard try t.varBytes().isEmpty else { throw invalid() } // unsigned: empty scriptSig
116
+ inputs.append(Input(prevout: prevout, sequence: try t.take(4), map: []))
117
+ }
118
+ var outputs = [Output]()
119
+ for _ in 0..<(try t.compact()) {
120
+ let amount = le(try t.take(8))
121
+ outputs.append(Output(amount: amount, script: try t.varBytes()))
122
+ }
123
+ let locktime = try t.take(4)
124
+ guard t.done else { throw invalid() }
125
+ for i in inputs.indices {
126
+ inputs[i].map = try r.map()
127
+ if let utxo = inputs[i].map.first(where: { $0.key == [0x01] }) { _ = try decodeTxOut(utxo.value) }
128
+ if let sighash = inputs[i].map.first(where: { $0.key == [0x03] })?.value, sighash != [1, 0, 0, 0] {
129
+ throw invalid("only SIGHASH_ALL inputs can be signed")
130
+ }
131
+ }
132
+ var outputMaps = [[KeyValue]]()
133
+ for _ in outputs { outputMaps.append(try r.map()) }
134
+ guard r.done else { throw invalid() }
135
+ return Psbt(global: global, version: version, locktime: locktime, inputs: inputs, outputs: outputs, outputMaps: outputMaps)
136
+ }
137
+
138
+ static func serialize(_ p: Psbt) -> [UInt8] {
139
+ func map(_ m: [KeyValue]) -> [UInt8] {
140
+ var out = [UInt8]()
141
+ for kv in m {
142
+ out += compact(kv.key.count)
143
+ out += kv.key
144
+ out += compact(kv.value.count)
145
+ out += kv.value
146
+ }
147
+ out.append(0)
148
+ return out
149
+ }
150
+ var out: [UInt8] = [0x70, 0x73, 0x62, 0x74, 0xff]
151
+ out += map(p.global)
152
+ for i in p.inputs { out += map(i.map) }
153
+ for m in p.outputMaps { out += map(m) }
154
+ return out
155
+ }
156
+
157
+ // MARK: - Scripts and addresses
158
+
159
+ static func p2wpkh(_ compressed: [UInt8]) -> [UInt8] { [0x00, 0x14] + ChainAddresses.hash160(compressed) }
160
+
161
+ private static func hrp(_ chain: WalletChain) -> String { chain == .bitcoin ? "bc" : "tb" }
162
+
163
+ /// Address of a standard output script, or nil.
164
+ static func address(script s: [UInt8], chain: WalletChain) -> String? {
165
+ let main = chain == .bitcoin
166
+ func base58(_ version: UInt8, _ hash: ArraySlice<UInt8>) -> String {
167
+ let payload = [version] + hash
168
+ return Base58.encode(payload + ChainAddresses.checksum4(payload), alphabet: Base58.bitcoin)
169
+ }
170
+ if s.count == 25, s[0] == 0x76, s[1] == 0xa9, s[2] == 0x14, s[23] == 0x88, s[24] == 0xac { return base58(main ? 0x00 : 0x6f, s[3..<23]) }
171
+ if s.count == 23, s[0] == 0xa9, s[1] == 0x14, s[22] == 0x87 { return base58(main ? 0x05 : 0xc4, s[2..<22]) }
172
+ if s.count == 22, s[0] == 0x00, s[1] == 0x14 { return Bech32.segwitAddress(hrp: hrp(chain), version: 0, program: Array(s[2...])) }
173
+ if s.count == 34, s[0] == 0x00, s[1] == 0x20 { return Bech32.segwitAddress(hrp: hrp(chain), version: 0, program: Array(s[2...])) }
174
+ if s.count == 34, s[0] == 0x51, s[1] == 0x20 { return Bech32.segwitAddress(hrp: hrp(chain), version: 1, program: Array(s[2...])) }
175
+ return nil
176
+ }
177
+
178
+ static func btc(_ sats: UInt64) -> String { Solana.units(sats, 8) + " BTC" }
179
+
180
+ private static func ownInputs(_ p: Psbt, script: [UInt8]) -> [Int] {
181
+ p.inputs.indices.filter { p.inputs[$0].witnessUtxo?.script == script }
182
+ }
183
+
184
+ static func summarize(_ p: Psbt, signer: String, chain: WalletChain) throws -> SignSummary {
185
+ guard let decoded = Bech32.decodeSegwit(hrp: hrp(chain), address: signer), decoded.version == 0, decoded.program.count == 20 else {
186
+ throw WalletError(.invalidRequest, "address is not a Bitcoin address of this network")
187
+ }
188
+ let script = [0x00, 0x14] + decoded.program
189
+ var rows: [SignSummary.Row] = [
190
+ .init(label: "Network", value: chain == .bitcoin ? "Bitcoin" : "Bitcoin testnet"),
191
+ .init(label: "Account", value: signer, mono: true),
192
+ ]
193
+ var warnings = [String]()
194
+ var inputs: UInt64 = 0
195
+ for i in p.inputs {
196
+ guard let utxo = i.witnessUtxo else { throw invalid("every input needs witness_utxo") }
197
+ let (sum, overflow) = inputs.addingReportingOverflow(utxo.amount)
198
+ guard !overflow else { throw invalid() }
199
+ inputs = sum
200
+ }
201
+ let mine = ownInputs(p, script: script)
202
+ guard !mine.isEmpty else { throw WalletError(.addressMismatch, "no input belongs to the signing account") }
203
+ if mine.count != p.inputs.count {
204
+ warnings.append("\(p.inputs.count - mine.count) of \(p.inputs.count) inputs are not yours and are not signed.")
205
+ }
206
+ rows.append(.init(label: "Spending", value: btc(inputs)))
207
+ var outputs: UInt64 = 0
208
+ for o in p.outputs {
209
+ let (sum, overflow) = outputs.addingReportingOverflow(o.amount)
210
+ guard !overflow else { throw invalid() }
211
+ outputs = sum
212
+ let to: String
213
+ if let a = address(script: o.script, chain: chain) {
214
+ to = a
215
+ } else {
216
+ to = "non-standard output"
217
+ warnings.append("An output has a script this wallet cannot show as an address.")
218
+ }
219
+ rows.append(o.script == script
220
+ ? .init(label: "Change back to you", value: btc(o.amount))
221
+ : .init(label: "Send \(btc(o.amount)) to", value: to, mono: true))
222
+ }
223
+ guard outputs <= inputs else { throw invalid("outputs exceed inputs") }
224
+ let fee = inputs - outputs
225
+ rows.append(.init(label: "Network fee", value: btc(fee)))
226
+ if fee.multipliedReportingOverflow(by: 10).partialValue > inputs && fee > 100_000 {
227
+ warnings.append("The network fee is more than 10% of the amount spent.")
228
+ }
229
+ return SignSummary(kind: .transaction, title: "Bitcoin transaction", rows: rows, body: nil, warnings: warnings, risk: .high)
230
+ }
231
+
232
+ // MARK: - Signing
233
+
234
+ private static func dsha(_ b: [UInt8]) -> [UInt8] { Bytes.sha256(Bytes.sha256(b)) }
235
+
236
+ private static func serializeOutputs(_ p: Psbt) -> [UInt8] {
237
+ p.outputs.flatMap { le64($0.amount) + compact($0.script.count) + $0.script }
238
+ }
239
+
240
+ /// BIP143 SIGHASH_ALL digest of input `index` spending a P2WPKH output of `pubkeyHash`.
241
+ static func sighash(_ p: Psbt, index: Int, pubkeyHash: [UInt8], amount: UInt64) -> [UInt8] {
242
+ let hashPrevouts = dsha(p.inputs.flatMap(\.prevout))
243
+ let hashSequence = dsha(p.inputs.flatMap(\.sequence))
244
+ let hashOutputs = dsha(serializeOutputs(p))
245
+ let scriptCode: [UInt8] = [0x19, 0x76, 0xa9, 0x14] + pubkeyHash + [0x88, 0xac]
246
+ let input = p.inputs[index]
247
+ var preimage: [UInt8] = p.version
248
+ preimage += hashPrevouts
249
+ preimage += hashSequence
250
+ preimage += input.prevout
251
+ preimage += scriptCode
252
+ preimage += le64(amount)
253
+ preimage += input.sequence
254
+ preimage += hashOutputs
255
+ preimage += p.locktime
256
+ preimage += [1, 0, 0, 0]
257
+ return dsha(preimage)
258
+ }
259
+
260
+ /// Signs the signer's inputs. Returns the signed PSBT (partial signatures added) and, when
261
+ /// every input was ours, the finalized raw transaction.
262
+ static func sign(_ psbt: Psbt, privateKey: [UInt8]) throws -> [String: String] {
263
+ var p = psbt
264
+ let pub = try Secp256k1.publicKey(of: privateKey, compressed: true)
265
+ let script = p2wpkh(pub)
266
+ let pubkeyHash = Array(script[2...])
267
+ let mine = ownInputs(p, script: script)
268
+ guard !mine.isEmpty else { throw WalletError(.addressMismatch) }
269
+ var witnesses = [Int: [UInt8]]()
270
+ for i in mine {
271
+ let amount = p.inputs[i].witnessUtxo!.amount
272
+ let sig = try Secp256k1.signDER(hash: sighash(p, index: i, pubkeyHash: pubkeyHash, amount: amount), key: privateKey) + [0x01]
273
+ let key = [0x02] + pub
274
+ if let at = p.inputs[i].map.firstIndex(where: { $0.key == key }) {
275
+ p.inputs[i].map[at].value = sig
276
+ } else {
277
+ p.inputs[i].map.append(KeyValue(key: key, value: sig))
278
+ }
279
+ witnesses[i] = compact(2) + compact(sig.count) + sig + compact(pub.count) + pub
280
+ }
281
+ var result = ["psbt": Bytes.base64(serialize(p))]
282
+ guard mine.count == p.inputs.count else { return result }
283
+ var tx = p.version + [0x00, 0x01] + compact(p.inputs.count)
284
+ for i in p.inputs { tx += i.prevout + [0x00] + i.sequence }
285
+ tx += compact(p.outputs.count) + serializeOutputs(p)
286
+ for i in p.inputs.indices { tx += witnesses[i]! }
287
+ tx += p.locktime
288
+ result["txHex"] = Bytes.hex(tx)
289
+ return result
290
+ }
291
+
292
+ /// Partial signatures (by input) for checks: the value of the PSBT_IN_PARTIAL_SIG entry for `pubkey`.
293
+ static func partialSignatures(_ p: Psbt, pubkey: [UInt8]) -> [[UInt8]?] {
294
+ p.inputs.map { i in i.map.first(where: { $0.key == [0x02] + pubkey })?.value }
295
+ }
296
+ }