@chainberry/trust-wallet-core 2.0.0 → 2.5.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/{TrustWalletCoreModule.podspec → ChainberryTrustWalletCoreModule.podspec} +2 -2
- package/README.md +15 -22
- package/android/build.gradle +29 -6
- package/android/libs/README.md +34 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar +0 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom +22 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar +0 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom +21 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.sha1 +1 -0
- package/android/libs/download.sh +52 -0
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/AddressDerivationConformanceTest.kt +106 -0
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/SigningConformanceTest.kt +186 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/AmountParsing.kt +45 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/Bech32.kt +68 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +526 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +147 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +796 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/AmountParsingConformanceTest.kt +57 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/Bech32Test.kt +35 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +274 -0
- package/expo-module.config.json +3 -2
- package/ios/AmountParsing.swift +62 -0
- package/ios/Bech32.swift +66 -0
- package/ios/ChainSigning.swift +602 -0
- package/ios/ChainberryTrustWalletCoreModule.swift +231 -0
- package/ios/NativeWalletStore.swift +232 -0
- package/package.json +4 -3
- package/src/index.ts +27 -12
- package/android/src/main/java/expo/modules/trustwalletcore/ChainSigning.kt +0 -299
- package/android/src/main/java/expo/modules/trustwalletcore/NativeWalletStore.kt +0 -182
- package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +0 -91
- package/ios/TrustWalletCoreModule.swift +0 -107
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import WalletCore
|
|
4
|
+
|
|
5
|
+
// All chains this module derives addresses for / signs transactions for.
|
|
6
|
+
enum ChainKey: String, CaseIterable {
|
|
7
|
+
case ethereum, bnb, polygon
|
|
8
|
+
case avax, base, arbitrum, optimism, sonic
|
|
9
|
+
case solana
|
|
10
|
+
case tron, ton
|
|
11
|
+
case bitcoin, bitcoincash, dogecoin, litecoin
|
|
12
|
+
case xrp
|
|
13
|
+
|
|
14
|
+
init(fromJs raw: String) throws {
|
|
15
|
+
guard let key = ChainKey(rawValue: raw) else {
|
|
16
|
+
throw Exception(name: "UnsupportedChain", description: "Unsupported chain: \(raw)")
|
|
17
|
+
}
|
|
18
|
+
self = key
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var symbol: String {
|
|
22
|
+
switch self {
|
|
23
|
+
case .ethereum: return "ETH"
|
|
24
|
+
case .bnb: return "BNB"
|
|
25
|
+
case .polygon: return "POL"
|
|
26
|
+
case .avax: return "AVAX"
|
|
27
|
+
case .base: return "ETH"
|
|
28
|
+
case .arbitrum: return "ETH"
|
|
29
|
+
case .optimism: return "ETH"
|
|
30
|
+
case .sonic: return "S"
|
|
31
|
+
case .solana: return "SOL"
|
|
32
|
+
case .tron: return "TRX"
|
|
33
|
+
case .ton: return "TON"
|
|
34
|
+
case .bitcoin: return "BTC"
|
|
35
|
+
case .bitcoincash: return "BCH"
|
|
36
|
+
case .dogecoin: return "DOGE"
|
|
37
|
+
case .litecoin: return "LTC"
|
|
38
|
+
case .xrp: return "XRP"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// All EVM chains share Ethereum's secp256k1 key/address (BIP44 slip44 = 60) — no distinct CoinType.
|
|
43
|
+
var coinType: CoinType {
|
|
44
|
+
switch self {
|
|
45
|
+
case .ethereum, .polygon, .avax, .base, .arbitrum, .optimism, .sonic: return .ethereum
|
|
46
|
+
case .bnb: return .smartChain
|
|
47
|
+
case .solana: return .solana
|
|
48
|
+
case .tron: return .tron
|
|
49
|
+
case .ton: return .ton
|
|
50
|
+
case .bitcoin: return .bitcoin
|
|
51
|
+
case .bitcoincash: return .bitcoinCash
|
|
52
|
+
case .dogecoin: return .dogecoin
|
|
53
|
+
case .litecoin: return .litecoin
|
|
54
|
+
case .xrp: return .xrp
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
enum ChainSigner {
|
|
60
|
+
struct Result {
|
|
61
|
+
let signedTx: String
|
|
62
|
+
let meta: [String: Any]?
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// SLIP-44 dedicates coin_type 1' to "testnet" for every coin — so a shared literal path
|
|
66
|
+
// would make Litecoin and Bitcoin Cash derive the *same* key (BIP32 derivation only depends
|
|
67
|
+
// on (seed, path, curve), and CoinType alone doesn't perturb it when the path and curve —
|
|
68
|
+
// secp256k1 for both — are identical). Disambiguate by using each coin's own SLIP-44 index
|
|
69
|
+
// as the account (3rd) path component. `purpose` follows the usual BIP44/49/84 convention
|
|
70
|
+
// (44' legacy, 84' native segwit) matching the address style each coin actually gets below.
|
|
71
|
+
private static func utxoTestnetPath(_ chain: ChainKey, purpose: Int) -> String {
|
|
72
|
+
// .rawValue is this coin's SLIP-44 id (same value `signUtxo` already sends as
|
|
73
|
+
// `input.coinType = coin.rawValue` below) — reusing it here instead of an unverified
|
|
74
|
+
// `.slip44Id` accessor keeps this to APIs already proven to exist in this binding.
|
|
75
|
+
"m/\(purpose)'/1'/\(chain.coinType.rawValue)'/0/0"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static let litecoinTestnetHRP = "tltc"
|
|
79
|
+
|
|
80
|
+
// Bitcoin Cash testnet legacy P2PKH version byte (0x6F) — same value Bitcoin/Litecoin
|
|
81
|
+
// testnets use for their base58 legacy prefix. BCH has no bech32/cashaddr testnet support in
|
|
82
|
+
// wallet-core (cashaddr is a different, more involved encoding than bech32 — unlike
|
|
83
|
+
// Litecoin below, not reimplemented here), so it stays on this legacy fallback.
|
|
84
|
+
private static let bchTestnetP2PKHPrefix: UInt8 = 0x6F
|
|
85
|
+
|
|
86
|
+
/// Address for `chain`, honoring `isTestnet`.
|
|
87
|
+
///
|
|
88
|
+
/// wallet-core's coin registry only carries a real testnet derivation for Bitcoin
|
|
89
|
+
/// (`.bitcoinTestnet`, native segwit — same "bc1"→"tb1" style shift as mainnet). Litecoin and
|
|
90
|
+
/// Bitcoin Cash have no testnet entry at all (no CoinType, no Derivation):
|
|
91
|
+
/// - Litecoin gets a hand-rolled native-segwit bech32 address (see Bech32.swift) — the same
|
|
92
|
+
/// style as its own mainnet "ltc1..." address, just hrp "tltc" instead of "ltc". wallet-
|
|
93
|
+
/// core's `SegwitAddress` can't do this itself (its HRP is a closed native enum with no
|
|
94
|
+
/// "tltc" entry), so this reimplements the encode half of BIP-173 by hand.
|
|
95
|
+
/// - Bitcoin Cash gets a legacy P2PKH address instead — a different *style* from its own
|
|
96
|
+
/// mainnet cashaddr address (cashaddr testnet isn't implemented), but still a real,
|
|
97
|
+
/// correctly-testnet-flagged one.
|
|
98
|
+
static func address(for chain: ChainKey, wallet: HDWallet, isTestnet: Bool) -> String {
|
|
99
|
+
guard isTestnet else { return wallet.getAddressForCoin(coin: chain.coinType) }
|
|
100
|
+
switch chain {
|
|
101
|
+
case .bitcoin:
|
|
102
|
+
return wallet.getAddressDerivation(coin: .bitcoin, derivation: .bitcoinTestnet)
|
|
103
|
+
case .litecoin:
|
|
104
|
+
let pubKey = key(for: chain, wallet: wallet, isTestnet: true).getPublicKeySecp256k1(compressed: true)
|
|
105
|
+
return Bech32.encodeSegwitV0(hrp: litecoinTestnetHRP, program: pubKey.bitcoinKeyHash)
|
|
106
|
+
case .bitcoincash:
|
|
107
|
+
let pubKey = key(for: chain, wallet: wallet, isTestnet: true).getPublicKeySecp256k1(compressed: true)
|
|
108
|
+
return BitcoinAddress(publicKey: pubKey, prefix: bchTestnetP2PKHPrefix)!.description
|
|
109
|
+
// Everything else (EVM/Solana/Tron/Ton/Xrp) shares one address format across
|
|
110
|
+
// mainnet/testnet — only the RPC endpoint differs, which lives entirely in JS.
|
|
111
|
+
default:
|
|
112
|
+
return wallet.getAddressForCoin(coin: chain.coinType)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// The signing key for `chain`, honoring `isTestnet` — must always derive the same key
|
|
117
|
+
/// `address(for:)` used, or a UTXO signer builds a transaction that can't spend the
|
|
118
|
+
/// wallet's own funds (wrong key ⇒ different scriptPubKey than what's actually sitting at
|
|
119
|
+
/// the receive address it was given).
|
|
120
|
+
private static func key(for chain: ChainKey, wallet: HDWallet, isTestnet: Bool) -> PrivateKey {
|
|
121
|
+
guard isTestnet else { return wallet.getKeyForCoin(coin: chain.coinType) }
|
|
122
|
+
switch chain {
|
|
123
|
+
case .bitcoin:
|
|
124
|
+
return wallet.getKeyDerivation(coin: .bitcoin, derivation: .bitcoinTestnet)
|
|
125
|
+
case .litecoin:
|
|
126
|
+
return wallet.getKey(coin: chain.coinType, derivationPath: utxoTestnetPath(chain, purpose: 84))
|
|
127
|
+
case .bitcoincash:
|
|
128
|
+
return wallet.getKey(coin: chain.coinType, derivationPath: utxoTestnetPath(chain, purpose: 44))
|
|
129
|
+
default:
|
|
130
|
+
return wallet.getKeyForCoin(coin: chain.coinType)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static func sign(chain: ChainKey, wallet: HDWallet, unsignedTx: [String: Any], isTestnet: Bool) throws -> Result {
|
|
135
|
+
switch chain {
|
|
136
|
+
case .ethereum, .bnb, .polygon, .avax, .base, .arbitrum, .optimism, .sonic:
|
|
137
|
+
return Result(signedTx: try signEvm(wallet: wallet, coin: chain.coinType, txParams: unsignedTx), meta: nil)
|
|
138
|
+
case .solana:
|
|
139
|
+
return Result(signedTx: try signSolana(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
140
|
+
case .bitcoin, .dogecoin, .litecoin:
|
|
141
|
+
return Result(signedTx: try signUtxo(wallet: wallet, chain: chain, txParams: unsignedTx, isTestnet: isTestnet), meta: nil)
|
|
142
|
+
case .tron:
|
|
143
|
+
return Result(signedTx: try signTron(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
144
|
+
case .xrp:
|
|
145
|
+
return Result(signedTx: try signXrp(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
146
|
+
case .ton:
|
|
147
|
+
return try signTon(wallet: wallet, txParams: unsignedTx)
|
|
148
|
+
case .bitcoincash:
|
|
149
|
+
return Result(signedTx: try signBch(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - EVM (ethereum / bnb / polygon)
|
|
154
|
+
// txParams: { to, chainId, nonce, gasLimitHex, valueHex?, dataHex?,
|
|
155
|
+
// gasPriceHex? (legacy) | maxFeePerGasHex + maxPriorityFeePerGasHex (EIP-1559) }
|
|
156
|
+
// Hex fields are bare hex (no 0x prefix required) — mirrors what wallet.ts's buildTxParams sends.
|
|
157
|
+
|
|
158
|
+
private static func signEvm(wallet: HDWallet, coin: CoinType, txParams: [String: Any]) throws -> String {
|
|
159
|
+
guard let to = txParams["to"] as? String,
|
|
160
|
+
let nonceNum = txParams["nonce"] as? NSNumber,
|
|
161
|
+
let gasLimHex = txParams["gasLimitHex"] as? String,
|
|
162
|
+
let chainIdNum = txParams["chainId"] as? NSNumber else {
|
|
163
|
+
throw Exception(name: "InvalidParams", description: "Missing required EVM tx params")
|
|
164
|
+
}
|
|
165
|
+
let nonce = nonceNum.intValue
|
|
166
|
+
let chainId = chainIdNum.intValue
|
|
167
|
+
let valueHex = (txParams["valueHex"] as? String) ?? "0"
|
|
168
|
+
let dataHex = (txParams["dataHex"] as? String) ?? ""
|
|
169
|
+
|
|
170
|
+
let privateKey = wallet.getKeyForCoin(coin: coin)
|
|
171
|
+
|
|
172
|
+
guard let gasLimitData = hexData(gasLimHex) else {
|
|
173
|
+
throw Exception(name: "InvalidParams", description: "Invalid gasLimitHex: \(gasLimHex)")
|
|
174
|
+
}
|
|
175
|
+
guard let valueData = hexData(valueHex) else {
|
|
176
|
+
throw Exception(name: "InvalidParams", description: "Invalid valueHex: \(valueHex)")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var input = EthereumSigningInput()
|
|
180
|
+
input.chainID = intToData(chainId)
|
|
181
|
+
input.nonce = intToData(nonce)
|
|
182
|
+
input.gasLimit = gasLimitData
|
|
183
|
+
input.toAddress = to
|
|
184
|
+
input.privateKey = privateKey.data
|
|
185
|
+
var transfer = EthereumTransaction.Transfer()
|
|
186
|
+
transfer.amount = valueData
|
|
187
|
+
if !dataHex.isEmpty {
|
|
188
|
+
guard let dataBytes = hexData(dataHex) else {
|
|
189
|
+
throw Exception(name: "InvalidParams", description: "Invalid dataHex: \(dataHex)")
|
|
190
|
+
}
|
|
191
|
+
transfer.data = dataBytes
|
|
192
|
+
}
|
|
193
|
+
var tx = EthereumTransaction()
|
|
194
|
+
tx.transfer = transfer
|
|
195
|
+
input.transaction = tx
|
|
196
|
+
|
|
197
|
+
if let gasPriceHex = txParams["gasPriceHex"] as? String {
|
|
198
|
+
guard let gasPriceData = hexData(gasPriceHex) else {
|
|
199
|
+
throw Exception(name: "InvalidParams", description: "Invalid gasPriceHex: \(gasPriceHex)")
|
|
200
|
+
}
|
|
201
|
+
input.gasPrice = gasPriceData
|
|
202
|
+
} else if let mfHex = txParams["maxFeePerGasHex"] as? String,
|
|
203
|
+
let pfHex = txParams["maxPriorityFeePerGasHex"] as? String {
|
|
204
|
+
guard let maxFeeData = hexData(mfHex) else {
|
|
205
|
+
throw Exception(name: "InvalidParams", description: "Invalid maxFeePerGasHex: \(mfHex)")
|
|
206
|
+
}
|
|
207
|
+
guard let maxPriorityData = hexData(pfHex) else {
|
|
208
|
+
throw Exception(name: "InvalidParams", description: "Invalid maxPriorityFeePerGasHex: \(pfHex)")
|
|
209
|
+
}
|
|
210
|
+
input.txMode = .enveloped
|
|
211
|
+
input.maxFeePerGas = maxFeeData
|
|
212
|
+
input.maxInclusionFeePerGas = maxPriorityData
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let output: EthereumSigningOutput = AnySigner.sign(input: input, coin: coin)
|
|
216
|
+
guard output.error == .ok else {
|
|
217
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
218
|
+
}
|
|
219
|
+
return "0x" + output.encoded.hexString
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// MARK: - Solana
|
|
223
|
+
// NOTE(verify-on-device): prepareSelfCustodyUnsignedTx's SOL branch (chainberry-wallet)
|
|
224
|
+
// returns a fully-built, base64-serialized unsigned @solana/web3.js Transaction (supports
|
|
225
|
+
// both native transfer and SPL-token transfer instructions) — not a simple {to,lamports,
|
|
226
|
+
// recentBlockhash} triple. wallet-core's SolanaSigningInput has a raw-message signing mode
|
|
227
|
+
// (`rawMessage` / legacy transaction bytes) intended for exactly this "sign an externally
|
|
228
|
+
// built transaction" case — confirm the exact field name against the installed wallet-core
|
|
229
|
+
// version and adjust the input construction below accordingly before relying on this path.
|
|
230
|
+
// txParams: { unsignedTxBase64: string }
|
|
231
|
+
private static func signSolana(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
232
|
+
guard let unsignedTxBase64 = txParams["unsignedTxBase64"] as? String,
|
|
233
|
+
let txData = Data(base64Encoded: unsignedTxBase64) else {
|
|
234
|
+
throw Exception(name: "InvalidParams", description: "Missing/invalid unsignedTxBase64")
|
|
235
|
+
}
|
|
236
|
+
let privateKey = wallet.getKeyForCoin(coin: .solana)
|
|
237
|
+
|
|
238
|
+
// Decode the unsigned tx to extract the embedded recentBlockhash, then re-sign via
|
|
239
|
+
// TW's sanctioned path. We pass the same blockhash back (no-op refresh) so the
|
|
240
|
+
// tx content is unchanged — only the signature is added.
|
|
241
|
+
let decodedData = TransactionDecoder.decode(coinType: .solana, encodedTx: txData)
|
|
242
|
+
let decoded = try SolanaDecodingTransactionOutput(serializedData: decodedData)
|
|
243
|
+
guard decoded.error == .ok else {
|
|
244
|
+
throw Exception(name: "DecodingFailed", description: "Failed to decode SOL tx: \(decoded.errorMessage)")
|
|
245
|
+
}
|
|
246
|
+
let recentBlockhash = decoded.transaction.legacy.recentBlockhash
|
|
247
|
+
|
|
248
|
+
let privateKeys = DataVector()
|
|
249
|
+
privateKeys.add(data: privateKey.data)
|
|
250
|
+
guard let outputData = SolanaTransaction.updateBlockhashAndSign(
|
|
251
|
+
encodedTx: unsignedTxBase64, recentBlockhash: recentBlockhash, privateKeys: privateKeys
|
|
252
|
+
) else {
|
|
253
|
+
throw Exception(name: "SigningFailed", description: "SolanaTransaction.updateBlockhashAndSign returned nil")
|
|
254
|
+
}
|
|
255
|
+
let output = try SolanaSigningOutput(serializedData: outputData)
|
|
256
|
+
guard output.error == .ok else {
|
|
257
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
258
|
+
}
|
|
259
|
+
return output.encoded
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// MARK: - BCH (UTXO-based, replay-protected)
|
|
263
|
+
// txParams: { unsignedDescriptorJson: string }
|
|
264
|
+
// descriptor (from wallet-broadcast's prepareBchTransaction):
|
|
265
|
+
// { inputs: [{ txid, vout, satoshis, scriptPubKeyHex }], toAddress, sendAmountSats,
|
|
266
|
+
// changeAddress?, changeSats? }
|
|
267
|
+
// BCH uses SIGHASH_ALL | SIGHASH_FORK_ID (0x41) for replay protection — distinct from
|
|
268
|
+
// BTC/LTC's plain SIGHASH_ALL (0x01).
|
|
269
|
+
private static func signBch(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
270
|
+
guard let descriptorJson = txParams["unsignedDescriptorJson"] as? String,
|
|
271
|
+
let descriptorData = descriptorJson.data(using: .utf8),
|
|
272
|
+
let descriptor = try? JSONSerialization.jsonObject(with: descriptorData) as? [String: Any],
|
|
273
|
+
let toAddress = descriptor["toAddress"] as? String,
|
|
274
|
+
let sendAmountSats = (descriptor["sendAmountSats"] as? NSNumber)?.int64Value,
|
|
275
|
+
let inputs = descriptor["inputs"] as? [[String: Any]] else {
|
|
276
|
+
throw Exception(name: "InvalidParams", description: "Invalid BCH descriptor JSON")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let privateKey = wallet.getKeyForCoin(coin: .bitcoinCash)
|
|
280
|
+
|
|
281
|
+
var input = BitcoinSigningInput()
|
|
282
|
+
input.hashType = 0x41 // SIGHASH_ALL | SIGHASH_FORK_ID (BCH replay protection)
|
|
283
|
+
input.amount = sendAmountSats
|
|
284
|
+
input.byteFee = 1 // BCH fees are minimal; 1 sat/byte is the ecosystem standard
|
|
285
|
+
input.toAddress = toAddress
|
|
286
|
+
if let changeAddress = descriptor["changeAddress"] as? String {
|
|
287
|
+
input.changeAddress = changeAddress
|
|
288
|
+
}
|
|
289
|
+
input.useMaxAmount = false
|
|
290
|
+
input.coinType = CoinType.bitcoinCash.rawValue
|
|
291
|
+
input.privateKey = [privateKey.data]
|
|
292
|
+
|
|
293
|
+
input.utxo = try inputs.map { entry in
|
|
294
|
+
guard let txid = entry["txid"] as? String,
|
|
295
|
+
let voutNum = entry["vout"] as? NSNumber,
|
|
296
|
+
let satoshisNum = entry["satoshis"] as? NSNumber,
|
|
297
|
+
let scriptHex = entry["scriptPubKeyHex"] as? String,
|
|
298
|
+
let scriptData = hexData(scriptHex),
|
|
299
|
+
var txIdData = hexData(txid) else {
|
|
300
|
+
throw Exception(name: "InvalidParams", description: "Invalid BCH UTXO entry")
|
|
301
|
+
}
|
|
302
|
+
txIdData.reverse()
|
|
303
|
+
|
|
304
|
+
var outPoint = BitcoinOutPoint()
|
|
305
|
+
outPoint.hash = txIdData
|
|
306
|
+
outPoint.index = UInt32(voutNum.intValue)
|
|
307
|
+
|
|
308
|
+
var utxo = BitcoinUnspentTransaction()
|
|
309
|
+
utxo.outPoint = outPoint
|
|
310
|
+
utxo.amount = satoshisNum.int64Value
|
|
311
|
+
utxo.script = scriptData
|
|
312
|
+
return utxo
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .bitcoinCash)
|
|
316
|
+
guard output.error == .ok else {
|
|
317
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
318
|
+
}
|
|
319
|
+
return output.encoded.hexString
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// MARK: - BTC / LTC (UTXO-based)
|
|
323
|
+
// txParams (from wallet-broadcast's additive prepareBtcUtxoSet/prepareLtcUtxoSet):
|
|
324
|
+
// { toAddress, changeAddress, sendAmountSats, changeAmountSats, satsPerByte,
|
|
325
|
+
// inputs: [{ txIdHex, vout, amountSats, scriptPubKeyHex }] }
|
|
326
|
+
// NOTE(verify-on-device): field names (hashType/toAddress/changeAddress/byteFee/utxo/
|
|
327
|
+
// outPoint/privateKey) match wallet-core's long-documented Bitcoin signing example, but
|
|
328
|
+
// confirm against the installed 4.1.19 generated Swift types before trusting with real funds.
|
|
329
|
+
private static func signUtxo(wallet: HDWallet, chain: ChainKey, txParams: [String: Any], isTestnet: Bool) throws -> String {
|
|
330
|
+
guard let toAddress = txParams["toAddress"] as? String,
|
|
331
|
+
let changeAddress = txParams["changeAddress"] as? String,
|
|
332
|
+
let sendAmountSats = (txParams["sendAmountSats"] as? String).flatMap(Int64.init),
|
|
333
|
+
let satsPerByteNum = txParams["satsPerByte"] as? NSNumber,
|
|
334
|
+
let inputs = txParams["inputs"] as? [[String: Any]] else {
|
|
335
|
+
throw Exception(name: "InvalidParams", description: "Missing required UTXO tx params")
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let coin = chain.coinType
|
|
339
|
+
let privateKey = key(for: chain, wallet: wallet, isTestnet: isTestnet)
|
|
340
|
+
|
|
341
|
+
var input = BitcoinSigningInput()
|
|
342
|
+
input.hashType = 1 // SIGHASH_ALL — stable Bitcoin protocol constant, not a wallet-core-specific value
|
|
343
|
+
input.amount = sendAmountSats
|
|
344
|
+
input.byteFee = Int64(satsPerByteNum.doubleValue.rounded(.up))
|
|
345
|
+
input.toAddress = toAddress
|
|
346
|
+
input.changeAddress = changeAddress
|
|
347
|
+
input.useMaxAmount = false
|
|
348
|
+
input.coinType = coin.rawValue
|
|
349
|
+
input.privateKey = [privateKey.data]
|
|
350
|
+
|
|
351
|
+
input.utxo = try inputs.map { entry in
|
|
352
|
+
guard let txIdHex = entry["txIdHex"] as? String,
|
|
353
|
+
let voutNum = entry["vout"] as? NSNumber,
|
|
354
|
+
let amount = (entry["amountSats"] as? String).flatMap(Int64.init),
|
|
355
|
+
let scriptHex = entry["scriptPubKeyHex"] as? String,
|
|
356
|
+
let scriptData = hexData(scriptHex),
|
|
357
|
+
var txIdData = hexData(txIdHex) else {
|
|
358
|
+
throw Exception(name: "InvalidParams", description: "Invalid UTXO input entry")
|
|
359
|
+
}
|
|
360
|
+
// On-chain/explorer txid hex is displayed big-endian; wallet-core's OutPoint.hash wants
|
|
361
|
+
// the reversed (little-endian, internal wire-format) byte order.
|
|
362
|
+
txIdData.reverse()
|
|
363
|
+
|
|
364
|
+
var outPoint = BitcoinOutPoint()
|
|
365
|
+
outPoint.hash = txIdData
|
|
366
|
+
outPoint.index = UInt32(voutNum.intValue)
|
|
367
|
+
|
|
368
|
+
var utxo = BitcoinUnspentTransaction()
|
|
369
|
+
utxo.outPoint = outPoint
|
|
370
|
+
utxo.amount = amount
|
|
371
|
+
utxo.script = scriptData
|
|
372
|
+
return utxo
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: coin)
|
|
376
|
+
guard output.error == .ok else {
|
|
377
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
378
|
+
}
|
|
379
|
+
return output.encoded.hexString
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// MARK: - TRX
|
|
383
|
+
// txParams: the raw TronGrid-shaped unsigned tx object from prepareSelfCustodyUnsignedTx's
|
|
384
|
+
// TRX branch (raw_data.contract[], ref_block_bytes/hash, expiration, timestamp).
|
|
385
|
+
// NOTE(verify-on-device): field mapping into TronSigningInput/TronTransaction must be checked
|
|
386
|
+
// against the installed wallet-core version's Tron.proto — this covers the plain TRX-transfer
|
|
387
|
+
// contract type only (matches prepareTrxTransaction's non-token branch); the TRC20
|
|
388
|
+
// (triggerSmartContract) branch is not mapped here and needs its own contract-call SigningInput.
|
|
389
|
+
private static func signTron(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
390
|
+
let privateKey = wallet.getKeyForCoin(coin: .tron)
|
|
391
|
+
|
|
392
|
+
// Pass the full TronGrid unsigned tx JSON via rawJson — wallet-core's direct-sign path
|
|
393
|
+
// reads txID from the JSON and signs that digest, returning the complete signed tx in output.json.
|
|
394
|
+
// This covers both plain TRX transfers and TRC20 triggerSmartContract payloads.
|
|
395
|
+
guard JSONSerialization.isValidJSONObject(txParams) else {
|
|
396
|
+
throw Exception(name: "InvalidParams", description: "TRX tx is not JSON-serializable")
|
|
397
|
+
}
|
|
398
|
+
let jsonData = try JSONSerialization.data(withJSONObject: txParams)
|
|
399
|
+
guard let jsonStr = String(data: jsonData, encoding: .utf8) else {
|
|
400
|
+
throw Exception(name: "InvalidParams", description: "TRX tx JSON encoding failed")
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
var input = TronSigningInput()
|
|
404
|
+
input.privateKey = privateKey.data
|
|
405
|
+
input.rawJson = jsonStr
|
|
406
|
+
|
|
407
|
+
let output: TronSigningOutput = AnySigner.sign(input: input, coin: .tron)
|
|
408
|
+
guard output.error == .ok else {
|
|
409
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
410
|
+
}
|
|
411
|
+
return output.json
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// MARK: - XRP
|
|
415
|
+
// txParams: xrpl.js `Payment` object from prepareSelfCustodyUnsignedTx's XRP branch
|
|
416
|
+
// (Account, Destination, Amount (drops, string), Fee (drops, string), Sequence,
|
|
417
|
+
// LastLedgerSequence, DestinationTag?).
|
|
418
|
+
private static func signXrp(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
419
|
+
guard let account = txParams["Account"] as? String,
|
|
420
|
+
let destination = txParams["Destination"] as? String,
|
|
421
|
+
let amountDrops = txParams["Amount"] as? String,
|
|
422
|
+
let feeDrops = txParams["Fee"] as? String,
|
|
423
|
+
let sequenceNum = txParams["Sequence"] as? NSNumber else {
|
|
424
|
+
throw Exception(name: "InvalidParams", description: "Missing required XRP Payment fields")
|
|
425
|
+
}
|
|
426
|
+
let sequence = sequenceNum.intValue
|
|
427
|
+
let lastLedgerSequence = (txParams["LastLedgerSequence"] as? NSNumber)?.intValue
|
|
428
|
+
let destinationTag = (txParams["DestinationTag"] as? NSNumber)?.intValue
|
|
429
|
+
|
|
430
|
+
let privateKey = wallet.getKeyForCoin(coin: .xrp)
|
|
431
|
+
|
|
432
|
+
var payment = RippleOperationPayment()
|
|
433
|
+
payment.amount = try parseXrpAmountDrops(amountDrops)
|
|
434
|
+
payment.destination = destination
|
|
435
|
+
if let resolvedTag = try parseXrpDestinationTag(destinationTag) {
|
|
436
|
+
payment.destinationTag = resolvedTag
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
var input = RippleSigningInput()
|
|
440
|
+
input.privateKey = privateKey.data
|
|
441
|
+
input.account = account
|
|
442
|
+
input.fee = try parseXrpFeeDrops(feeDrops)
|
|
443
|
+
input.sequence = UInt32(sequence)
|
|
444
|
+
if let lls = lastLedgerSequence { input.lastLedgerSequence = UInt32(lls) }
|
|
445
|
+
input.opPayment = payment
|
|
446
|
+
|
|
447
|
+
let output: RippleSigningOutput = AnySigner.sign(input: input, coin: .xrp)
|
|
448
|
+
guard output.error == .ok else {
|
|
449
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
450
|
+
}
|
|
451
|
+
return output.encoded.hexString
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// MARK: - TON
|
|
455
|
+
// txParams: { toAddress, amount, seqno, memoId? } from prepareSelfCustodyUnsignedTx's TON
|
|
456
|
+
// branch. `amount` there is a human-readable TON string (see chainberry-wallet's TON case,
|
|
457
|
+
// which passes the raw `amount` param through unconverted) — NOTE(verify-on-device): confirm
|
|
458
|
+
// whether that's already nanoton or needs *1e9 conversion, and confirm wallet_version V4R2
|
|
459
|
+
// matches the address format the current @ton/* implementation derives (address-format parity
|
|
460
|
+
// is the main risk for this chain per the migration plan).
|
|
461
|
+
private static func signTon(wallet: HDWallet, txParams: [String: Any]) throws -> ChainSigner.Result {
|
|
462
|
+
guard let toAddress = txParams["toAddress"] as? String,
|
|
463
|
+
let amountStr = txParams["amount"] as? String,
|
|
464
|
+
let seqnoNum = txParams["seqno"] as? NSNumber else {
|
|
465
|
+
throw Exception(name: "InvalidParams", description: "Missing required TON tx params")
|
|
466
|
+
}
|
|
467
|
+
let seqno = seqnoNum.intValue
|
|
468
|
+
let memoId = txParams["memoId"] as? String
|
|
469
|
+
|
|
470
|
+
let privateKey = wallet.getKeyForCoin(coin: .ton)
|
|
471
|
+
|
|
472
|
+
// amount is Data (uint128 big-endian); encode the nanoton UInt64 as 8 big-endian bytes.
|
|
473
|
+
let nanotons = try parseTonNanotons(amountStr)
|
|
474
|
+
var bigEndianNano = nanotons.bigEndian
|
|
475
|
+
let amountData = withUnsafeBytes(of: &bigEndianNano) { Data($0) }
|
|
476
|
+
|
|
477
|
+
var transfer = TheOpenNetworkTransfer()
|
|
478
|
+
transfer.dest = toAddress
|
|
479
|
+
transfer.amount = amountData
|
|
480
|
+
transfer.mode = UInt32(TheOpenNetworkSendMode.payFeesSeparately.rawValue | TheOpenNetworkSendMode.ignoreActionPhaseErrors.rawValue)
|
|
481
|
+
transfer.bounceable = true
|
|
482
|
+
if let memoId { transfer.comment = memoId }
|
|
483
|
+
|
|
484
|
+
var input = TheOpenNetworkSigningInput()
|
|
485
|
+
input.privateKey = privateKey.data
|
|
486
|
+
input.walletVersion = .walletV4R2
|
|
487
|
+
input.sequenceNumber = UInt32(seqno)
|
|
488
|
+
input.expireAt = UInt32(Date().timeIntervalSince1970) + 600
|
|
489
|
+
input.messages = [transfer]
|
|
490
|
+
|
|
491
|
+
let output: TheOpenNetworkSigningOutput = AnySigner.sign(input: input, coin: .ton)
|
|
492
|
+
guard output.error == .ok else {
|
|
493
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
494
|
+
}
|
|
495
|
+
return ChainSigner.Result(signedTx: output.encoded, meta: ["txHash": output.hash.hexString])
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// MARK: - Transaction summary for native confirmation UI
|
|
499
|
+
|
|
500
|
+
static func buildSummary(chain: ChainKey, unsignedTx: [String: Any]) -> String {
|
|
501
|
+
var lines = ["Network: \(chain.rawValue.uppercased())"]
|
|
502
|
+
switch chain {
|
|
503
|
+
case .ethereum, .bnb, .polygon:
|
|
504
|
+
if let to = unsignedTx["to"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
505
|
+
let val_ = txHexToDouble((unsignedTx["valueHex"] as? String) ?? "0")
|
|
506
|
+
lines.append("Amount: \(fmtAmt(val_ / 1e18)) \(chain.symbol)")
|
|
507
|
+
let gasLimit = txHexToDouble((unsignedTx["gasLimitHex"] as? String) ?? "0")
|
|
508
|
+
let gasPrice = txHexToDouble(
|
|
509
|
+
(unsignedTx["gasPriceHex"] as? String) ?? (unsignedTx["maxFeePerGasHex"] as? String) ?? "0"
|
|
510
|
+
)
|
|
511
|
+
let fee = gasLimit * gasPrice
|
|
512
|
+
if fee > 0 { lines.append("Max fee: \(fmtAmt(fee / 1e18)) \(chain.symbol)") }
|
|
513
|
+
|
|
514
|
+
case .bitcoin, .dogecoin, .litecoin:
|
|
515
|
+
if let to = unsignedTx["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
516
|
+
if let sats = (unsignedTx["sendAmountSats"] as? String).flatMap(Int64.init) {
|
|
517
|
+
lines.append("Amount: \(fmtAmt(Double(sats) / 1e8)) \(chain.symbol)")
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case .xrp:
|
|
521
|
+
if let dest = unsignedTx["Destination"] as? String { lines.append("To: \(fmtAddr(dest))") }
|
|
522
|
+
if let drops = (unsignedTx["Amount"] as? String).flatMap(Int64.init) {
|
|
523
|
+
lines.append("Amount: \(fmtAmt(Double(drops) / 1_000_000)) XRP")
|
|
524
|
+
}
|
|
525
|
+
if let feeDrops = (unsignedTx["Fee"] as? String).flatMap(Int64.init) {
|
|
526
|
+
lines.append("Fee: \(fmtAmt(Double(feeDrops) / 1_000_000)) XRP")
|
|
527
|
+
}
|
|
528
|
+
if let tag = unsignedTx["DestinationTag"] { lines.append("Tag: \(tag)") }
|
|
529
|
+
|
|
530
|
+
case .ton:
|
|
531
|
+
if let to = unsignedTx["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
532
|
+
if let nano = (unsignedTx["amount"] as? String).flatMap(UInt64.init) {
|
|
533
|
+
lines.append("Amount: \(fmtAmt(Double(nano) / 1e9)) TON")
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
case .tron:
|
|
537
|
+
if let rawData = unsignedTx["raw_data"] as? [String: Any],
|
|
538
|
+
let contracts = rawData["contract"] as? [[String: Any]],
|
|
539
|
+
let param = contracts.first?["parameter"] as? [String: Any],
|
|
540
|
+
let value = param["value"] as? [String: Any] {
|
|
541
|
+
if let to = value["to_address"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
542
|
+
if let amount = value["amount"] as? Int { lines.append("Amount: \(fmtAmt(Double(amount) / 1e6)) TRX") }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
case .solana:
|
|
546
|
+
lines.append("(Solana — details verified by the network)")
|
|
547
|
+
|
|
548
|
+
case .bitcoincash:
|
|
549
|
+
if let descriptorJson = unsignedTx["unsignedDescriptorJson"] as? String,
|
|
550
|
+
let data = descriptorJson.data(using: .utf8),
|
|
551
|
+
let descriptor = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
552
|
+
if let to = descriptor["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
553
|
+
if let sats = (descriptor["sendAmountSats"] as? NSNumber)?.int64Value {
|
|
554
|
+
lines.append("Amount: \(fmtAmt(Double(sats) / 1e8)) BCH")
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return lines.joined(separator: "\n")
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private static func txHexToDouble(_ hex: String) -> Double {
|
|
562
|
+
let s = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
|
|
563
|
+
if let v = UInt64(s, radix: 16) { return Double(v) }
|
|
564
|
+
return hexData(s)?.reduce(0.0) { $0 * 256 + Double($1) } ?? 0
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private static func fmtAmt(_ value: Double) -> String {
|
|
568
|
+
var s = String(format: "%.8f", value)
|
|
569
|
+
while s.hasSuffix("0") { s.removeLast() }
|
|
570
|
+
if s.hasSuffix(".") { s.removeLast() }
|
|
571
|
+
return s
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private static func fmtAddr(_ addr: String) -> String { addr }
|
|
575
|
+
|
|
576
|
+
// MARK: - Helpers
|
|
577
|
+
|
|
578
|
+
// Parses a hex string (with or without 0x, odd or even length) into Data. An empty string
|
|
579
|
+
// deliberately maps to a single zero byte (fields like valueHex already default to "0"
|
|
580
|
+
// when absent — that's a legitimate zero-value transfer, not malformed input). Any
|
|
581
|
+
// non-empty string containing a non-hex character returns nil so callers can fail closed
|
|
582
|
+
// instead of silently coercing garbage into a zero/empty value.
|
|
583
|
+
static func hexData(_ hex: String) -> Data? {
|
|
584
|
+
let s = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
|
|
585
|
+
let padded = s.count % 2 == 0 ? s : "0" + s
|
|
586
|
+
guard !padded.isEmpty else { return Data([0]) }
|
|
587
|
+
guard padded.allSatisfy({ $0.isHexDigit }) else { return nil }
|
|
588
|
+
return Data(hexString: padded)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Encodes a non-negative integer as minimal big-endian Data.
|
|
592
|
+
static func intToData(_ value: Int) -> Data {
|
|
593
|
+
guard value > 0 else { return Data([0]) }
|
|
594
|
+
var v = value
|
|
595
|
+
var bytes: [UInt8] = []
|
|
596
|
+
while v > 0 {
|
|
597
|
+
bytes.insert(UInt8(v & 0xFF), at: 0)
|
|
598
|
+
v >>= 8
|
|
599
|
+
}
|
|
600
|
+
return Data(bytes)
|
|
601
|
+
}
|
|
602
|
+
}
|