@authon/react-native-wallet 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +34 -4
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
  5. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
  6. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +16 -8
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
  21. package/build/client.d.ts +18 -2
  22. package/build/client.d.ts.map +1 -1
  23. package/build/client.js +88 -6
  24. package/build/client.js.map +1 -1
  25. package/build/errors.d.ts +8 -1
  26. package/build/errors.d.ts.map +1 -1
  27. package/build/errors.js +7 -1
  28. package/build/errors.js.map +1 -1
  29. package/build/index.d.ts +1 -1
  30. package/build/index.d.ts.map +1 -1
  31. package/build/native.d.ts +5 -1
  32. package/build/native.d.ts.map +1 -1
  33. package/build/native.js.map +1 -1
  34. package/build/types.d.ts +47 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/ios/AuthonReactNativeWallet.podspec +6 -2
  37. package/ios/AuthonWalletModule.swift +46 -3
  38. package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
  39. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  40. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  41. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  42. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  43. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  44. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  45. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
  46. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
  47. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
  48. package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
  49. package/ios/vendor/CKeccak/README.md +6 -3
  50. package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
  51. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  52. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  53. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  54. package/ios/vendor/CMonocypher/README.md +27 -0
  55. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  56. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  57. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  58. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  59. package/ios/vendor/GENERATED.md +1 -1
  60. package/package.json +3 -2
  61. package/src/client.ts +90 -6
  62. package/src/errors.ts +7 -1
  63. package/src/index.ts +5 -0
  64. package/src/native.ts +4 -2
  65. package/src/types.ts +52 -1
