diff --git a/include/TrustWalletCore/TWTransactionCompiler.h b/include/TrustWalletCore/TWTransactionCompiler.h index 67dfc4f0124..6966e1a91cc 100644 --- a/include/TrustWalletCore/TWTransactionCompiler.h +++ b/include/TrustWalletCore/TWTransactionCompiler.h @@ -42,6 +42,11 @@ TWData* _Nonnull TWTransactionCompilerCompileWithSignatures( enum TWCoinType coinType, TWData* _Nonnull txInputData, const struct TWDataVector* _Nonnull signatures, const struct TWDataVector* _Nonnull publicKeys); +TW_EXPORT_STATIC_METHOD +TWData *_Nonnull TWTransactionCompilerCompileWithMultipleSignatures( + enum TWCoinType coinType, TWData *_Nonnull txInputData, + const struct TWDataVector *_Nonnull signatures, const struct TWDataVector *_Nonnull publicKeys); + TW_EXPORT_STATIC_METHOD TWData *_Nonnull TWTransactionCompilerCompileWithSignaturesAndPubKeyType( enum TWCoinType coinType, TWData *_Nonnull txInputData, @@ -49,3 +54,4 @@ TWData *_Nonnull TWTransactionCompilerCompileWithSignaturesAndPubKeyType( enum TWPublicKeyType pubKeyType); TW_EXTERN_C_END + diff --git a/src/Cardano/Entry.cpp b/src/Cardano/Entry.cpp index 688ac1ed355..4d68e590b3d 100644 --- a/src/Cardano/Entry.cpp +++ b/src/Cardano/Entry.cpp @@ -60,4 +60,32 @@ void Entry::compile([[maybe_unused]] TWCoinType coin, const Data& txInputData, c }); } +void Entry::compileWithMultipleSignatures([[maybe_unused]] TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const { + auto input = Proto::SigningInput(); + auto output = Proto::SigningOutput(); + + if (!input.ParseFromArray(txInputData.data(), (int)txInputData.size())) { + output.set_error(Common::Proto::Error_input_parse); + output.set_error_message("failed to parse input data"); + dataOut = TW::data(output.SerializeAsString()); + return; + } + + if (signatures.empty() || publicKeys.empty()) { + output.set_error(Common::Proto::Error_invalid_params); + output.set_error_message("empty signatures or publickeys"); + dataOut = TW::data(output.SerializeAsString()); + return; + } + + try { + auto encoded = Signer::encodeTransactionWithSignatures(input, publicKeys, signatures); + output.set_encoded(encoded.data(), encoded.size()); + } catch (const std::exception& e) { + output.set_error(Common::Proto::Error_internal); + output.set_error_message(e.what()); + } + dataOut = TW::data(output.SerializeAsString()); +} + } // namespace TW::Cardano diff --git a/src/Cardano/Entry.h b/src/Cardano/Entry.h index ed73e24e5c7..f7874af85c7 100644 --- a/src/Cardano/Entry.h +++ b/src/Cardano/Entry.h @@ -20,6 +20,8 @@ class Entry final : public CoinEntry { Data preImageHashes(TWCoinType coin, const Data& txInputData) const; void compile(TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const; + void compileWithMultipleSignatures(TWCoinType coin, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& dataOut) const; + }; } // namespace TW::Cardano diff --git a/src/Cardano/Signer.cpp b/src/Cardano/Signer.cpp index 80336de5e94..5a0928a0f9f 100644 --- a/src/Cardano/Signer.cpp +++ b/src/Cardano/Signer.cpp @@ -614,6 +614,60 @@ Data Signer::encodeTransactionWithSig(const Proto::SigningInput &input, const Pu return cbor.encoded(); } +Data Signer::encodeTransactionWithSignatures( + const Proto::SigningInput &input, + const std::vector& publicKeys, + const std::vector& signatures + ) { + Transaction txAux; + auto buildRet = buildTx(txAux, input); + if (buildRet != Common::Proto::OK) { + throw Common::Proto::SigningError(buildRet); + } + + std::vector> keySignaturesPairs = convertToKeySignaturePairs(publicKeys, signatures); + + bool hasLegacyUtxos = false; + for (const auto& utxo : input.utxos()) { + if (AddressV2::isValid(utxo.address())) { + hasLegacyUtxos = true; + break; + } + } + + const auto sigsCbor = cborizeSignatures(keySignaturesPairs, hasLegacyUtxos); + + // Cbor-encode txAux & signatures + const auto cbor = Cbor::Encode::array({ + // txaux + Cbor::Encode::fromRaw(txAux.encode()), + // signatures + sigsCbor, + // aux data + Cbor::Encode::null(), + }); + + return cbor.encoded(); +} + +std::vector> Signer::convertToKeySignaturePairs(const std::vector& keys, const std::vector& signatures) { + // Check if the two vectors are of the same size + if (keys.size() != signatures.size()) { + throw std::invalid_argument("Vectors must be of the same size."); + } + + std::vector> result; + result.reserve(keys.size()); // Reserve space to avoid reallocations + + // Iterate over the vectors and create pairs + for (size_t i = 0; i < keys.size(); ++i) { + result.emplace_back(keys[i].bytes, signatures[i]); + } + + return result; +} + + Common::Proto::SigningError Signer::buildTx(Transaction& tx, const Proto::SigningInput& input) { auto plan = Signer(input).doPlan(); return buildTransactionAux(tx, input, plan); diff --git a/src/Cardano/Signer.h b/src/Cardano/Signer.h index d6036e8d2d8..2c42435471c 100644 --- a/src/Cardano/Signer.h +++ b/src/Cardano/Signer.h @@ -42,6 +42,8 @@ class Signer { // Build encoded transaction static Common::Proto::SigningError encodeTransaction(Data& encoded, Data& txId, const Proto::SigningInput& input, const TransactionPlan& plan, bool sizeEstimationOnly = false); static Data encodeTransactionWithSig(const Proto::SigningInput &input, const PublicKey &publicKey, const Data &signature); + static Data encodeTransactionWithSignatures(const Proto::SigningInput &input, const std::vector& publicKeys, const std::vector& signatures); + static std::vector> convertToKeySignaturePairs(const std::vector& keys, const std::vector& signatures); // Build aux transaction object, using input and plan static Common::Proto::SigningError buildTransactionAux(Transaction& tx, const Proto::SigningInput& input, const TransactionPlan& plan); static Common::Proto::SigningError buildTx(Transaction& tx, const Proto::SigningInput& input); diff --git a/src/Coin.cpp b/src/Coin.cpp index 447c5c2f5a0..eb7d124ecb3 100644 --- a/src/Coin.cpp +++ b/src/Coin.cpp @@ -336,6 +336,12 @@ void TW::anyCoinCompileWithSignatures(TWCoinType coinType, const Data& txInputDa dispatcher->compile(coinType, txInputData, signatures, publicKeys, txOutputOut); } +void TW::anyCoinCompileWithMultipleSignatures(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& txOutputOut) { + auto* dispatcher = coinDispatcher(coinType); + assert(dispatcher != nullptr); + dispatcher->compileWithMultipleSignatures(coinType, txInputData, signatures, publicKeys, txOutputOut); +} + // Coin info accessors extern const CoinInfo getCoinInfo(TWCoinType coin); // in generated CoinInfoData.cpp file diff --git a/src/Coin.h b/src/Coin.h index 35ad5bc1d59..7f30c73d936 100644 --- a/src/Coin.h +++ b/src/Coin.h @@ -124,6 +124,9 @@ Data anyCoinPreImageHashes(TWCoinType coinType, const Data& txInputData); void anyCoinCompileWithSignatures(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& txOutputOut); +void anyCoinCompileWithMultipleSignatures(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, Data& txOutputOut); + + // Describes a derivation: path + optional format + optional name struct Derivation { TWDerivation name = TWDerivationDefault; diff --git a/src/CoinEntry.h b/src/CoinEntry.h index 569593dac84..f5763730e98 100644 --- a/src/CoinEntry.h +++ b/src/CoinEntry.h @@ -68,6 +68,8 @@ class CoinEntry { virtual Data preImageHashes([[maybe_unused]] TWCoinType coin, [[maybe_unused]] const Data& txInputData) const { return {}; } // Optional method for compiling a transaction with externally-supplied signatures & pubkeys. virtual void compile([[maybe_unused]] TWCoinType coin, [[maybe_unused]] const Data& txInputData, [[maybe_unused]] const std::vector& signatures, [[maybe_unused]] const std::vector& publicKeys, [[maybe_unused]] Data& dataOut) const {} + virtual void compileWithMultipleSignatures([[maybe_unused]] TWCoinType coin, [[maybe_unused]] const Data& txInputData, [[maybe_unused]] const std::vector& signatures, [[maybe_unused]] const std::vector& publicKeys, [[maybe_unused]] Data& dataOut) const {} + }; // In each coin's Entry.cpp the specific types of the coin are used, this template enforces the Signer implement: diff --git a/src/TransactionCompiler.cpp b/src/TransactionCompiler.cpp index b9ee21bb760..f571629f95a 100644 --- a/src/TransactionCompiler.cpp +++ b/src/TransactionCompiler.cpp @@ -28,6 +28,21 @@ Data TransactionCompiler::compileWithSignatures(TWCoinType coinType, const Data& return txOutput; } +Data TransactionCompiler::compileWithMultipleSignatures(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys) { + std::vector pubs; + const auto publicKeyType = ::publicKeyType(coinType); + for (auto& p: publicKeys) { + if (!PublicKey::isValid(p, publicKeyType)) { + throw std::invalid_argument("Invalid public key"); + } + pubs.push_back(PublicKey(p, publicKeyType)); + } + + Data txOutput; + anyCoinCompileWithMultipleSignatures(coinType, txInputData, signatures, pubs, txOutput); + return txOutput; +} + Data TransactionCompiler::compileWithSignaturesAndPubKeyType(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, enum TWPublicKeyType pubKeyType) { std::vector pubs; for (auto& p: publicKeys) { diff --git a/src/TransactionCompiler.h b/src/TransactionCompiler.h index 0c12b4dd23c..2f7d4456346 100644 --- a/src/TransactionCompiler.h +++ b/src/TransactionCompiler.h @@ -24,9 +24,13 @@ class TransactionCompiler { /// Compile a complete transation with an external signature, put together from transaction input and provided public key and signature static Data compileWithSignatures(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys); + + /// Compile a complete transation with an external signatures, put together from transaction input and provided public keys and signatures + static Data compileWithMultipleSignatures(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys); static Data compileWithSignaturesAndPubKeyType(TWCoinType coinType, const Data& txInputData, const std::vector& signatures, const std::vector& publicKeys, TWPublicKeyType pubKeyType); + }; } // namespace TW diff --git a/src/interface/TWTransactionCompiler.cpp b/src/interface/TWTransactionCompiler.cpp index 5db643063ee..48cbf4929c4 100644 --- a/src/interface/TWTransactionCompiler.cpp +++ b/src/interface/TWTransactionCompiler.cpp @@ -37,6 +37,21 @@ TWData *_Nonnull TWTransactionCompilerCompileWithSignatures(enum TWCoinType coin return TWDataCreateWithBytes(result.data(), result.size()); } +TWData *_Nonnull TWTransactionCompilerCompileWithMultipleSignatures(enum TWCoinType coinType, TWData *_Nonnull txInputData, const struct TWDataVector *_Nonnull signatures, const struct TWDataVector *_Nonnull publicKeys) { + Data result; + try { + assert(txInputData != nullptr); + const Data inputData = data(TWDataBytes(txInputData), TWDataSize(txInputData)); + assert(signatures != nullptr); + const auto signaturesVec = createFromTWDataVector(signatures); + assert(publicKeys != nullptr); + const auto publicKeysVec = createFromTWDataVector(publicKeys); + + result = TransactionCompiler::compileWithMultipleSignatures(coinType, inputData, signaturesVec, publicKeysVec); + } catch (...) {} // return empty + return TWDataCreateWithBytes(result.data(), result.size()); +} + TWData *_Nonnull TWTransactionCompilerCompileWithSignaturesAndPubKeyType(enum TWCoinType coinType, TWData *_Nonnull txInputData, const struct TWDataVector *_Nonnull signatures, const struct TWDataVector *_Nonnull publicKeys, enum TWPublicKeyType pubKeyType) { Data result; try { @@ -51,3 +66,4 @@ TWData *_Nonnull TWTransactionCompilerCompileWithSignaturesAndPubKeyType(enum TW } catch (...) {} // return empty return TWDataCreateWithBytes(result.data(), result.size()); } + diff --git a/swift/Sources/Extensions/Data+Hex.swift b/swift/Sources/Extensions/Data+Hex.swift index e4c4582fd0d..f57ac017624 100644 --- a/swift/Sources/Extensions/Data+Hex.swift +++ b/swift/Sources/Extensions/Data+Hex.swift @@ -6,7 +6,7 @@ import Foundation extension Data { /// Initializes `Data` with a hex string representation. - init?(hexString: String) { + public init?(hexString: String) { let string: String if hexString.hasPrefix("0x") { string = String(hexString.dropFirst(2)) @@ -56,7 +56,7 @@ extension Data { } /// Returns the hex string representation of the data. - var hexString: String { + public var hexString: String { return map({ String(format: "%02x", $0) }).joined() } } diff --git a/swift/Tests/Blockchains/CardanoTests.swift b/swift/Tests/Blockchains/CardanoTests.swift index 3176da700d9..3872b61d32c 100644 --- a/swift/Tests/Blockchains/CardanoTests.swift +++ b/swift/Tests/Blockchains/CardanoTests.swift @@ -334,4 +334,92 @@ class CardanoTests: XCTestCase { let txid = output.txID XCTAssertEqual(txid.hexString, "87ca43a36b09c0b140f0ef2b71fbdcfcf1fdc88f7aa378b861e8eed3e8974628") } + + func testSignStakingRegisterAndDelegateExternalSign() throws { + // input + let ownAddress = "addr1q8043m5heeaydnvtmmkyuhe6qv5havvhsf0d26q3jygsspxlyfpyk6yqkw0yhtyvtr0flekj84u64az82cufmqn65zdsylzk23" + let stakingAddress = Cardano.getStakingAddress(baseAddress: ownAddress) + let poolIdNufi = "7d7ac07a2f2a25b7a4db868a40720621c4939cf6aefbb9a11464f1a6" + + var input = CardanoSigningInput.with { + $0.transferMessage.toAddress = ownAddress + $0.transferMessage.changeAddress = ownAddress + $0.transferMessage.amount = 4000000 // not relevant as we use MaxAmount + $0.transferMessage.useMaxAmount = true + $0.ttl = 69885081 + // Register staking key, 2 ADA desposit + $0.registerStakingKey.stakingAddress = stakingAddress + $0.registerStakingKey.depositAmount = 2000000 + // Delegate + $0.delegate.stakingAddress = stakingAddress + $0.delegate.poolID = Data(hexString: poolIdNufi)! + $0.delegate.depositAmount = 0 + } + + let utxo1 = CardanoTxInput.with { + $0.outPoint.txHash = Data(hexString: "9b06de86b253549b99f6a050b61217d8824085ca5ed4eb107a5e7cce4f93802e")! + $0.outPoint.outputIndex = 0 + $0.address = ownAddress + $0.amount = 4000000 + } + let utxo2 = CardanoTxInput.with { + $0.outPoint.txHash = Data(hexString: "9b06de86b253549b99f6a050b61217d8824085ca5ed4eb107a5e7cce4f93802e")! + $0.outPoint.outputIndex = 1 + $0.address = ownAddress + $0.amount = 26651312 + } + input.utxos.append(utxo1) + input.utxos.append(utxo2) + + // public/private keys + let privateKeyData = Data(hexString: "089b68e458861be0c44bf9f7967f05cc91e51ede86dc679448a3566990b7785bd48c330875b1e0d03caaed0e67cecc42075dce1c7a13b1c49240508848ac82f603391c68824881ae3fc23a56a1a75ada3b96382db502e37564e84a5413cfaf1290dbd508e5ec71afaea98da2df1533c22ef02a26bb87b31907d0b2738fb7785b38d53aa68fc01230784c9209b2b2a2faf28491b3b1f1d221e63e704bbd0403c4154425dfbb01a2c5c042da411703603f89af89e57faae2946e2a5c18b1c5ca0e")! + + let privateKeyBytes = Array(privateKeyData) + var stakingPrivateKeyBytes = privateKeyBytes[privateKeyBytes.count / 2 ..< privateKeyBytes.count] + stakingPrivateKeyBytes.append(contentsOf: Data(repeating: 0, count: 96)) + let stakingPrivateKeyData = Data(stakingPrivateKeyBytes) + + let privateKey = PrivateKey(data: privateKeyData)! + let stakingPrivateKey = PrivateKey(data: stakingPrivateKeyData)! + + let publicKey = privateKey.getPublicKeyByType(pubkeyType: .ed25519Cardano) + let stakingPublicKey = stakingPrivateKey.getPublicKeyByType(pubkeyType: .ed25519Cardano) + + // hash to sign + let txInputData = try input.serializedData() + + let preImageHashes = TransactionCompiler.preImageHashes(coinType: .cardano, txInputData: txInputData) + let preSigningOutput = try TxCompilerPreSigningOutput(serializedData: preImageHashes) + let hash = preSigningOutput.dataHash + + // signatures + let defaultSignature = privateKey.sign(digest: hash, curve: .ed25519ExtendedCardano)! + let stakingSignature = stakingPrivateKey.sign(digest: hash, curve: .ed25519ExtendedCardano)! + + let signatures = DataVector() + signatures.add(data: defaultSignature) + signatures.add(data: stakingSignature) + + let publicKeys = DataVector() + + publicKeys.add(data: publicKey.data) + publicKeys.add(data: stakingPublicKey.data) + + let serializedOutput = TransactionCompiler.compileWithMultipleSignatures( + coinType: .cardano, + txInputData: txInputData, + signatures: signatures, + publicKeys: publicKeys + ) + + let signingOutput = try CardanoSigningOutput(serializedData: serializedOutput) + + let encoded = signingOutput.encoded + XCTAssertEqual( + encoded.hexString, + "83a500828258209b06de86b253549b99f6a050b61217d8824085ca5ed4eb107a5e7cce4f93802e008258209b06de86b253549b99f6a050b61217d8824085ca5ed4eb107a5e7cce4f93802e01018182583901df58ee97ce7a46cd8bdeec4e5f3a03297eb197825ed5681191110804df22424b6880b39e4bac8c58de9fe6d23d79aaf44756389d827aa09b1a01b27ef5021a0002b03b031a042a5c99048282008200581cdf22424b6880b39e4bac8c58de9fe6d23d79aaf44756389d827aa09b83028200581cdf22424b6880b39e4bac8c58de9fe6d23d79aaf44756389d827aa09b581c7d7ac07a2f2a25b7a4db868a40720621c4939cf6aefbb9a11464f1a6a100828258206d8a0b425bd2ec9692af39b1c0cf0e51caa07a603550e22f54091e872c7df2905840677c901704be027d9a1734e8aa06f0700009476fa252baaae0de280331746a320a61456d842d948ea5c0e204fc36f3bd04c88ca7ee3d657d5a38014243c37c07825820e554163344aafc2bbefe778a6953ddce0583c2f8e0a0686929c020ca33e0693258401fa21bdc62b85ca217bf08cbacdeba2fadaf33dc09ee3af9cc25b40f24822a1a42cfbc03585cc31a370ef75aaec4d25db6edcf329e40a4e725ec8718c94f220af6" + ) + + XCTAssertEqual(hash.hexString, "96a781fd6481b6a7fd3926da110265e8c44b53947b81daa84da5b148825d02aa") + } } diff --git a/swift/Tests/Blockchains/TheOpenNetworkTests.swift b/swift/Tests/Blockchains/TheOpenNetworkTests.swift index 00c2833946b..da7c48dc2a6 100644 --- a/swift/Tests/Blockchains/TheOpenNetworkTests.swift +++ b/swift/Tests/Blockchains/TheOpenNetworkTests.swift @@ -108,7 +108,7 @@ class TheOpenNetworkTests: XCTestCase { let privateKeyData = Data(hexString: "c054900a527538c1b4325688a421c0469b171c29f23a62da216e90b0df2412ee")! let jettonTransfer = TheOpenNetworkJettonTransfer.with { - $0.jettonAmount = 500 * 1000 * 1000 + $0.jettonAmount = Data(hexString: "1dcd6500")! // 500 * 1000 * 1000 $0.toOwner = "EQAFwMs5ha8OgZ9M4hQr80z9NkE7rGxUpE1hCFndiY6JnDx8" $0.responseAddress = "EQBaKIMq5Am2p_rfR1IFTwsNWHxBkOpLTmwUain5Fj4llTXk" $0.forwardAmount = 1 diff --git a/version.properties b/version.properties index 7131632bb93..d235641732f 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -tangem=4.1.20-tangem4 +tangem=4.1.20-tangem5