@chainberry/trust-wallet-core 2.5.0 → 2.5.2
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/ChainberryTrustWalletCoreModule.podspec +9 -2
- package/README.md +42 -5
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/AddressDerivationConformanceTest.kt +4 -4
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/SigningConformanceTest.kt +2 -2
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +393 -18
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +101 -18
- package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +128 -18
- package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +92 -4
- package/ios/ChainSigning.swift +430 -40
- package/ios/ChainberryTrustWalletCoreModule.swift +131 -20
- package/ios/ConformanceTests/AddressDerivationConformanceTests.swift +39 -0
- package/ios/ConformanceTests/SigningConformanceTests.swift +292 -0
- package/ios/NativeWalletStore.swift +73 -6
- package/package.json +1 -1
- package/src/index.ts +24 -10
package/ios/ChainSigning.swift
CHANGED
|
@@ -3,6 +3,7 @@ import ExpoModulesCore
|
|
|
3
3
|
import WalletCore
|
|
4
4
|
|
|
5
5
|
// All chains this module derives addresses for / signs transactions for.
|
|
6
|
+
|
|
6
7
|
enum ChainKey: String, CaseIterable {
|
|
7
8
|
case ethereum, bnb, polygon
|
|
8
9
|
case avax, base, arbitrum, optimism, sonic
|
|
@@ -10,6 +11,9 @@ enum ChainKey: String, CaseIterable {
|
|
|
10
11
|
case tron, ton
|
|
11
12
|
case bitcoin, bitcoincash, dogecoin, litecoin
|
|
12
13
|
case xrp
|
|
14
|
+
case cosmos
|
|
15
|
+
case aptos
|
|
16
|
+
case tezos
|
|
13
17
|
|
|
14
18
|
init(fromJs raw: String) throws {
|
|
15
19
|
guard let key = ChainKey(rawValue: raw) else {
|
|
@@ -36,6 +40,9 @@ enum ChainKey: String, CaseIterable {
|
|
|
36
40
|
case .dogecoin: return "DOGE"
|
|
37
41
|
case .litecoin: return "LTC"
|
|
38
42
|
case .xrp: return "XRP"
|
|
43
|
+
case .cosmos: return "ATOM"
|
|
44
|
+
case .aptos: return "APT"
|
|
45
|
+
case .tezos: return "XTZ"
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -52,6 +59,9 @@ enum ChainKey: String, CaseIterable {
|
|
|
52
59
|
case .dogecoin: return .dogecoin
|
|
53
60
|
case .litecoin: return .litecoin
|
|
54
61
|
case .xrp: return .xrp
|
|
62
|
+
case .cosmos: return .cosmos
|
|
63
|
+
case .aptos: return .aptos
|
|
64
|
+
case .tezos: return .tezos
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
}
|
|
@@ -147,6 +157,12 @@ enum ChainSigner {
|
|
|
147
157
|
return try signTon(wallet: wallet, txParams: unsignedTx)
|
|
148
158
|
case .bitcoincash:
|
|
149
159
|
return Result(signedTx: try signBch(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
160
|
+
case .cosmos:
|
|
161
|
+
return Result(signedTx: try signCosmos(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
162
|
+
case .aptos:
|
|
163
|
+
return Result(signedTx: try signAptos(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
164
|
+
case .tezos:
|
|
165
|
+
return Result(signedTx: try signTezos(wallet: wallet, txParams: unsignedTx), meta: nil)
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
168
|
|
|
@@ -239,7 +255,7 @@ enum ChainSigner {
|
|
|
239
255
|
// TW's sanctioned path. We pass the same blockhash back (no-op refresh) so the
|
|
240
256
|
// tx content is unchanged — only the signature is added.
|
|
241
257
|
let decodedData = TransactionDecoder.decode(coinType: .solana, encodedTx: txData)
|
|
242
|
-
let decoded = try SolanaDecodingTransactionOutput(
|
|
258
|
+
let decoded = try SolanaDecodingTransactionOutput(serializedBytes: decodedData)
|
|
243
259
|
guard decoded.error == .ok else {
|
|
244
260
|
throw Exception(name: "DecodingFailed", description: "Failed to decode SOL tx: \(decoded.errorMessage)")
|
|
245
261
|
}
|
|
@@ -247,12 +263,10 @@ enum ChainSigner {
|
|
|
247
263
|
|
|
248
264
|
let privateKeys = DataVector()
|
|
249
265
|
privateKeys.add(data: privateKey.data)
|
|
250
|
-
|
|
266
|
+
let outputData = SolanaTransaction.updateBlockhashAndSign(
|
|
251
267
|
encodedTx: unsignedTxBase64, recentBlockhash: recentBlockhash, privateKeys: privateKeys
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
let output = try SolanaSigningOutput(serializedData: outputData)
|
|
268
|
+
)
|
|
269
|
+
let output = try SolanaSigningOutput(serializedBytes: outputData)
|
|
256
270
|
guard output.error == .ok else {
|
|
257
271
|
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
258
272
|
}
|
|
@@ -389,26 +403,29 @@ enum ChainSigner {
|
|
|
389
403
|
private static func signTron(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
390
404
|
let privateKey = wallet.getKeyForCoin(coin: .tron)
|
|
391
405
|
|
|
392
|
-
//
|
|
393
|
-
// reads txID from the JSON and signs that digest, returning the complete signed tx in output.json.
|
|
406
|
+
// Sign via txID — wallet-core reads the txID digest and signs it directly.
|
|
394
407
|
// This covers both plain TRX transfers and TRC20 triggerSmartContract payloads.
|
|
395
|
-
guard
|
|
396
|
-
throw Exception(name: "InvalidParams", description: "TRX tx
|
|
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")
|
|
408
|
+
guard let txID = txParams["txID"] as? String else {
|
|
409
|
+
throw Exception(name: "InvalidParams", description: "Missing txID in TRX tx params")
|
|
401
410
|
}
|
|
402
411
|
|
|
403
412
|
var input = TronSigningInput()
|
|
404
413
|
input.privateKey = privateKey.data
|
|
405
|
-
input.
|
|
414
|
+
input.txID = txID
|
|
406
415
|
|
|
407
416
|
let output: TronSigningOutput = AnySigner.sign(input: input, coin: .tron)
|
|
408
417
|
guard output.error == .ok else {
|
|
409
418
|
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
410
419
|
}
|
|
411
|
-
|
|
420
|
+
|
|
421
|
+
// Reconstruct the full TronGrid broadcast payload with the signature appended.
|
|
422
|
+
var broadcastTx = txParams
|
|
423
|
+
broadcastTx["signature"] = [output.signature.hexString]
|
|
424
|
+
let broadcastData = try JSONSerialization.data(withJSONObject: broadcastTx)
|
|
425
|
+
guard let broadcastJson = String(data: broadcastData, encoding: .utf8) else {
|
|
426
|
+
throw Exception(name: "EncodingFailed", description: "TRX broadcast tx JSON encoding failed")
|
|
427
|
+
}
|
|
428
|
+
return broadcastJson
|
|
412
429
|
}
|
|
413
430
|
|
|
414
431
|
// MARK: - XRP
|
|
@@ -433,15 +450,15 @@ enum ChainSigner {
|
|
|
433
450
|
payment.amount = try parseXrpAmountDrops(amountDrops)
|
|
434
451
|
payment.destination = destination
|
|
435
452
|
if let resolvedTag = try parseXrpDestinationTag(destinationTag) {
|
|
436
|
-
payment.destinationTag = resolvedTag
|
|
453
|
+
payment.destinationTag = Int64(resolvedTag)
|
|
437
454
|
}
|
|
438
455
|
|
|
439
456
|
var input = RippleSigningInput()
|
|
440
457
|
input.privateKey = privateKey.data
|
|
441
458
|
input.account = account
|
|
442
459
|
input.fee = try parseXrpFeeDrops(feeDrops)
|
|
443
|
-
input.sequence =
|
|
444
|
-
if let lls = lastLedgerSequence { input.lastLedgerSequence =
|
|
460
|
+
input.sequence = Int32(sequence)
|
|
461
|
+
if let lls = lastLedgerSequence { input.lastLedgerSequence = Int32(lls) }
|
|
445
462
|
input.opPayment = payment
|
|
446
463
|
|
|
447
464
|
let output: RippleSigningOutput = AnySigner.sign(input: input, coin: .xrp)
|
|
@@ -469,14 +486,11 @@ enum ChainSigner {
|
|
|
469
486
|
|
|
470
487
|
let privateKey = wallet.getKeyForCoin(coin: .ton)
|
|
471
488
|
|
|
472
|
-
// amount is Data (uint128 big-endian); encode the nanoton UInt64 as 8 big-endian bytes.
|
|
473
489
|
let nanotons = try parseTonNanotons(amountStr)
|
|
474
|
-
var bigEndianNano = nanotons.bigEndian
|
|
475
|
-
let amountData = withUnsafeBytes(of: &bigEndianNano) { Data($0) }
|
|
476
490
|
|
|
477
491
|
var transfer = TheOpenNetworkTransfer()
|
|
478
492
|
transfer.dest = toAddress
|
|
479
|
-
transfer.amount =
|
|
493
|
+
transfer.amount = nanotons
|
|
480
494
|
transfer.mode = UInt32(TheOpenNetworkSendMode.payFeesSeparately.rawValue | TheOpenNetworkSendMode.ignoreActionPhaseErrors.rawValue)
|
|
481
495
|
transfer.bounceable = true
|
|
482
496
|
if let memoId { transfer.comment = memoId }
|
|
@@ -495,12 +509,179 @@ enum ChainSigner {
|
|
|
495
509
|
return ChainSigner.Result(signedTx: output.encoded, meta: ["txHash": output.hash.hexString])
|
|
496
510
|
}
|
|
497
511
|
|
|
512
|
+
// MARK: - Cosmos (ATOM)
|
|
513
|
+
// txParams: { accountNumber, sequence, chainId, feeAmount, gas, memo, fromAddress, toAddress,
|
|
514
|
+
// amount (uatom, decimal string), denom }
|
|
515
|
+
// Returns output.serialized — the ready-to-broadcast JSON
|
|
516
|
+
// {"mode":"BROADCAST_MODE_SYNC","tx_bytes":"<base64>"} posted directly to the Cosmos LCD.
|
|
517
|
+
private static func signCosmos(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
518
|
+
guard let fromAddress = txParams["fromAddress"] as? String,
|
|
519
|
+
let toAddress = txParams["toAddress"] as? String,
|
|
520
|
+
let amountStr = txParams["amount"] as? String,
|
|
521
|
+
let feeAmountStr = txParams["feeAmount"] as? String,
|
|
522
|
+
let denom = txParams["denom"] as? String,
|
|
523
|
+
let chainId = txParams["chainId"] as? String,
|
|
524
|
+
let accountNumberNum = txParams["accountNumber"] as? NSNumber,
|
|
525
|
+
let sequenceNum = txParams["sequence"] as? NSNumber,
|
|
526
|
+
let gasNum = txParams["gas"] as? NSNumber else {
|
|
527
|
+
throw Exception(name: "InvalidParams", description: "Missing required Cosmos tx params")
|
|
528
|
+
}
|
|
529
|
+
let memo = (txParams["memo"] as? String) ?? ""
|
|
530
|
+
|
|
531
|
+
let privateKey = wallet.getKeyForCoin(coin: .cosmos)
|
|
532
|
+
|
|
533
|
+
var sendAmount = CosmosAmount()
|
|
534
|
+
sendAmount.denom = denom
|
|
535
|
+
sendAmount.amount = amountStr
|
|
536
|
+
|
|
537
|
+
var send = CosmosMessage.Send()
|
|
538
|
+
send.fromAddress = fromAddress
|
|
539
|
+
send.toAddress = toAddress
|
|
540
|
+
send.amounts = [sendAmount]
|
|
541
|
+
|
|
542
|
+
var message = CosmosMessage()
|
|
543
|
+
message.sendCoinsMessage = send
|
|
544
|
+
|
|
545
|
+
var feeAmt = CosmosAmount()
|
|
546
|
+
feeAmt.denom = denom
|
|
547
|
+
feeAmt.amount = feeAmountStr
|
|
548
|
+
|
|
549
|
+
var fee = CosmosFee()
|
|
550
|
+
fee.amounts = [feeAmt]
|
|
551
|
+
fee.gas = UInt64(gasNum.intValue)
|
|
552
|
+
|
|
553
|
+
var input = CosmosSigningInput()
|
|
554
|
+
input.signingMode = .protobuf
|
|
555
|
+
input.accountNumber = UInt64(accountNumberNum.intValue)
|
|
556
|
+
input.chainID = chainId
|
|
557
|
+
input.sequence = UInt64(sequenceNum.intValue)
|
|
558
|
+
input.memo = memo
|
|
559
|
+
input.fee = fee
|
|
560
|
+
input.messages = [message]
|
|
561
|
+
input.privateKey = privateKey.data
|
|
562
|
+
input.mode = .sync
|
|
563
|
+
|
|
564
|
+
let output: CosmosSigningOutput = AnySigner.sign(input: input, coin: .cosmos)
|
|
565
|
+
guard output.error == .ok else {
|
|
566
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
567
|
+
}
|
|
568
|
+
return output.serialized
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// MARK: - Aptos (APT)
|
|
572
|
+
// txParams: { sender, sequenceNumber, maxGasAmount, gasUnitPrice, expirationTimestampSecs,
|
|
573
|
+
// chainId, toAddress, amount (octas, decimal string) }
|
|
574
|
+
// Returns output.json — the signed JSON body posted directly to the Aptos REST API.
|
|
575
|
+
private static func signAptos(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
576
|
+
guard let sender = txParams["sender"] as? String,
|
|
577
|
+
let toAddress = txParams["toAddress"] as? String,
|
|
578
|
+
let amountStr = txParams["amount"] as? String,
|
|
579
|
+
let seqNum = txParams["sequenceNumber"] as? NSNumber,
|
|
580
|
+
let maxGas = txParams["maxGasAmount"] as? NSNumber,
|
|
581
|
+
let gasPrice = txParams["gasUnitPrice"] as? NSNumber,
|
|
582
|
+
let expiry = txParams["expirationTimestampSecs"] as? NSNumber,
|
|
583
|
+
let chainId = txParams["chainId"] as? NSNumber else {
|
|
584
|
+
throw Exception(name: "InvalidParams", description: "Missing required Aptos tx params")
|
|
585
|
+
}
|
|
586
|
+
guard let amountOctas = UInt64(amountStr) else {
|
|
587
|
+
throw Exception(name: "InvalidParams", description: "Invalid Aptos amount: \(amountStr)")
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let privateKey = wallet.getKeyForCoin(coin: .aptos)
|
|
591
|
+
|
|
592
|
+
var transfer = AptosTransferMessage()
|
|
593
|
+
transfer.to = toAddress
|
|
594
|
+
transfer.amount = amountOctas
|
|
595
|
+
|
|
596
|
+
var input = AptosSigningInput()
|
|
597
|
+
input.sender = sender
|
|
598
|
+
input.sequenceNumber = Int64(seqNum.intValue)
|
|
599
|
+
input.maxGasAmount = UInt64(maxGas.intValue)
|
|
600
|
+
input.gasUnitPrice = UInt64(gasPrice.intValue)
|
|
601
|
+
input.expirationTimestampSecs = UInt64(expiry.intValue)
|
|
602
|
+
input.chainID = UInt32(chainId.intValue)
|
|
603
|
+
input.privateKey = privateKey.data
|
|
604
|
+
input.transfer = transfer
|
|
605
|
+
|
|
606
|
+
let output: AptosSigningOutput = AnySigner.sign(input: input, coin: .aptos)
|
|
607
|
+
guard output.error == .ok else {
|
|
608
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
609
|
+
}
|
|
610
|
+
return output.json
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// MARK: - Tezos (XTZ)
|
|
614
|
+
// txParams: { branch, fromAddress, toAddress, counter, amount (mutez), fee (mutez),
|
|
615
|
+
// gasLimit, storageLimit, needsReveal }
|
|
616
|
+
// Returns output.encoded hex — posted to /injection/operation as a JSON-encoded string.
|
|
617
|
+
private static func signTezos(wallet: HDWallet, txParams: [String: Any]) throws -> String {
|
|
618
|
+
guard let branch = txParams["branch"] as? String,
|
|
619
|
+
let fromAddress = txParams["fromAddress"] as? String,
|
|
620
|
+
let toAddress = txParams["toAddress"] as? String,
|
|
621
|
+
let counterNum = txParams["counter"] as? NSNumber,
|
|
622
|
+
let amountNum = txParams["amount"] as? NSNumber,
|
|
623
|
+
let feeNum = txParams["fee"] as? NSNumber,
|
|
624
|
+
let gasLimitNum = txParams["gasLimit"] as? NSNumber,
|
|
625
|
+
let storageLimitNum = txParams["storageLimit"] as? NSNumber else {
|
|
626
|
+
throw Exception(name: "InvalidParams", description: "Missing required Tezos tx params")
|
|
627
|
+
}
|
|
628
|
+
let needsReveal = (txParams["needsReveal"] as? Bool) ?? false
|
|
629
|
+
let counter = counterNum.int64Value
|
|
630
|
+
|
|
631
|
+
let privateKey = wallet.getKeyForCoin(coin: .tezos)
|
|
632
|
+
var operations: [TezosOperation] = []
|
|
633
|
+
|
|
634
|
+
if needsReveal {
|
|
635
|
+
let pubKey = privateKey.getPublicKeyEd25519()
|
|
636
|
+
var revealData = TezosRevealOperationData()
|
|
637
|
+
revealData.publicKey = pubKey.data
|
|
638
|
+
|
|
639
|
+
var reveal = TezosOperation()
|
|
640
|
+
reveal.source = fromAddress
|
|
641
|
+
reveal.counter = counter - 1
|
|
642
|
+
reveal.fee = 1420
|
|
643
|
+
reveal.gasLimit = 10600
|
|
644
|
+
reveal.storageLimit = 0
|
|
645
|
+
reveal.kind = .reveal
|
|
646
|
+
reveal.revealOperationData = revealData
|
|
647
|
+
operations.append(reveal)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
var txData = TezosTransactionOperationData()
|
|
651
|
+
txData.destination = toAddress
|
|
652
|
+
txData.amount = amountNum.int64Value
|
|
653
|
+
|
|
654
|
+
var txOp = TezosOperation()
|
|
655
|
+
txOp.source = fromAddress
|
|
656
|
+
txOp.counter = counter
|
|
657
|
+
txOp.fee = feeNum.int64Value
|
|
658
|
+
txOp.gasLimit = gasLimitNum.int64Value
|
|
659
|
+
txOp.storageLimit = storageLimitNum.int64Value
|
|
660
|
+
txOp.kind = .transaction
|
|
661
|
+
txOp.transactionOperationData = txData
|
|
662
|
+
operations.append(txOp)
|
|
663
|
+
|
|
664
|
+
var opList = TezosOperationList()
|
|
665
|
+
opList.branch = branch
|
|
666
|
+
opList.operations = operations
|
|
667
|
+
|
|
668
|
+
var input = TezosSigningInput()
|
|
669
|
+
input.operationList = opList
|
|
670
|
+
input.privateKey = privateKey.data
|
|
671
|
+
|
|
672
|
+
let output: TezosSigningOutput = AnySigner.sign(input: input, coin: .tezos)
|
|
673
|
+
guard output.error == .ok else {
|
|
674
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
675
|
+
}
|
|
676
|
+
return output.encoded.hexString
|
|
677
|
+
}
|
|
678
|
+
|
|
498
679
|
// MARK: - Transaction summary for native confirmation UI
|
|
499
680
|
|
|
500
|
-
static func buildSummary(chain: ChainKey, unsignedTx: [String: Any]) -> String {
|
|
681
|
+
static func buildSummary(chain: ChainKey, unsignedTx: [String: Any]) throws -> String {
|
|
501
682
|
var lines = ["Network: \(chain.rawValue.uppercased())"]
|
|
502
683
|
switch chain {
|
|
503
|
-
case .ethereum, .bnb, .polygon:
|
|
684
|
+
case .ethereum, .bnb, .polygon, .avax, .base, .arbitrum, .optimism, .sonic:
|
|
504
685
|
if let to = unsignedTx["to"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
505
686
|
let val_ = txHexToDouble((unsignedTx["valueHex"] as? String) ?? "0")
|
|
506
687
|
lines.append("Amount: \(fmtAmt(val_ / 1e18)) \(chain.symbol)")
|
|
@@ -510,12 +691,41 @@ enum ChainSigner {
|
|
|
510
691
|
)
|
|
511
692
|
let fee = gasLimit * gasPrice
|
|
512
693
|
if fee > 0 { lines.append("Max fee: \(fmtAmt(fee / 1e18)) \(chain.symbol)") }
|
|
694
|
+
if let chainId = unsignedTx["chainId"] as? NSNumber { lines.append("Chain ID: \(chainId.intValue)") }
|
|
695
|
+
if let nonce = unsignedTx["nonce"] as? NSNumber { lines.append("Nonce: \(nonce.intValue)") }
|
|
696
|
+
let dataHex = (unsignedTx["dataHex"] as? String) ?? ""
|
|
697
|
+
let stripped = dataHex.hasPrefix("0x") ? String(dataHex.dropFirst(2)) : dataHex
|
|
698
|
+
if !stripped.isEmpty && stripped != "0" {
|
|
699
|
+
let sel = stripped.prefix(8).lowercased()
|
|
700
|
+
if sel == "a9059cbb", stripped.count >= 136 {
|
|
701
|
+
// transfer(address recipient, uint256 amount)
|
|
702
|
+
let recipient = "0x" + String(stripped.dropFirst(32).prefix(40))
|
|
703
|
+
let amountHex = String(stripped.dropFirst(72).prefix(64)).drop(while: { $0 == "0" })
|
|
704
|
+
lines.append("Token transfer to: \(fmtAddr(recipient))")
|
|
705
|
+
lines.append("Token amount (raw units): 0x\(amountHex.isEmpty ? "0" : String(amountHex))")
|
|
706
|
+
} else if sel == "23b872dd", stripped.count >= 200 {
|
|
707
|
+
// transferFrom(address from, address to, uint256 amount)
|
|
708
|
+
let to = "0x" + String(stripped.dropFirst(96).prefix(40))
|
|
709
|
+
let amountHex = String(stripped.dropFirst(136).prefix(64)).drop(while: { $0 == "0" })
|
|
710
|
+
lines.append("Token transfer to: \(fmtAddr(to))")
|
|
711
|
+
lines.append("Token amount (raw units): 0x\(amountHex.isEmpty ? "0" : String(amountHex))")
|
|
712
|
+
} else {
|
|
713
|
+
lines.append("Contract data: \(stripped.count / 2) bytes — review carefully")
|
|
714
|
+
}
|
|
715
|
+
}
|
|
513
716
|
|
|
514
717
|
case .bitcoin, .dogecoin, .litecoin:
|
|
515
718
|
if let to = unsignedTx["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
719
|
+
let sendSats = (unsignedTx["sendAmountSats"] as? String).flatMap(Int64.init) ?? 0
|
|
720
|
+
if sendSats > 0 { lines.append("Amount: \(fmtAmt(Double(sendSats) / 1e8)) \(chain.symbol)") }
|
|
721
|
+
if let change = unsignedTx["changeAddress"] as? String { lines.append("Change to: \(fmtAddr(change))") }
|
|
722
|
+
if let spb = unsignedTx["satsPerByte"] as? NSNumber { lines.append("Fee rate: \(spb.intValue) sat/vB") }
|
|
723
|
+
let inputTotal = (unsignedTx["inputs"] as? [[String: Any]])?
|
|
724
|
+
.compactMap { ($0["amountSats"] as? String).flatMap(Int64.init) }
|
|
725
|
+
.reduce(Int64(0), +) ?? 0
|
|
726
|
+
let changeSats = (unsignedTx["changeAmountSats"] as? String).flatMap(Int64.init) ?? 0
|
|
727
|
+
let totalFee = inputTotal - sendSats - changeSats
|
|
728
|
+
if totalFee > 0 { lines.append("Total fee: \(fmtAmt(Double(totalFee) / 1e8)) \(chain.symbol)") }
|
|
519
729
|
|
|
520
730
|
case .xrp:
|
|
521
731
|
if let dest = unsignedTx["Destination"] as? String { lines.append("To: \(fmtAddr(dest))") }
|
|
@@ -532,32 +742,208 @@ enum ChainSigner {
|
|
|
532
742
|
if let nano = (unsignedTx["amount"] as? String).flatMap(UInt64.init) {
|
|
533
743
|
lines.append("Amount: \(fmtAmt(Double(nano) / 1e9)) TON")
|
|
534
744
|
}
|
|
745
|
+
let memoTon = unsignedTx["memoId"] as? String
|
|
746
|
+
if let memo = memoTon, !memo.isEmpty { lines.append("Memo: \(memo)") }
|
|
747
|
+
let feeEst = (memoTon?.isEmpty == false) ? "~0.006" : "~0.005"
|
|
748
|
+
lines.append("Fee: \(feeEst) TON (estimate)")
|
|
535
749
|
|
|
536
750
|
case .tron:
|
|
537
751
|
if let rawData = unsignedTx["raw_data"] as? [String: Any],
|
|
538
752
|
let contracts = rawData["contract"] as? [[String: Any]],
|
|
539
|
-
let
|
|
540
|
-
|
|
541
|
-
if let
|
|
542
|
-
|
|
753
|
+
let first = contracts.first {
|
|
754
|
+
let type_ = first["type"] as? String ?? ""
|
|
755
|
+
if let param = first["parameter"] as? [String: Any],
|
|
756
|
+
let value = param["value"] as? [String: Any] {
|
|
757
|
+
switch type_ {
|
|
758
|
+
case "TransferContract":
|
|
759
|
+
if let to = value["to_address"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
760
|
+
if let amount = value["amount"] as? Int { lines.append("Amount: \(fmtAmt(Double(amount) / 1e6)) TRX") }
|
|
761
|
+
case "TriggerSmartContract":
|
|
762
|
+
if let contractAddr = value["contract_address"] as? String {
|
|
763
|
+
lines.append("Token contract: \(fmtAddr(contractAddr))")
|
|
764
|
+
}
|
|
765
|
+
let dataHex = (value["data"] as? String) ?? ""
|
|
766
|
+
let stripped = dataHex.hasPrefix("0x") ? String(dataHex.dropFirst(2)) : dataHex
|
|
767
|
+
let sel = stripped.prefix(8).lowercased()
|
|
768
|
+
if sel == "a9059cbb" && stripped.count >= 136 {
|
|
769
|
+
// TRC-20 transfer(address, uint256) — ABI encoding identical to EVM
|
|
770
|
+
let recipientHex = "0x" + String(stripped.dropFirst(32).prefix(40))
|
|
771
|
+
let amountHex = String(stripped.dropFirst(72).prefix(64)).drop(while: { $0 == "0" })
|
|
772
|
+
lines.append("TRC-20 to: \(fmtAddr(recipientHex))")
|
|
773
|
+
lines.append("Token amount (raw units): 0x\(amountHex.isEmpty ? "0" : String(amountHex))")
|
|
774
|
+
} else {
|
|
775
|
+
lines.append("Contract call: \(stripped.count / 2) bytes — review carefully")
|
|
776
|
+
}
|
|
777
|
+
default:
|
|
778
|
+
if !type_.isEmpty { lines.append("Contract type: \(type_) — review carefully") }
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Verify txID == SHA256(raw_data_hex). raw_data_hex must be present — fail closed if absent
|
|
783
|
+
// so a JS caller cannot suppress the integrity check by omitting the field.
|
|
784
|
+
guard let txID = unsignedTx["txID"] as? String else {
|
|
785
|
+
throw Exception(name: "TxIntegrityFailed", description: "TRX txID missing — signing refused")
|
|
543
786
|
}
|
|
787
|
+
guard let rawHex = unsignedTx["raw_data_hex"] as? String,
|
|
788
|
+
let rawBytes = hexData(rawHex) else {
|
|
789
|
+
throw Exception(name: "TxIntegrityFailed",
|
|
790
|
+
description: "TRX raw_data_hex missing — cannot verify txID, signing refused")
|
|
791
|
+
}
|
|
792
|
+
let computed = Hash.sha256(data: rawBytes)
|
|
793
|
+
let computedHex = computed.map { String(format: "%02x", $0) }.joined()
|
|
794
|
+
guard computedHex.lowercased() == txID.lowercased() else {
|
|
795
|
+
throw Exception(name: "TxIntegrityFailed",
|
|
796
|
+
description: "TRX txID does not match SHA256(raw_data_hex) — signing refused")
|
|
797
|
+
}
|
|
798
|
+
lines.append("TxID verified ✓")
|
|
544
799
|
|
|
545
800
|
case .solana:
|
|
546
|
-
|
|
801
|
+
// Decode the pre-built tx to extract recipient and lamports (SOL) or destination and
|
|
802
|
+
// amount (SPL token). Falls closed — throws if the tx cannot be decoded at all.
|
|
803
|
+
guard let info = decodeSolanaForSummary(unsignedTx) else {
|
|
804
|
+
throw Exception(name: "UndecodableTx",
|
|
805
|
+
description: "Cannot decode Solana transaction — signing refused to prevent blind signing")
|
|
806
|
+
}
|
|
807
|
+
if info.isSplTransfer {
|
|
808
|
+
if let dest = info.splDest { lines.append("SPL Token to: \(fmtAddr(dest))") }
|
|
809
|
+
if let amt = info.splAmount { lines.append("SPL Token amount (raw): \(amt)") }
|
|
810
|
+
} else {
|
|
811
|
+
if let to = info.to { lines.append("To: \(fmtAddr(to))") }
|
|
812
|
+
if let lamports = info.lamports { lines.append("Amount: \(fmtAmt(Double(lamports) / 1e9)) SOL") }
|
|
813
|
+
if !info.isTransfer { lines.append("Non-transfer instruction — review carefully") }
|
|
814
|
+
}
|
|
547
815
|
|
|
548
816
|
case .bitcoincash:
|
|
549
|
-
if
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
817
|
+
// Fail closed — if descriptor is absent or unparseable we cannot show what will be signed.
|
|
818
|
+
guard let descriptorJson = unsignedTx["unsignedDescriptorJson"] as? String,
|
|
819
|
+
let data = descriptorJson.data(using: .utf8),
|
|
820
|
+
let descriptor = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
821
|
+
throw Exception(name: "UndecodableTx",
|
|
822
|
+
description: "Cannot decode BCH descriptor — signing refused to prevent blind signing")
|
|
823
|
+
}
|
|
824
|
+
if let to = descriptor["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
825
|
+
if let sats = (descriptor["sendAmountSats"] as? NSNumber)?.int64Value {
|
|
826
|
+
lines.append("Amount: \(fmtAmt(Double(sats) / 1e8)) BCH")
|
|
827
|
+
}
|
|
828
|
+
if let change = descriptor["changeAddress"] as? String { lines.append("Change to: \(fmtAddr(change))") }
|
|
829
|
+
if let spb = (descriptor["satsPerByte"] as? NSNumber)?.intValue { lines.append("Fee rate: \(spb) sat/vB") }
|
|
830
|
+
let bchInputs = (descriptor["inputs"] as? [[String: Any]])?
|
|
831
|
+
.compactMap { ($0["amountSats"] as? NSNumber)?.int64Value }
|
|
832
|
+
.reduce(Int64(0), +) ?? 0
|
|
833
|
+
let bchSend = (descriptor["sendAmountSats"] as? NSNumber)?.int64Value ?? 0
|
|
834
|
+
let bchChange = (descriptor["changeAmountSats"] as? NSNumber)?.int64Value ?? 0
|
|
835
|
+
let bchFee = bchInputs - bchSend - bchChange
|
|
836
|
+
if bchFee > 0 { lines.append("Total fee: \(fmtAmt(Double(bchFee) / 1e8)) BCH") }
|
|
837
|
+
|
|
838
|
+
case .cosmos:
|
|
839
|
+
if let to = unsignedTx["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
840
|
+
if let uatom = (unsignedTx["amount"] as? String).flatMap(Int64.init) {
|
|
841
|
+
lines.append("Amount: \(fmtAmt(Double(uatom) / 1_000_000)) ATOM")
|
|
842
|
+
}
|
|
843
|
+
if let feeUatom = (unsignedTx["feeAmount"] as? String).flatMap(Int64.init) {
|
|
844
|
+
lines.append("Fee: \(fmtAmt(Double(feeUatom) / 1_000_000)) ATOM")
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
case .aptos:
|
|
848
|
+
if let to = unsignedTx["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
849
|
+
if let octas = (unsignedTx["amount"] as? String).flatMap(UInt64.init) {
|
|
850
|
+
lines.append("Amount: \(fmtAmt(Double(octas) / 1e8)) APT")
|
|
851
|
+
}
|
|
852
|
+
if let maxGas = (unsignedTx["maxGasAmount"] as? NSNumber)?.uint64Value,
|
|
853
|
+
let gasPrice = (unsignedTx["gasUnitPrice"] as? NSNumber)?.uint64Value {
|
|
854
|
+
let feeOctas = maxGas * gasPrice
|
|
855
|
+
lines.append("Max fee: \(fmtAmt(Double(feeOctas) / 1e8)) APT")
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
case .tezos:
|
|
859
|
+
if let to = unsignedTx["toAddress"] as? String { lines.append("To: \(fmtAddr(to))") }
|
|
860
|
+
if let mutez = (unsignedTx["amount"] as? NSNumber)?.int64Value {
|
|
861
|
+
lines.append("Amount: \(fmtAmt(Double(mutez) / 1_000_000)) XTZ")
|
|
862
|
+
}
|
|
863
|
+
if let feeMutez = (unsignedTx["fee"] as? NSNumber)?.int64Value {
|
|
864
|
+
lines.append("Fee: \(fmtAmt(Double(feeMutez) / 1_000_000)) XTZ")
|
|
865
|
+
}
|
|
866
|
+
if let reveal = unsignedTx["needsReveal"] as? Bool, reveal {
|
|
867
|
+
lines.append("(includes reveal operation)")
|
|
556
868
|
}
|
|
557
869
|
}
|
|
558
870
|
return lines.joined(separator: "\n")
|
|
559
871
|
}
|
|
560
872
|
|
|
873
|
+
private struct SolanaSummaryInfo {
|
|
874
|
+
let to: String?
|
|
875
|
+
let lamports: UInt64?
|
|
876
|
+
let isTransfer: Bool
|
|
877
|
+
let splDest: String?
|
|
878
|
+
let splAmount: UInt64?
|
|
879
|
+
let isSplTransfer: Bool
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private static func decodeSolanaForSummary(_ txParams: [String: Any]) -> SolanaSummaryInfo? {
|
|
883
|
+
guard let b64 = txParams["unsignedTxBase64"] as? String,
|
|
884
|
+
let txData = Data(base64Encoded: b64) else { return nil }
|
|
885
|
+
let rawBytes = TransactionDecoder.decode(coinType: .solana, encodedTx: txData)
|
|
886
|
+
guard let decoded = try? SolanaDecodingTransactionOutput(serializedBytes: rawBytes),
|
|
887
|
+
decoded.error == .ok else { return nil }
|
|
888
|
+
let accounts = decoded.transaction.legacy.accountKeys
|
|
889
|
+
let instrs = decoded.transaction.legacy.instructions
|
|
890
|
+
|
|
891
|
+
let systemProgram = "11111111111111111111111111111111"
|
|
892
|
+
// SPL Token Program and Token-2022
|
|
893
|
+
let splPrograms: Set<String> = [
|
|
894
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
895
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
896
|
+
]
|
|
897
|
+
|
|
898
|
+
// Find first SystemProgram instruction (skip ComputeBudget etc.)
|
|
899
|
+
// and first SPL Token instruction in a single pass.
|
|
900
|
+
var systemIx: TW_Solana_Proto_RawMessage.Instruction? = nil
|
|
901
|
+
var splIx: TW_Solana_Proto_RawMessage.Instruction? = nil
|
|
902
|
+
for instr in instrs {
|
|
903
|
+
let prog = safeGet(accounts, Int(instr.programID)) ?? ""
|
|
904
|
+
if systemIx == nil && prog == systemProgram { systemIx = instr }
|
|
905
|
+
if splIx == nil && splPrograms.contains(prog) { splIx = instr }
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if let ix = systemIx {
|
|
909
|
+
let to: String? = ix.accounts.count >= 2 ? safeGet(accounts, Int(ix.accounts[1])) : nil
|
|
910
|
+
// SystemProgram Transfer discriminator: [2, 0, 0, 0] as u32-LE
|
|
911
|
+
let isTransfer = ix.programData.count >= 12 && ix.programData.prefix(4) == Data([2, 0, 0, 0])
|
|
912
|
+
var lamports: UInt64?
|
|
913
|
+
if isTransfer {
|
|
914
|
+
var v: UInt64 = 0
|
|
915
|
+
for (i, b) in ix.programData.dropFirst(4).prefix(8).enumerated() { v |= UInt64(b) << (i * 8) }
|
|
916
|
+
lamports = v
|
|
917
|
+
}
|
|
918
|
+
return SolanaSummaryInfo(to: to, lamports: lamports, isTransfer: isTransfer,
|
|
919
|
+
splDest: nil, splAmount: nil, isSplTransfer: false)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if let ix = splIx {
|
|
923
|
+
let data = ix.programData
|
|
924
|
+
// SPL instruction byte 0: 3 = Transfer, 12 = TransferChecked
|
|
925
|
+
// Transfer: accounts[0]=src, accounts[1]=dest, accounts[2]=owner; data[1..8]=amount LE u64
|
|
926
|
+
// TransferChecked: accounts[0]=src, accounts[1]=mint, accounts[2]=dest, accounts[3]=owner
|
|
927
|
+
if !data.isEmpty && data[0] == 3 && data.count >= 9 {
|
|
928
|
+
let dest = ix.accounts.count >= 2 ? safeGet(accounts, Int(ix.accounts[1])) : nil
|
|
929
|
+
var amount: UInt64 = 0
|
|
930
|
+
for (i, b) in data.dropFirst(1).prefix(8).enumerated() { amount |= UInt64(b) << (i * 8) }
|
|
931
|
+
return SolanaSummaryInfo(to: nil, lamports: nil, isTransfer: false,
|
|
932
|
+
splDest: dest, splAmount: amount, isSplTransfer: true)
|
|
933
|
+
} else if !data.isEmpty && data[0] == 12 && data.count >= 10 {
|
|
934
|
+
let dest = ix.accounts.count >= 3 ? safeGet(accounts, Int(ix.accounts[2])) : nil
|
|
935
|
+
var amount: UInt64 = 0
|
|
936
|
+
for (i, b) in data.dropFirst(1).prefix(8).enumerated() { amount |= UInt64(b) << (i * 8) }
|
|
937
|
+
return SolanaSummaryInfo(to: nil, lamports: nil, isTransfer: false,
|
|
938
|
+
splDest: dest, splAmount: amount, isSplTransfer: true)
|
|
939
|
+
}
|
|
940
|
+
return SolanaSummaryInfo(to: nil, lamports: nil, isTransfer: false,
|
|
941
|
+
splDest: nil, splAmount: nil, isSplTransfer: false)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return nil
|
|
945
|
+
}
|
|
946
|
+
|
|
561
947
|
private static func txHexToDouble(_ hex: String) -> Double {
|
|
562
948
|
let s = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
|
|
563
949
|
if let v = UInt64(s, radix: 16) { return Double(v) }
|
|
@@ -575,6 +961,10 @@ enum ChainSigner {
|
|
|
575
961
|
|
|
576
962
|
// MARK: - Helpers
|
|
577
963
|
|
|
964
|
+
private static func safeGet<T>(_ array: [T], _ index: Int) -> T? {
|
|
965
|
+
array.indices.contains(index) ? array[index] : nil
|
|
966
|
+
}
|
|
967
|
+
|
|
578
968
|
// Parses a hex string (with or without 0x, odd or even length) into Data. An empty string
|
|
579
969
|
// deliberately maps to a single zero byte (fields like valueHex already default to "0"
|
|
580
970
|
// when absent — that's a legitimate zero-value transfer, not malformed input). Any
|