@@ -0,0 +1,191 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Chains/XRP.swift. Do not edit: change the source and run the script.
2
+ import CryptoKit
3
+ import Foundation
4
+
5
+ /// XRP Ledger signing (docs/wallet/NATIVE.md section 12, `apps/wallet/src/xrp.ts`). The app sends
6
+ /// the output of ripple-binary-codec encodeForSigning (prefix STX\0); the wallet decodes it only
7
+ /// to describe it and signs SHA-512Half(blob) with secp256k1, DER, low S.
8
+ enum XRP {
9
+ static let maxBlob = 4096
10
+ static let signingPrefix: [UInt8] = [0x53, 0x54, 0x58, 0x00]
11
+
12
+ struct Tx {
13
+ var transactionType: Int?
14
+ var account: String?
15
+ var destination: String?
16
+ /// Drops for XRP amounts; `.some(nil)` for issued-token amounts.
17
+ var amount: UInt64??
18
+ var fee: UInt64??
19
+ var sequence: Int?
20
+ var destinationTag: Int?
21
+ var lastLedgerSequence: Int?
22
+ var signingPubKey: [UInt8]?
23
+ var memos: [String] = []
24
+ }
25
+
26
+ private static func invalid(_ why: String = "not a valid XRP signing blob") -> WalletError { WalletError(.invalidRequest, why) }
27
+
28
+ static func address(accountId: [UInt8]) throws -> String {
29
+ guard accountId.count == 20 else { throw invalid() }
30
+ let payload = [0x00] + accountId
31
+ return Base58.encode(payload + ChainAddresses.checksum4(payload), alphabet: Base58.xrp)
32
+ }
33
+
34
+ static func address(publicKey compressed: [UInt8]) throws -> String {
35
+ try address(accountId: ChainAddresses.hash160(compressed))
36
+ }
37
+
38
+ private struct Reader {
39
+ let b: [UInt8]
40
+ var pos = 0
41
+ var done: Bool { pos >= b.count }
42
+
43
+ mutating func u8() throws -> Int {
44
+ guard pos < b.count else { throw invalid() }
45
+ pos += 1
46
+ return Int(b[pos - 1])
47
+ }
48
+
49
+ mutating func take(_ n: Int) throws -> [UInt8] {
50
+ guard pos + n <= b.count else { throw invalid() }
51
+ pos += n
52
+ return Array(b[(pos - n)..<pos])
53
+ }
54
+
55
+ mutating func uint(_ n: Int) throws -> Int { try take(n).reduce(0) { $0 * 256 + Int($1) } }
56
+
57
+ /// XRPL variable-length prefix.
58
+ mutating func vl() throws -> [UInt8] {
59
+ let b1 = try u8()
60
+ if b1 <= 192 { return try take(b1) }
61
+ if b1 <= 240 { return try take(193 + (b1 - 193) * 256 + (try u8())) }
62
+ if b1 <= 254 { return try take(12481 + (b1 - 241) * 65536 + (try u8()) * 256 + (try u8())) }
63
+ throw invalid()
64
+ }
65
+
66
+ mutating func field() throws -> (type: Int, code: Int) {
67
+ let h = try u8()
68
+ var type = h >> 4
69
+ var code = h & 0x0f
70
+ if type == 0 { type = try u8() }
71
+ if code == 0 { code = try u8() }
72
+ return (type, code)
73
+ }
74
+
75
+ mutating func amount() throws -> UInt64? {
76
+ guard pos < b.count else { throw invalid() }
77
+ if b[pos] & 0x80 != 0 {
78
+ _ = try take(48) // issued token: 8 value + 20 currency + 20 issuer
79
+ return nil
80
+ }
81
+ let v = try take(8).reduce(UInt64(0)) { $0 << 8 | UInt64($1) }
82
+ return v & 0x3fff_ffff_ffff_ffff
83
+ }
84
+ }
85
+
86
+ /// Reads one STObject until its end marker (or the end of the blob at the top level).
87
+ /// `memos` collects MemoData inside the top-level Memos array.
88
+ private static func readObject(_ r: inout Reader, _ tx: inout Tx?, depth: Int, memos: inout [String]?) throws {
89
+ guard depth <= 4 else { throw invalid() }
90
+ while !r.done {
91
+ let (type, code) = try r.field()
92
+ if type == 14 && code == 1 { return } // ObjectEndMarker
93
+ if type == 15 && code == 1 { return } // ArrayEndMarker
94
+ switch type {
95
+ case 1:
96
+ let v = try r.uint(2)
97
+ if code == 2 { tx?.transactionType = v }
98
+ case 2:
99
+ let v = try r.uint(4)
100
+ if code == 4 { tx?.sequence = v } else if code == 14 { tx?.destinationTag = v } else if code == 27 { tx?.lastLedgerSequence = v }
101
+ case 3: _ = try r.take(8)
102
+ case 4: _ = try r.take(16)
103
+ case 5: _ = try r.take(32)
104
+ case 16: _ = try r.take(1)
105
+ case 17: _ = try r.take(20)
106
+ case 6:
107
+ let v = try r.amount()
108
+ if code == 1 { tx?.amount = .some(v) } else if code == 8 { tx?.fee = .some(v) }
109
+ case 7:
110
+ let v = try r.vl()
111
+ if tx != nil, code == 3 {
112
+ tx?.signingPubKey = v
113
+ } else if tx == nil, code == 13 {
114
+ memos?.append(String(decoding: v, as: UTF8.self))
115
+ }
116
+ case 8:
117
+ let v = try r.vl()
118
+ if tx != nil, code == 1 { tx?.account = try address(accountId: v) } else if tx != nil, code == 3 { tx?.destination = try address(accountId: v) }
119
+ case 14:
120
+ var none: Tx? = nil
121
+ try readObject(&r, &none, depth: depth + 1, memos: &memos)
122
+ case 15:
123
+ var none: Tx? = nil
124
+ if tx != nil && code == 9 {
125
+ var collected: [String]? = []
126
+ try readObject(&r, &none, depth: depth + 1, memos: &collected)
127
+ tx?.memos += collected ?? []
128
+ } else {
129
+ try readObject(&r, &none, depth: depth + 1, memos: &memos)
130
+ }
131
+ default:
132
+ throw invalid("XRP field type \(type) is not supported")
133
+ }
134
+ }
135
+ }
136
+
137
+ static func parse(_ blob: [UInt8]) throws -> Tx {
138
+ guard blob.count >= 5, blob.count <= maxBlob, Array(blob.prefix(4)) == signingPrefix else { throw invalid() }
139
+ var r = Reader(b: Array(blob.dropFirst(4)))
140
+ var tx: Tx? = Tx()
141
+ var memos: [String]? = nil
142
+ try readObject(&r, &tx, depth: 0, memos: &memos)
143
+ guard let parsed = tx, parsed.transactionType != nil, parsed.account != nil, parsed.signingPubKey != nil else { throw invalid() }
144
+ return parsed
145
+ }
146
+
147
+ static func xrp(_ drops: UInt64) -> String { Solana.units(drops, 6) + " XRP" }
148
+
149
+ private static let types: [Int: String] = [0: "Payment", 3: "AccountSet", 7: "OfferCreate", 8: "OfferCancel", 12: "SetRegularKey", 20: "TrustSet"]
150
+
151
+ static func summarize(_ tx: Tx, signer: String, signerPublicKey: [UInt8]) throws -> SignSummary {
152
+ guard tx.account == signer else { throw WalletError(.addressMismatch, "the transaction account is not the signing account") }
153
+ guard tx.signingPubKey == signerPublicKey else {
154
+ throw WalletError(.invalidRequest, "SigningPubKey must be the signing account public key")
155
+ }
156
+ let type = tx.transactionType ?? 0
157
+ var rows: [SignSummary.Row] = [
158
+ .init(label: "Network", value: "XRP Ledger"),
159
+ .init(label: "Account", value: signer, mono: true),
160
+ .init(label: "Type", value: types[type] ?? "Type \(type)"),
161
+ ]
162
+ var warnings = [String]()
163
+ if type == 0 {
164
+ switch tx.amount {
165
+ case .some(.none):
166
+ rows.append(.init(label: "Send", value: "Issued token amount"))
167
+ warnings.append("Issued-token payments cannot be described in detail.")
168
+ case .some(.some(let drops)):
169
+ rows.append(.init(label: "Send", value: xrp(drops)))
170
+ case .none:
171
+ break
172
+ }
173
+ if let d = tx.destination, !d.isEmpty { rows.append(.init(label: "To", value: d, mono: true)) }
174
+ if let tag = tx.destinationTag { rows.append(.init(label: "Destination tag", value: String(tag))) }
175
+ } else {
176
+ warnings.append("\(types[type] ?? "This") transaction cannot be described in detail. Approve only if you trust the app.")
177
+ }
178
+ for m in tx.memos { rows.append(.init(label: "Memo", value: m)) }
179
+ if case .some(.some(let fee)) = tx.fee { rows.append(.init(label: "Network fee", value: xrp(fee))) }
180
+ if let s = tx.sequence { rows.append(.init(label: "Sequence", value: String(s))) }
181
+ if let l = tx.lastLedgerSequence { rows.append(.init(label: "Valid until ledger", value: String(l))) }
182
+ return SignSummary(kind: .transaction, title: "XRP Ledger transaction", rows: rows, body: nil, warnings: warnings, risk: .high)
183
+ }
184
+
185
+ static func sha512Half(_ data: [UInt8]) -> [UInt8] { Array(Array(SHA512.hash(data: data))[0..<32]) }
186
+
187
+ /// DER (low S) over SHA-512Half of the signing blob, uppercase hex like ripple-keypairs.
188
+ static func sign(_ blob: [UInt8], privateKey: [UInt8]) throws -> [String: String] {
189
+ ["signature": Bytes.hex(try Secp256k1.signDER(hash: sha512Half(blob), key: privateKey)).uppercased()]
190
+ }
191
+ }
@@ -0,0 +1,278 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Crypto/Chains.swift. Do not edit: change the source and run the script.
2
+ #if canImport(CKeccak)
3
+ import CKeccak
4
+ #endif
5
+ import CryptoKit
6
+ import Foundation
7
+
8
+ /// Chains a wallet derives addresses for (docs/wallet/NATIVE.md section 11, `apps/wallet/src/chains.ts`).
9
+ /// Paths match standard wallets, so the same seed gives the same addresses everywhere.
10
+ /// Signing is EVM only for now.
11
+ public enum WalletChain: String, CaseIterable, Sendable {
12
+ case evm
13
+ case tron
14
+ case xrp
15
+ case bitcoin
16
+ case bitcoinTestnet = "bitcoin-testnet"
17
+ case solana
18
+
19
+ public func path(_ index: Int) -> String {
20
+ switch self {
21
+ case .evm: return "m/44'/60'/0'/0/\(index)"
22
+ case .tron: return "m/44'/195'/\(index)'/0/0"
23
+ case .xrp: return "m/44'/144'/\(index)'/0/0"
24
+ case .bitcoin: return "m/84'/0'/\(index)'/0/0"
25
+ case .bitcoinTestnet: return "m/84'/1'/\(index)'/0/0"
26
+ case .solana: return "m/44'/501'/\(index)'/0'"
27
+ }
28
+ }
29
+ }
30
+
31
+ enum ChainAddresses {
32
+ static func ripemd(_ data: [UInt8]) -> [UInt8] {
33
+ var out = [UInt8](repeating: 0, count: 20)
34
+ data.withUnsafeBufferPointer { input in
35
+ out.withUnsafeMutableBufferPointer { ripemd160(input.baseAddress, UInt32(input.count), $0.baseAddress) }
36
+ }
37
+ return out
38
+ }
39
+
40
+ static func hash160(_ data: [UInt8]) -> [UInt8] { ripemd(Bytes.sha256(data)) }
41
+
42
+ static func checksum4(_ payload: [UInt8]) -> [UInt8] { Array(Bytes.sha256(Bytes.sha256(payload))[0..<4]) }
43
+
44
+ /// Address of a secp256k1 key on a non-Solana chain.
45
+ static func secpAddress(_ chain: WalletChain, privateKey: [UInt8]) throws -> String {
46
+ switch chain {
47
+ case .evm:
48
+ return try EvmAddress.of(privateKey: privateKey)
49
+ case .tron:
50
+ let uncompressed = try Secp256k1.publicKey(of: privateKey, compressed: false)
51
+ let payload = [0x41] + Array(Keccak.hash256(Array(uncompressed.dropFirst()))[12..<32])
52
+ return Base58.encode(payload + checksum4(payload), alphabet: Base58.bitcoin)
53
+ case .xrp:
54
+ let payload = [0x00] + hash160(try Secp256k1.publicKey(of: privateKey, compressed: true))
55
+ return Base58.encode(payload + checksum4(payload), alphabet: Base58.xrp)
56
+ case .bitcoin, .bitcoinTestnet:
57
+ let program = hash160(try Secp256k1.publicKey(of: privateKey, compressed: true))
58
+ return Bech32.segwitAddress(hrp: chain == .bitcoin ? "bc" : "tb", version: 0, program: program)
59
+ case .solana:
60
+ throw WalletError(.internal, "solana is not a secp256k1 chain")
61
+ }
62
+ }
63
+
64
+ /// secp256k1 key of account `index` on a BIP32 chain other than EVM. Callers wipe it.
65
+ static func secpPrivateKey(_ secret: [UInt8], chain: WalletChain, index: Int) throws -> [UInt8] {
66
+ guard chain != .solana, chain != .evm else { throw WalletError(.internal, "not a BIP32 chain") }
67
+ guard WalletShares.isAccountIndex(index) else { throw WalletError(.invalidRequest, "invalid account index") }
68
+ guard WalletShares.isHdSecret(secret) else { throw WalletError(.accountUnavailable, "this wallet has only EVM account 0") }
69
+ var seed = try BIP39.seed(mnemonic: try BIP39.mnemonic(entropy: secret))
70
+ defer { seed.wipe() }
71
+ var root = try HDNode(seed: seed)
72
+ defer { root.wipe() }
73
+ var node = try root.derive(chain.path(index))
74
+ defer { node.wipe() }
75
+ return node.key
76
+ }
77
+
78
+ /// SLIP-0010 ed25519 derivation; every level is hardened. Callers wipe the 32-byte key.
79
+ static func ed25519Key(seed: [UInt8], path: String) throws -> [UInt8] {
80
+ func hmac(_ key: [UInt8], _ data: [UInt8]) -> [UInt8] {
81
+ Array(HMAC<SHA512>.authenticationCode(for: data, using: SymmetricKey(data: key)))
82
+ }
83
+ var i = hmac(Array("ed25519 seed".utf8), seed)
84
+ var key = Array(i[0..<32])
85
+ var code = Array(i[32..<64])
86
+ defer {
87
+ i.wipe()
88
+ code.wipe()
89
+ }
90
+ for part in path.split(separator: "/").dropFirst() {
91
+ guard part.hasSuffix("'"), let n = UInt32(part.dropLast()), n < HDNode.hardened else {
92
+ key.wipe()
93
+ throw WalletError(.internal, "ed25519 paths are hardened only")
94
+ }
95
+ let index = n + HDNode.hardened
96
+ var data = [0] + key + [UInt8(index >> 24), UInt8(truncatingIfNeeded: index >> 16), UInt8(truncatingIfNeeded: index >> 8), UInt8(truncatingIfNeeded: index)]
97
+ i.wipe()
98
+ i = hmac(code, data)
99
+ data.wipe()
100
+ key.wipe()
101
+ code.wipe()
102
+ key = Array(i[0..<32])
103
+ code = Array(i[32..<64])
104
+ }
105
+ return key
106
+ }
107
+
108
+ static func solanaAddress(seed: [UInt8], index: Int) throws -> String {
109
+ var key = try ed25519Key(seed: seed, path: WalletChain.solana.path(index))
110
+ defer { key.wipe() }
111
+ let pub = try Curve25519.Signing.PrivateKey(rawRepresentation: key).publicKey.rawRepresentation
112
+ return Base58.encode(Array(pub), alphabet: Base58.bitcoin)
113
+ }
114
+
115
+ /// Addresses of accounts start..<start+count on `chain`. Only HD (BIP39) wallets have chains other than EVM.
116
+ static func addresses(_ secret: [UInt8], chain: WalletChain, start: Int, count: Int) throws -> [String] {
117
+ guard WalletShares.isAccountIndex(start), count >= 1, WalletShares.isAccountIndex(start + count - 1) else {
118
+ throw WalletError(.invalidRequest, "invalid account range")
119
+ }
120
+ if chain == .evm { return try WalletShares.addresses(secret, start: start, count: count) }
121
+ guard WalletShares.isHdSecret(secret) else { throw WalletError(.accountUnavailable, "this wallet has only EVM account 0") }
122
+ var seed = try BIP39.seed(mnemonic: try BIP39.mnemonic(entropy: secret))
123
+ defer { seed.wipe() }
124
+ var out = [String]()
125
+ out.reserveCapacity(count)
126
+ if chain == .solana {
127
+ for i in start..<(start + count) { out.append(try solanaAddress(seed: seed, index: i)) }
128
+ return out
129
+ }
130
+ var root = try HDNode(seed: seed)
131
+ defer { root.wipe() }
132
+ for i in start..<(start + count) {
133
+ var node = try root.derive(chain.path(i))
134
+ defer { node.wipe() }
135
+ out.append(try secpAddress(chain, privateKey: node.key))
136
+ }
137
+ return out
138
+ }
139
+
140
+ /// Compressed public keys (uppercase hex) of XRP accounts start..<start+count (the SigningPubKey apps need).
141
+ static func xrpPublicKeys(_ secret: [UInt8], start: Int, count: Int) throws -> [String] {
142
+ guard WalletShares.isHdSecret(secret) else { throw WalletError(.accountUnavailable, "this wallet has only EVM account 0") }
143
+ var seed = try BIP39.seed(mnemonic: try BIP39.mnemonic(entropy: secret))
144
+ defer { seed.wipe() }
145
+ var root = try HDNode(seed: seed)
146
+ defer { root.wipe() }
147
+ return try (start..<(start + count)).map { i in
148
+ var node = try root.derive(WalletChain.xrp.path(i))
149
+ defer { node.wipe() }
150
+ return Bytes.hex(try Secp256k1.publicKey(of: node.key, compressed: true)).uppercased()
151
+ }
152
+ }
153
+ }
154
+
155
+ /// Base58 (Bitcoin and XRP alphabets).
156
+ enum Base58 {
157
+ static let bitcoin = Array("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".utf8)
158
+ static let xrp = Array("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".utf8)
159
+
160
+ static func encode(_ bytes: [UInt8], alphabet: [UInt8]) -> String {
161
+ let zeros = bytes.prefix { $0 == 0 }.count
162
+ var digits = [UInt8]() // little-endian base-58 digits
163
+ for byte in bytes[zeros...] {
164
+ var carry = Int(byte)
165
+ for j in 0..<digits.count {
166
+ carry += Int(digits[j]) << 8
167
+ digits[j] = UInt8(carry % 58)
168
+ carry /= 58
169
+ }
170
+ while carry > 0 {
171
+ digits.append(UInt8(carry % 58))
172
+ carry /= 58
173
+ }
174
+ }
175
+ let out = [UInt8](repeating: alphabet[0], count: zeros) + digits.reversed().map { alphabet[Int($0)] }
176
+ return String(decoding: out, as: UTF8.self)
177
+ }
178
+
179
+ static func decode(_ text: String, alphabet: [UInt8]) -> [UInt8]? {
180
+ let chars = Array(text.utf8)
181
+ let zeros = chars.prefix { $0 == alphabet[0] }.count
182
+ var bytes = [UInt8]() // little-endian base-256
183
+ for c in chars[zeros...] {
184
+ guard let value = alphabet.firstIndex(of: c) else { return nil }
185
+ var carry = value
186
+ for j in 0..<bytes.count {
187
+ carry += Int(bytes[j]) * 58
188
+ bytes[j] = UInt8(carry & 0xff)
189
+ carry >>= 8
190
+ }
191
+ while carry > 0 {
192
+ bytes.append(UInt8(carry & 0xff))
193
+ carry >>= 8
194
+ }
195
+ }
196
+ return [UInt8](repeating: 0, count: zeros) + bytes.reversed()
197
+ }
198
+ }
199
+
200
+ /// Bech32 (BIP173) for SegWit v0 addresses.
201
+ enum Bech32 {
202
+ static let charset = Array("qpzry9x8gf2tvdw0s3jn54khce6mua7l".utf8)
203
+ private static let generator: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
204
+
205
+ private static func polymod(_ values: [UInt8]) -> UInt32 {
206
+ var chk: UInt32 = 1
207
+ for v in values {
208
+ let top = chk >> 25
209
+ chk = (chk & 0x1ffffff) << 5 ^ UInt32(v)
210
+ for i in 0..<5 where (top >> UInt32(i)) & 1 == 1 { chk ^= generator[i] }
211
+ }
212
+ return chk
213
+ }
214
+
215
+ private static func hrpExpand(_ hrp: [UInt8]) -> [UInt8] {
216
+ hrp.map { $0 >> 5 } + [0] + hrp.map { $0 & 31 }
217
+ }
218
+
219
+ /// Bech32 (BIP173) or, with `m`, Bech32m (BIP350, SegWit v1+).
220
+ static func encode(hrp: String, data: [UInt8], m: Bool = false) -> String {
221
+ let h = Array(hrp.lowercased().utf8)
222
+ let mod = polymod(hrpExpand(h) + data + [0, 0, 0, 0, 0, 0]) ^ (m ? 0x2bc8_30a3 : 1)
223
+ let checksum = (0..<6).map { UInt8((mod >> UInt32(5 * (5 - $0))) & 31) }
224
+ return String(decoding: h + [UInt8(ascii: "1")] + (data + checksum).map { charset[Int($0)] }, as: UTF8.self)
225
+ }
226
+
227
+ /// (hrp, 5-bit data without checksum), or nil for an invalid string.
228
+ static func decode(_ text: String) -> (hrp: String, data: [UInt8])? {
229
+ let bytes = Array(text.utf8)
230
+ guard bytes.count <= 90, bytes.allSatisfy({ $0 >= 33 && $0 <= 126 }) else { return nil }
231
+ let lower = text.lowercased(), upper = text.uppercased()
232
+ guard text == lower || text == upper else { return nil }
233
+ let chars = Array(lower.utf8)
234
+ guard let sep = chars.lastIndex(of: UInt8(ascii: "1")), sep >= 1, sep + 7 <= chars.count else { return nil }
235
+ let hrp = Array(chars[0..<sep])
236
+ var data = [UInt8]()
237
+ for c in chars[(sep + 1)...] {
238
+ guard let v = charset.firstIndex(of: c) else { return nil }
239
+ data.append(UInt8(v))
240
+ }
241
+ guard polymod(hrpExpand(hrp) + data) == 1 else { return nil }
242
+ return (String(decoding: hrp, as: UTF8.self), Array(data.dropLast(6)))
243
+ }
244
+
245
+ /// Regroups bits (BIP173 convertbits).
246
+ static func convertBits(_ data: [UInt8], from: Int, to: Int, pad: Bool) -> [UInt8]? {
247
+ var acc = 0, bits = 0
248
+ var out = [UInt8]()
249
+ let maxv = (1 << to) - 1
250
+ for v in data {
251
+ guard Int(v) >> from == 0 else { return nil }
252
+ acc = acc << from | Int(v)
253
+ bits += from
254
+ while bits >= to {
255
+ bits -= to
256
+ out.append(UInt8((acc >> bits) & maxv))
257
+ }
258
+ }
259
+ if pad {
260
+ if bits > 0 { out.append(UInt8((acc << (to - bits)) & maxv)) }
261
+ } else if bits >= from || (acc << (to - bits)) & maxv != 0 {
262
+ return nil
263
+ }
264
+ return out
265
+ }
266
+
267
+ static func segwitAddress(hrp: String, version: UInt8, program: [UInt8]) -> String {
268
+ encode(hrp: hrp, data: [version] + convertBits(program, from: 8, to: 5, pad: true)!, m: version > 0)
269
+ }
270
+
271
+ /// (version, program) of a SegWit address with the expected HRP.
272
+ static func decodeSegwit(hrp: String, address: String) -> (version: UInt8, program: [UInt8])? {
273
+ guard let d = decode(address), d.hrp == hrp, let version = d.data.first, version <= 16,
274
+ let program = convertBits(Array(d.data.dropFirst()), from: 5, to: 8, pad: false),
275
+ (2...40).contains(program.count), version != 0 || program.count == 20 || program.count == 32 else { return nil }
276
+ return (version, program)
277
+ }
278
+ }
@@ -87,6 +87,30 @@ enum Secp256k1 {
87
87
  }
