@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.
- package/CHANGELOG.md +20 -0
- package/README.md +34 -4
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
- 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 +16 -8
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/client.d.ts +18 -2
- package/build/client.d.ts.map +1 -1
- package/build/client.js +88 -6
- package/build/client.js.map +1 -1
- package/build/errors.d.ts +8 -1
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +7 -1
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/native.d.ts +5 -1
- package/build/native.d.ts.map +1 -1
- package/build/native.js.map +1 -1
- package/build/types.d.ts +47 -1
- package/build/types.d.ts.map +1 -1
- package/ios/AuthonReactNativeWallet.podspec +6 -2
- package/ios/AuthonWalletModule.swift +46 -3
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
- 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/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
- package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
- package/ios/vendor/CKeccak/README.md +6 -3
- package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -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/GENERATED.md +1 -1
- package/package.json +3 -2
- package/src/client.ts +90 -6
- package/src/errors.ts +7 -1
- package/src/index.ts +5 -0
- package/src/native.ts +4 -2
- 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
|
|
122
|
+
throw WalletError.network()
|
|
114
123
|
}
|
|
115
|
-
let
|
|
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:
|
|
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
|
|