@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,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
+ }
@@ -0,0 +1,207 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Chains/Solana.swift. Do not edit: change the source and run the script.
2
+ #if canImport(CMonocypher)
3
+ import CMonocypher
4
+ #endif
5
+ import Foundation
6
+
7
+ /// Solana transaction signing (docs/wallet/NATIVE.md section 12, `apps/wallet/src/solana.ts`).
8
+ /// The app builds the serialized Message; the wallet parses it only to describe it and signs
9
+ /// the exact bytes.
10
+ enum Solana {
11
+ static let maxMessage = 1232
12
+
13
+ static let system = "11111111111111111111111111111111"
14
+ static let token = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
15
+ static let token2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
16
+ static let associatedToken = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
17
+ static let computeBudget = "ComputeBudget111111111111111111111111111111"
18
+ static let memo = "MemoSq4gqABAXKb96qnH8TyNoJ6CDrADxwaj4uuQ"
19
+
20
+ struct Instruction {
21
+ var programId: String
22
+ /// nil for an account that comes from an address lookup table.
23
+ var accounts: [String?]
24
+ var data: [UInt8]
25
+ }
26
+
27
+ struct Message {
28
+ var isV0: Bool
29
+ var numRequiredSignatures: Int
30
+ var accountKeys: [String]
31
+ var recentBlockhash: String
32
+ var instructions: [Instruction]
33
+ /// Accounts loaded from address lookup tables (v0 only).
34
+ var lookupAccounts: Int
35
+ }
36
+
37
+ private static func invalid() -> WalletError { WalletError(.invalidRequest, "not a valid Solana message") }
38
+
39
+ private struct Reader {
40
+ let bytes: [UInt8]
41
+ var pos = 0
42
+ var done: Bool { pos == bytes.count }
43
+
44
+ mutating func u8() throws -> Int {
45
+ guard pos < bytes.count else { throw invalid() }
46
+ pos += 1
47
+ return Int(bytes[pos - 1])
48
+ }
49
+
50
+ mutating func take(_ n: Int) throws -> [UInt8] {
51
+ guard n >= 0, pos + n <= bytes.count else { throw invalid() }
52
+ pos += n
53
+ return Array(bytes[(pos - n)..<pos])
54
+ }
55
+
56
+ /// compact-u16 (shortvec)
57
+ mutating func len() throws -> Int {
58
+ var value = 0
59
+ for i in 0..<3 {
60
+ let b = try u8()
61
+ value |= (b & 0x7f) << (7 * i)
62
+ if b & 0x80 == 0 { return value }
63
+ }
64
+ throw invalid()
65
+ }
66
+ }
67
+
68
+ static func parse(_ bytes: [UInt8]) throws -> Message {
69
+ guard !bytes.isEmpty, bytes.count <= maxMessage else { throw invalid() }
70
+ var r = Reader(bytes: bytes)
71
+ var isV0 = false
72
+ var first = try r.u8()
73
+ if first & 0x80 != 0 {
74
+ guard first & 0x7f == 0 else { throw WalletError(.invalidRequest, "unsupported Solana message version") }
75
+ isV0 = true
76
+ first = try r.u8()
77
+ }
78
+ let required = first
79
+ _ = try r.u8() // readonly signed
80
+ _ = try r.u8() // readonly unsigned
81
+ let keyCount = try r.len()
82
+ var keys = [String]()
83
+ for _ in 0..<keyCount { keys.append(Base58.encode(try r.take(32), alphabet: Base58.bitcoin)) }
84
+ let blockhash = Base58.encode(try r.take(32), alphabet: Base58.bitcoin)
85
+ var raw = [(program: Int, accounts: [Int], data: [UInt8])]()
86
+ for _ in 0..<(try r.len()) {
87
+ let program = try r.u8()
88
+ var accounts = [Int]()
89
+ for _ in 0..<(try r.len()) { accounts.append(try r.u8()) }
90
+ let data = try r.take(try r.len())
91
+ raw.append((program, accounts, data))
92
+ }
93
+ var lookups = 0
94
+ if isV0 {
95
+ for _ in 0..<(try r.len()) {
96
+ _ = try r.take(32)
97
+ lookups += try r.take(try r.len()).count
98
+ lookups += try r.take(try r.len()).count
99
+ }
100
+ }
101
+ guard r.done, required >= 1, required <= keyCount else { throw invalid() }
102
+ let total = keyCount + lookups
103
+ func key(_ i: Int) throws -> String? {
104
+ guard i < total else { throw invalid() }
105
+ return i < keyCount ? keys[i] : nil
106
+ }
107
+ let instructions = try raw.map { ix -> Instruction in
108
+ // Programs must be static keys.
109
+ guard let program = try key(ix.program) else { throw invalid() }
110
+ return Instruction(programId: program, accounts: try ix.accounts.map(key), data: ix.data)
111
+ }
112
+ return Message(isV0: isV0, numRequiredSignatures: required, accountKeys: keys, recentBlockhash: blockhash, instructions: instructions, lookupAccounts: lookups)
113
+ }
114
+
115
+ private static func u64(_ d: [UInt8], _ at: Int) throws -> UInt64 {
116
+ guard d.count >= at + 8 else { throw invalid() }
117
+ return (0..<8).reduce(UInt64(0)) { $0 | UInt64(d[at + $1]) << (8 * UInt64($1)) }
118
+ }
119
+
120
+ private static func u32(_ d: [UInt8], _ at: Int) throws -> UInt32 {
121
+ guard d.count >= at + 4 else { throw invalid() }
122
+ return (0..<4).reduce(UInt32(0)) { $0 | UInt32(d[at + $1]) << (8 * UInt32($1)) }
123
+ }
124
+
125
+ /// Integer amount with `decimals` places, trailing zeros removed.
126
+ static func units(_ amount: UInt64, _ decimals: Int) -> String {
127
+ if decimals == 0 { return String(amount) }
128
+ var digits = String(amount)
129
+ if digits.count <= decimals { digits = String(repeating: "0", count: decimals - digits.count + 1) + digits }
130
+ let whole = String(digits.dropLast(decimals))
131
+ var frac = String(digits.suffix(decimals))
132
+ while frac.hasSuffix("0") { frac.removeLast() }
133
+ return whole + (frac.isEmpty ? "" : "." + frac)
134
+ }
135
+
136
+ private static func addr(_ a: String??) -> String { (a ?? nil) ?? "(address from a lookup table)" }
137
+
138
+ /// The approval summary. The signer must pay the fee (account 0).
139
+ static func summarize(_ msg: Message, signer: String, network: String) throws -> SignSummary {
140
+ guard msg.accountKeys.first == signer else {
141
+ throw WalletError(.addressMismatch, "the fee payer is not the signing account")
142
+ }
143
+ var rows: [SignSummary.Row] = [.init(label: "Network", value: network), .init(label: "Account", value: signer, mono: true)]
144
+ var warnings = [String]()
145
+ if msg.numRequiredSignatures > 1 {
146
+ warnings.append("This transaction needs \(msg.numRequiredSignatures) signatures; you sign as the fee payer.")
147
+ }
148
+ if msg.lookupAccounts > 0 { warnings.append("Some accounts come from address lookup tables and cannot be shown.") }
149
+ for ix in msg.instructions {
150
+ let d = ix.data
151
+ func account(_ i: Int) -> String?? { i < ix.accounts.count ? ix.accounts[i] : .some(nil) }
152
+ let isToken = ix.programId == token || ix.programId == token2022
153
+ if ix.programId == system, d.count >= 12, try u32(d, 0) == 2 {
154
+ rows += [.init(label: "Send", value: units(try u64(d, 4), 9) + " SOL"), .init(label: "To", value: addr(account(1)), mono: true)]
155
+ if (ix.accounts.first ?? nil) != signer { warnings.append("SOL is sent from an account other than yours.") }
156
+ } else if isToken, d.first == 12, d.count >= 10 {
157
+ rows += [
158
+ .init(label: "Send token", value: units(try u64(d, 1), Int(d[9]))),
159
+ .init(label: "Token", value: addr(account(1)), mono: true),
160
+ .init(label: "To token account", value: addr(account(2)), mono: true),
161
+ ]
162
+ } else if isToken, d.first == 3, d.count >= 9 {
163
+ rows += [
164
+ .init(label: "Send token (raw units)", value: String(try u64(d, 1))),
165
+ .init(label: "To token account", value: addr(account(1)), mono: true),
166
+ ]
167
+ warnings.append("Token amount is shown in raw units (no decimals in this instruction).")
168
+ } else if ix.programId == associatedToken, d.isEmpty || d[0] == 0 || d[0] == 1 {
169
+ rows.append(.init(label: "Create token account", value: addr(account(1)), mono: true))
170
+ } else if ix.programId == computeBudget, d.first == 2, d.count >= 5 {
171
+ rows.append(.init(label: "Compute limit", value: String(try u32(d, 1))))
172
+ } else if ix.programId == computeBudget, d.first == 3, d.count >= 9 {
173
+ rows.append(.init(label: "Priority fee", value: "\(try u64(d, 1)) micro-lamports per unit"))
174
+ } else if ix.programId == memo {
175
+ rows.append(.init(label: "Memo", value: String(decoding: d, as: UTF8.self)))
176
+ } else {
177
+ rows.append(.init(label: "Program call", value: ix.programId, mono: true))
178
+ warnings.append("Calls a program this wallet cannot describe (\(ix.programId)). Approve only if you trust the app.")
179
+ }
180
+ }
181
+ rows.append(.init(label: "Recent blockhash", value: msg.recentBlockhash, mono: true))
182
+ return SignSummary(kind: .transaction, title: "Solana transaction", rows: rows, body: nil, warnings: warnings, risk: .high)
183
+ }
184
+
185
+ /// The SLIP-0010 key of Solana account `index`. Callers wipe it.
186
+ static func privateKey(_ secret: [UInt8], index: Int) throws -> [UInt8] {
187
+ guard WalletShares.isAccountIndex(index) else { throw WalletError(.invalidRequest, "invalid account index") }
188
+ guard WalletShares.isHdSecret(secret) else { throw WalletError(.accountUnavailable, "this wallet has only EVM account 0") }
189
+ var seed = try BIP39.seed(mnemonic: try BIP39.mnemonic(entropy: secret))
190
+ defer { seed.wipe() }
191
+ return try ChainAddresses.ed25519Key(seed: seed, path: WalletChain.solana.path(index))
192
+ }
193
+
194
+ /// Deterministic RFC 8032 Ed25519 (Monocypher) over the exact message bytes.
195
+ /// Returns (address, base58 signature).
196
+ static func sign(_ message: [UInt8], privateKey: [UInt8]) -> (address: String, signature: String) {
197
+ var seed = privateKey // crypto_ed25519_key_pair wipes its seed argument
198
+ var secretKey = [UInt8](repeating: 0, count: 64)
199
+ var publicKey = [UInt8](repeating: 0, count: 32)
200
+ var signature = [UInt8](repeating: 0, count: 64)
201
+ crypto_ed25519_key_pair(&secretKey, &publicKey, &seed)
202
+ message.withUnsafeBufferPointer { m in crypto_ed25519_sign(&signature, secretKey, m.baseAddress, m.count) }
203
+ secretKey.wipe()
204
+ seed.wipe()
205
+ return (Base58.encode(publicKey, alphabet: Base58.bitcoin), Base58.encode(signature, alphabet: Base58.bitcoin))
206
+ }
207
+ }
@@ -0,0 +1,181 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Chains/Tron.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+
4
+ /// TRON transaction signing (docs/wallet/NATIVE.md section 12, `apps/wallet/src/tron.ts`). The app
5
+ /// builds raw_data; the wallet decodes it only to describe it and signs SHA-256(raw_data) like
6
+ /// TronWeb (r || s || v with v = recovery + 27).
7
+ enum Tron {
8
+ static let maxRawData = 4096
9
+
10
+ struct Contract {
11
+ var type: UInt64
12
+ /// Decoded fields of TransferContract (1) / TriggerSmartContract (31).
13
+ var owner: String?
14
+ var to: String?
15
+ var amount: UInt64?
16
+ var contractAddress: String?
17
+ var callValue: UInt64?
18
+ var data: [UInt8]?
19
+ }
20
+
21
+ struct RawData {
22
+ var expiration: UInt64?
23
+ var timestamp: UInt64?
24
+ var feeLimit: UInt64?
25
+ var memo: [UInt8]?
26
+ var contracts: [Contract]
27
+ }
28
+
29
+ private static func invalid() -> WalletError { WalletError(.invalidRequest, "not a valid TRON raw_data") }
30
+
31
+ struct Field {
32
+ var no: UInt64
33
+ var varint: UInt64?
34
+ var bytes: [UInt8]?
35
+ }
36
+
37
+ /// Minimal protobuf reader: varint (0), 64-bit (1), length-delimited (2), 32-bit (5).
38
+ static func fields(_ buf: [UInt8]) throws -> [Field] {
39
+ var out = [Field]()
40
+ var pos = 0
41
+ func varint() throws -> UInt64 {
42
+ var v: UInt64 = 0
43
+ var shift: UInt64 = 0
44
+ while shift < 64 {
45
+ guard pos < buf.count else { throw invalid() }
46
+ let b = buf[pos]
47
+ pos += 1
48
+ if shift == 63 && b > 1 { throw invalid() }
49
+ v |= UInt64(b & 0x7f) << shift
50
+ if b & 0x80 == 0 { return v }
51
+ shift += 7
52
+ }
53
+ throw invalid()
54
+ }
55
+ while pos < buf.count {
56
+ let key = try varint()
57
+ let no = key >> 3
58
+ let wire = key & 7
59
+ guard no != 0 else { throw invalid() }
60
+ switch wire {
61
+ case 0:
62
+ out.append(Field(no: no, varint: try varint()))
63
+ case 2:
64
+ let len = try varint()
65
+ guard len <= UInt64(buf.count - pos) else { throw invalid() }
66
+ out.append(Field(no: no, bytes: Array(buf[pos..<(pos + Int(len))])))
67
+ pos += Int(len)
68
+ case 1, 5:
69
+ let len = wire == 1 ? 8 : 4
70
+ guard pos + len <= buf.count else { throw invalid() }
71
+ pos += len
72
+ out.append(Field(no: no))
73
+ default:
74
+ throw invalid()
75
+ }
76
+ }
77
+ return out
78
+ }
79
+
80
+ private static func one(_ fs: [Field], _ no: UInt64) -> Field? { fs.first { $0.no == no } }
81
+
82
+ static func address(_ raw: [UInt8]) throws -> String {
83
+ guard raw.count == 21, raw[0] == 0x41 else { throw invalid() }
84
+ return Base58.encode(raw + ChainAddresses.checksum4(raw), alphabet: Base58.bitcoin)
85
+ }
86
+
87
+ static func parse(_ raw: [UInt8]) throws -> RawData {
88
+ guard !raw.isEmpty, raw.count <= maxRawData else { throw invalid() }
89
+ let top = try fields(raw)
90
+ let contracts = try top.filter { $0.no == 11 && $0.bytes != nil }.map { f -> Contract in
91
+ let c = try fields(f.bytes!)
92
+ let type = one(c, 1)?.varint ?? 0
93
+ var contract = Contract(type: type)
94
+ let any = one(c, 2)?.bytes
95
+ let value = try any.flatMap { one(try fields($0), 2)?.bytes }
96
+ if let value, type == 1 || type == 31 {
97
+ let v = try fields(value)
98
+ func addr(_ no: UInt64) throws -> String? { try one(v, no)?.bytes.map(address) }
99
+ contract.owner = try addr(1)
100
+ if type == 1 {
101
+ contract.to = try addr(2)
102
+ contract.amount = one(v, 3)?.varint ?? 0
103
+ } else {
104
+ contract.contractAddress = try addr(2)
105
+ contract.callValue = one(v, 3)?.varint ?? 0
106
+ contract.data = one(v, 4)?.bytes ?? []
107
+ }
108
+ }
109
+ return contract
110
+ }
111
+ guard !contracts.isEmpty else { throw invalid() }
112
+ return RawData(
113
+ expiration: one(top, 8)?.varint,
114
+ timestamp: one(top, 14)?.varint,
115
+ feeLimit: one(top, 18)?.varint,
116
+ memo: one(top, 10)?.bytes,
117
+ contracts: contracts
118
+ )
119
+ }
120
+
121
+ static func trx(_ sun: UInt64) -> String { Solana.units(sun, 6) + " TRX" }
122
+
123
+ private static func word(_ data: [UInt8], _ i: Int) -> [UInt8] { Array(data[(4 + 32 * i)..<(4 + 32 * (i + 1))]) }
124
+
125
+ /// `new Date(ms).toISOString()`
126
+ static func isoDate(_ ms: UInt64) throws -> String {
127
+ guard ms <= 8_640_000_000_000_000 else { throw invalid() }
128
+ let f = ISO8601DateFormatter()
129
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
130
+ f.timeZone = TimeZone(identifier: "UTC")
131
+ return f.string(from: Date(timeIntervalSince1970: Double(ms) / 1000))
132
+ }
133
+
134
+ static func summarize(_ tx: RawData, signer: String) throws -> SignSummary {
135
+ var rows: [SignSummary.Row] = [.init(label: "Network", value: "TRON"), .init(label: "Account", value: signer, mono: true)]
136
+ var warnings = [String]()
137
+ if tx.contracts.count != 1 { warnings.append("This transaction has \(tx.contracts.count) contracts.") }
138
+ for c in tx.contracts {
139
+ if let owner = c.owner, owner != signer {
140
+ throw WalletError(.addressMismatch, "the transaction owner is not the signing account")
141
+ }
142
+ if c.type == 1, let to = c.to, !to.isEmpty {
143
+ rows += [.init(label: "Send", value: trx(c.amount ?? 0)), .init(label: "To", value: to, mono: true)]
144
+ } else if c.type == 31, let data = c.data {
145
+ rows.append(.init(label: "Contract", value: c.contractAddress ?? "?", mono: true))
146
+ if let v = c.callValue, v > 0 { rows.append(.init(label: "Send", value: trx(v))) }
147
+ let selector = Bytes.hex(Array(data.prefix(4)))
148
+ if selector == "a9059cbb" && data.count == 68 {
149
+ rows += [
150
+ .init(label: "Token transfer to", value: try address([0x41] + word(data, 0)[12...]), mono: true),
151
+ .init(label: "Token amount (raw units)", value: BigUInt(bytes: word(data, 1)).description),
152
+ ]
153
+ } else if selector == "095ea7b3" && data.count == 68 {
154
+ let amount = BigUInt(bytes: word(data, 1))
155
+ rows += [
156
+ .init(label: "Allow spender", value: try address([0x41] + word(data, 0)[12...]), mono: true),
157
+ .init(label: "Allowance (raw units)", value: amount == BigUInt.power2(256) - BigUInt(1) ? "Unlimited" : amount.description),
158
+ ]
159
+ warnings.append("This lets another address spend your tokens.")
160
+ } else {
161
+ rows.append(.init(label: "Call data", value: Bytes.hex(data, prefix: true), mono: true))
162
+ warnings.append("Calls a contract function this wallet cannot describe. Approve only if you trust the app.")
163
+ }
164
+ } else {
165
+ rows.append(.init(label: "Contract type", value: String(c.type)))
166
+ warnings.append("TRON contract type \(c.type) cannot be described. Approve only if you trust the app.")
167
+ }
168
+ }
169
+ if let fee = tx.feeLimit { rows.append(.init(label: "Fee limit", value: trx(fee))) }
170
+ if let memo = tx.memo, !memo.isEmpty { rows.append(.init(label: "Memo", value: String(decoding: memo, as: UTF8.self))) }
171
+ if let exp = tx.expiration { rows.append(.init(label: "Expires", value: try isoDate(exp))) }
172
+ return SignSummary(kind: .transaction, title: "TRON transaction", rows: rows, body: nil, warnings: warnings, risk: .high)
173
+ }
174
+
175
+ /// txID = SHA-256(raw_data); signature r || s || (recovery + 27) in lowercase hex.
176
+ static func sign(_ raw: [UInt8], privateKey: [UInt8]) throws -> [String: String] {
177
+ let txID = Bytes.sha256(raw)
178
+ let sig = try Secp256k1.sign(hash: txID, key: privateKey)
179
+ return ["txID": Bytes.hex(txID), "signature": Bytes.hex(sig.r + sig.s + [UInt8(27 + sig.recoveryId)])]
180
+ }
181
+ }