88
88
  }
89
89
 
90
+ /// RFC 6979 deterministic, low-s ECDSA over a 32-byte hash, DER-encoded.
91
+ static func signDER(hash: [UInt8], key: [UInt8]) throws -> [UInt8] {
92
+ guard hash.count == 32, key.count == 32 else { throw WalletError(.internal, "invalid signing input") }
93
+ return try withContext { ctx in
94
+ var sig = secp256k1_ecdsa_signature()
95
+ guard secp256k1_ecdsa_sign(ctx, &sig, hash, key, nil, nil) == 1 else { throw WalletError(.internal, "signing failed") }
96
+ var out = [UInt8](repeating: 0, count: 72)
97
+ var len = out.count
98
+ guard secp256k1_ecdsa_signature_serialize_der(ctx, &out, &len, &sig) == 1 else { throw WalletError(.internal, "signing failed") }
99
+ return Array(out[0..<len])
100
+ }
101
+ }
102
+
103
+ /// Verifies a DER signature (tests and self-checks).
104
+ static func verifyDER(_ der: [UInt8], hash: [UInt8], publicKey: [UInt8]) -> Bool {
105
+ withContext { ctx in
106
+ var sig = secp256k1_ecdsa_signature()
107
+ var pub = secp256k1_pubkey()
108
+ guard secp256k1_ecdsa_signature_parse_der(ctx, &sig, der, der.count) == 1,
109
+ secp256k1_ec_pubkey_parse(ctx, &pub, publicKey, publicKey.count) == 1 else { return false }
110
+ return secp256k1_ecdsa_verify(ctx, &sig, hash, &pub) == 1
111
+ }
112
+ }
113
+
90
114
  /// Recovers the uncompressed public key of a signature (used to verify proofs in tests and checks).
