@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.
- package/CHANGELOG.md +20 -0
- package/README.md +251 -2
- package/android/build.gradle +40 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +235 -0
- package/android/src/vendor/GENERATED.md +4 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +218 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +236 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +361 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +551 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +78 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/authonContext.d.ts +19 -0
- package/build/authonContext.d.ts.map +1 -0
- package/build/authonContext.js +20 -0
- package/build/authonContext.js.map +1 -0
- package/build/client.d.ts +92 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +431 -0
- package/build/client.js.map +1 -0
- package/build/errors.d.ts +25 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +15 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/native.d.ts +43 -0
- package/build/native.d.ts.map +1 -0
- package/build/native.js +12 -0
- package/build/native.js.map +1 -0
- package/build/types.d.ts +167 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +3 -0
- package/build/types.js.map +1 -0
- package/build/useAuthonWallet.d.ts +25 -0
- package/build/useAuthonWallet.d.ts.map +1 -0
- package/build/useAuthonWallet.js +77 -0
- package/build/useAuthonWallet.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AuthonReactNativeWallet.podspec +41 -0
- package/ios/AuthonWalletModule.swift +233 -0
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +232 -0
- package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
- package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
- package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
- package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
- package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
- package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
- package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +128 -0
- package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
- package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
- package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
- package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
- package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
- package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
- package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
- package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
- package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
- package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
- package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
- package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
- package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
- package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
- package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +361 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +654 -0
- package/ios/vendor/AuthonWallet/WalletError.swift +84 -0
- package/ios/vendor/CKeccak/LICENSE +22 -0
- package/ios/vendor/CKeccak/README.md +30 -0
- package/ios/vendor/CKeccak/byte_order.h +59 -0
- package/ios/vendor/CKeccak/include/ckeccak.h +13 -0
- package/ios/vendor/CKeccak/memzero.c +75 -0
- package/ios/vendor/CKeccak/memzero.h +8 -0
- package/ios/vendor/CKeccak/options.h +99 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -0
- package/ios/vendor/CKeccak/sha3.c +413 -0
- package/ios/vendor/CKeccak/sha3.h +89 -0
- package/ios/vendor/CMonocypher/LICENCE.md +173 -0
- package/ios/vendor/CMonocypher/README.md +27 -0
- package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
- package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
- package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
- package/ios/vendor/CMonocypher/monocypher.c +2956 -0
- package/ios/vendor/CSecp256k1/COPYING +19 -0
- package/ios/vendor/CSecp256k1/README.md +35 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
- package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
- package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
- package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
- package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
- package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
- package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
- package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
- package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
- package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
- package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
- package/ios/vendor/CSecp256k1/src/field.h +350 -0
- package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
- package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
- package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
- package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
- package/ios/vendor/CSecp256k1/src/group.h +212 -0
- package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
- package/ios/vendor/CSecp256k1/src/hash.h +44 -0
- package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
- package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
- package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
- package/ios/vendor/CSecp256k1/src/int128.h +90 -0
- package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
- package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
- package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
- package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
- package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
- package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
- package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
- package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
- package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
- package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
- package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
- package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
- package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
- package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
- package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
- package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
- package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
- package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
- package/ios/vendor/CSecp256k1/src/util.h +451 -0
- package/ios/vendor/GENERATED.md +4 -0
- package/package.json +66 -3
- package/src/authonContext.ts +30 -0
- package/src/client.ts +459 -0
- package/src/errors.ts +58 -0
- package/src/index.ts +26 -0
- package/src/native.ts +52 -0
- package/src/types.ts +171 -0
- 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
|
+
}
|