91
115
  static func recover(hash: [UInt8], r: [UInt8], s: [UInt8], recoveryId: Int) -> [UInt8]? {
92
116
  guard hash.count == 32, r.count == 32, s.count == 32, (0...3).contains(recoveryId) else { return nil }
@@ -35,6 +35,10 @@ public struct WalletAppConfig: Equatable, Sendable {
35
35
  public var prfSaltVersion: BackupVersion
36
36
  public var rpId: String
37
37
  public var minFrameProtocol: Int?
38
+ /// `X-Request-Id` of the app-config response.
39
+ public var diagnosticId: String? = nil
40
+ /// False when the API answered 403/404 (unknown publishable key or project).
41
+ var projectFound = true
38
42
  }
39
43
 
40
44
  enum ProofKind: String {
@@ -90,6 +94,11 @@ final class WalletAPIClient: @unchecked Sendable {
90
94
  struct Response {
91
95
  var status: Int
92
96
  var data: JSONObject
97
+ /// `X-Request-Id` response header.
98
+ var requestId: String?
99
+
100
+ /// The server's diagnostic ID: `requestId` in the body, else the header.
101
+ var diagnosticId: String? { data["requestId"]?.stringValue ?? requestId }
93
102
  }
94
103
 
95
104
  private func call(_ method: String, _ path: String, _ token: String?, _ body: JSONValue? = nil) async throws -> Response {
@@ -110,20 +119,21 @@ final class WalletAPIClient: @unchecked Sendable {
110
119
  do {
111
120
  (data, response) = try await session.data(for: req)
112
121
  } catch {
113
- throw WalletError(.apiError, "network error")
122
+ throw WalletError.network()
114
123
  }
115
- let status = (response as? HTTPURLResponse)?.statusCode ?? 0
124
+ let http = response as? HTTPURLResponse
116
125
  let parsed = (try? JSONValue.parse(String(decoding: data, as: UTF8.self)))?.objectValue ?? JSONObject()
117
- return Response(status: status, data: parsed)
126
+ return Response(status: http?.statusCode ?? 0, data: parsed, requestId: http?.value(forHTTPHeaderField: "X-Request-Id"))
118
127
  }
119
128
 
120
129
  private func ensureOk(_ res: Response) throws -> JSONObject {
121
130
  if (200..<300).contains(res.status) { return res.data }
131
+ let id = res.diagnosticId
122
132
  if case .string(let code) = res.data["code"] ?? .null {
123
- if Self.restoreCodes.contains(code) { throw WalletError(.needsRestore) }
124
- if Self.knownCodes.contains(code), let c = WalletErrorCode(rawValue: code) { throw WalletError(c) }
133
+ if Self.restoreCodes.contains(code) { throw WalletError(.needsRestore, diagnosticId: id) }
134
+ if Self.knownCodes.contains(code), let c = WalletErrorCode(rawValue: code) { throw WalletError(c, diagnosticId: id) }
125
135
  }
126
- throw WalletError(Self.statusCodes[res.status] ?? .apiError)
136
+ throw WalletError(Self.statusCodes[res.status] ?? .apiError, diagnosticId: id)
127
137
  }
128
138
 
129
139
  private static func string(_ v: JSONValue?) -> String? { v?.stringValue }
@@ -184,7 +194,7 @@ final class WalletAPIClient: @unchecked Sendable {
184
194
  let query = "platform=\(Self.encode(platform))&appId=\(Self.encode(appId))"
185
195
  let res = try await call("GET", "/app-config?\(query)", nil)
186
196
  if res.status == 403 || res.status == 404 {
187
- return WalletAppConfig(projectId: nil, allowed: false, approved: false, enabled: false, chainIds: [], confirmSigning: true, prfSaltVersion: .v1, rpId: "wallet.authon.dev", minFrameProtocol: nil)
197
+ return WalletAppConfig(projectId: nil, allowed: false, approved: false, enabled: false, chainIds: [], confirmSigning: true, prfSaltVersion: .v1, rpId: "wallet.authon.dev", minFrameProtocol: nil, diagnosticId: res.diagnosticId, projectFound: false)
188
198
  }
189
199
  let data = try ensureOk(res)
190
200
  let cfg = data["walletConfig"]?.objectValue ?? JSONObject()
@@ -198,7 +208,8 @@ final class WalletAPIClient: @unchecked Sendable {
198
208
  confirmSigning: cfg["confirmSigning"] != .bool(false),
199
209
  prfSaltVersion: Self.int(cfg["prfSaltVersion"]) == 2 ? .v2 : .v1,
200
210
  rpId: rpId,
201
- minFrameProtocol: Self.int(data["minFrameProtocol"])
211
+ minFrameProtocol: Self.int(data["minFrameProtocol"]),
212
+ diagnosticId: res.diagnosticId
202
213
  )
203
214
  }
204
215