diff --git a/android/src/main/kotlin/africa/ejara/trustdart/Coin.kt b/android/src/main/kotlin/africa/ejara/trustdart/Coin.kt index 7490432..f8f5b17 100644 --- a/android/src/main/kotlin/africa/ejara/trustdart/Coin.kt +++ b/android/src/main/kotlin/africa/ejara/trustdart/Coin.kt @@ -4,6 +4,7 @@ import africa.ejara.trustdart.interfaces.CoinInterface import africa.ejara.trustdart.utils.base64String import africa.ejara.trustdart.utils.toHex import africa.ejara.trustdart.utils.toHexByteArray +import com.google.protobuf.ByteString import org.json.JSONObject import wallet.core.java.AnySigner import wallet.core.jni.CoinType @@ -43,13 +44,13 @@ open class Coin(nameOfCoin: String, typeOfCoin: CoinType) : CoinInterface { override fun getRawPrivateKey(path: String, mnemonic: String, passphrase: String): ByteArray? { val wallet = HDWallet(mnemonic, passphrase) return wallet.getKey(coinType, path).data() - } + } override fun getPublicKey(path: String, mnemonic: String, passphrase: String): String? { val wallet = HDWallet(mnemonic, passphrase) return wallet.getKey(coinType, path).getPublicKeySecp256k1(true).data().base64String() } - + override fun getRawPublicKey(path: String, mnemonic: String, passphrase: String): ByteArray? { val wallet = HDWallet(mnemonic, passphrase) return wallet.getKey(coinType, path).getPublicKeySecp256k1(true).data() @@ -82,4 +83,18 @@ open class Coin(nameOfCoin: String, typeOfCoin: CoinType) : CoinInterface { return AnySigner.signJSON(opJson, privateKey.data(), coinType!!.value()) } + override fun multiSignTransaction( + txData: Map, + privateKeys: ArrayList + ): String? { + val opJson = JSONObject(txData).toString() + val signatures = mutableListOf() + + for (privateKey in privateKeys) { + val signature = AnySigner.signJSON(opJson, privateKey.toByteArray(), coinType!!.value()) + signatures.add(signature) + } + return signatures.joinToString(",") + } + } diff --git a/android/src/main/kotlin/africa/ejara/trustdart/TrustdartPlugin.kt b/android/src/main/kotlin/africa/ejara/trustdart/TrustdartPlugin.kt index 36b3795..f9984c6 100644 --- a/android/src/main/kotlin/africa/ejara/trustdart/TrustdartPlugin.kt +++ b/android/src/main/kotlin/africa/ejara/trustdart/TrustdartPlugin.kt @@ -56,7 +56,7 @@ class TrustdartPlugin : FlutterPlugin, MethodCallHandler { validator.details.errorMessage, validator.details.errorDetails ) - } + } "checkMnemonic" -> { val mnemonic: String? = call.argument("mnemonic") val passphrase: String? = call.argument("passphrase") @@ -164,6 +164,35 @@ class TrustdartPlugin : FlutterPlugin, MethodCallHandler { validator.details.errorDetails ) } + "multiSignTransaction" -> { + val coin: String? = call.argument("coin") + val txData: Map? = call.argument("txData") + val privateKeys: ArrayList? = call.argument("privateKeys") + var validator = WalletHandler().validate( + WalletError( + WalletHandlerErrorCodes.ArgumentsNull, + "[coin], [privateKeys] and [txData] are required.", + null + ), arrayOf(coin, txData, privateKeys) + ) + if (validator.isValid) { + val txHash = WalletHandler().getCoin(coin) + .multiSignTransaction(txData!!, privateKeys!!) + validator = WalletHandler().validate( + WalletError( + WalletHandlerErrorCodes.TxHashNull, + "Could not sign transaction.", + null + ), arrayOf(txHash) + ) + if (validator.isValid) return result.success(txHash) + } + return result.error( + validator.details.errorCode, + validator.details.errorMessage, + validator.details.errorDetails + ) + } "signDataWithPrivateKey" -> { val coin: String? = call.argument("coin") val path: String? = call.argument("path") diff --git a/android/src/main/kotlin/africa/ejara/trustdart/coins/BTC.kt b/android/src/main/kotlin/africa/ejara/trustdart/coins/BTC.kt index 3825345..3c35715 100644 --- a/android/src/main/kotlin/africa/ejara/trustdart/coins/BTC.kt +++ b/android/src/main/kotlin/africa/ejara/trustdart/coins/BTC.kt @@ -1,5 +1,6 @@ import africa.ejara.trustdart.Coin import africa.ejara.trustdart.Numeric +import africa.ejara.trustdart.utils.toHex import africa.ejara.trustdart.utils.toLong import com.google.protobuf.ByteString import wallet.core.java.AnySigner @@ -9,6 +10,8 @@ import wallet.core.jni.CoinType import wallet.core.jni.HDWallet import wallet.core.jni.proto.Bitcoin + + class BTC : Coin("BTC", CoinType.BITCOIN) { @@ -77,4 +80,56 @@ class BTC : Coin("BTC", CoinType.BITCOIN) { return Numeric.toHexString(output.encoded.toByteArray()) } -} \ No newline at end of file + + override fun multiSignTransaction( + txData: Map, + privateKeys: ArrayList + ): String? { + val utxos: List> = txData["utxos"] as List> + + val input = Bitcoin.SigningInput.newBuilder() + .setAmount(txData["amount"]!!.toLong()) + .setHashType(BitcoinScript.hashTypeForCoin(coinType)) + .setToAddress(txData["toAddress"] as String) + .setChangeAddress(txData["changeAddress"] as String) + .setByteFee(1) + + val byteStrings: MutableList = privateKeys.map { ByteString.copyFrom(Numeric.hexStringToByteArray(it)) }.toMutableList() + + input.addAllPrivateKey(byteStrings); + + for (utx in utxos) { + val txHash = Numeric.hexStringToByteArray(utx["txid"] as String) + txHash.reverse() + val outPoint = Bitcoin.OutPoint.newBuilder() + .setHash(ByteString.copyFrom(txHash)) + .setIndex(utx["vout"] as Int) + .setSequence(Long.MAX_VALUE.toInt()) + .build() + val txScript = Numeric.hexStringToByteArray(utx["script"] as String) + val utxo = Bitcoin.UnspentTransaction.newBuilder() + .setAmount(utx["value"]!!.toLong()) + .setOutPoint(outPoint) + .setScript(ByteString.copyFrom(txScript)) + .build() + input.addUtxo(utxo) + } + + var output = AnySigner.sign(input.build(), coinType, Bitcoin.SigningOutput.parser()) + + // since we want to set our own fee + // but such functionality is not obvious in the trustwalletcore library + // a hack is used for now to calculate the byteFee + val size = output.encoded.toByteArray().size + val fees = txData["fees"]!!.toLong() + if (size > 0) { // prevent division by zero + val byteFee = fees.div(size) // this gives the fee per byte truncated to Long + // now we set new byte size + if (byteFee > 1) input.byteFee = byteFee + } + output = AnySigner.sign(input.build(), coinType, Bitcoin.SigningOutput.parser()) + return Numeric.toHexString(output.encoded.toByteArray()) + } + +}; + \ No newline at end of file diff --git a/android/src/main/kotlin/africa/ejara/trustdart/coins/XTZ.kt b/android/src/main/kotlin/africa/ejara/trustdart/coins/XTZ.kt index 07de6bd..669ef35 100644 --- a/android/src/main/kotlin/africa/ejara/trustdart/coins/XTZ.kt +++ b/android/src/main/kotlin/africa/ejara/trustdart/coins/XTZ.kt @@ -4,7 +4,6 @@ import africa.ejara.trustdart.utils.base64String import africa.ejara.trustdart.utils.toHex import africa.ejara.trustdart.utils.toHexBytes import africa.ejara.trustdart.utils.toLong -import android.util.Log import com.google.protobuf.ByteString import org.json.JSONObject import wallet.core.java.AnySigner diff --git a/android/src/main/kotlin/africa/ejara/trustdart/interfaces/CoinInterface.kt b/android/src/main/kotlin/africa/ejara/trustdart/interfaces/CoinInterface.kt index 752a20c..a7e556b 100644 --- a/android/src/main/kotlin/africa/ejara/trustdart/interfaces/CoinInterface.kt +++ b/android/src/main/kotlin/africa/ejara/trustdart/interfaces/CoinInterface.kt @@ -1,4 +1,6 @@ package africa.ejara.trustdart.interfaces + +import com.google.protobuf.ByteString interface CoinInterface { fun generateAddress(path: String, mnemonic: String, passphrase: String): Map? @@ -10,4 +12,8 @@ interface CoinInterface { fun validateAddress(address: String): Boolean fun signDataWithPrivateKey(path: String, mnemonic: String, passphrase: String, txData: String): String? fun signTransaction(path: String, txData: Map, mnemonic: String, passphrase: String): String? + fun multiSignTransaction( + txData: Map, + privateKeys: ArrayList + ): String? } \ No newline at end of file diff --git a/example/lib/operations.dart b/example/lib/operations.dart index b27fd15..08fb7df 100644 --- a/example/lib/operations.dart +++ b/example/lib/operations.dart @@ -102,7 +102,13 @@ Map operations = { "amount": 3000, "fees": 1000, "changeAddress": "15o5bzVX58t1NRvLchBUGuHscCs1sumr2R", - "change": 500 + "change": 500, + "privateKeys": [ + "a321c4996143e0add05864bbb694ceb399fbe5d0884d721b1a04755f9f7497a9", + "bbc27228ddcb9209d7fd6f36b02f7dfa6252af40bb2f1cbc7a557da8027ff866", + "619c335025c7f4012e556c2a58b2506e30b8511b53ade95ea316fd8c3286feb9", + "eae04f225475e7630e58efdbefe50a003efd7e2ade3e67e171e023e9278b6ea4" + ] }, 'TRX': { "cmd": "TRC20", // can be TRC20 | TRX | TRC10 | CONTRACT | FREEZE @@ -320,6 +326,12 @@ runOperations() async { print('Transaction Check ...'); print([tx]); + print(operations[coin.code]["privateKeys"]); + String multiTxSign = await Trustdart.multiSignTransaction(coin.code, + operations[coin.code], operations[coin.code]["privateKeys"]); + print('MultiSig Transaction Check ...'); + print([multiTxSign]); + String signedData = (await Trustdart.signDataWithPrivateKey( dondo, coin.code, diff --git a/ios/Classes/Coin.swift b/ios/Classes/Coin.swift index bfe1874..5ed68c5 100644 --- a/ios/Classes/Coin.swift +++ b/ios/Classes/Coin.swift @@ -15,7 +15,7 @@ class Coin: CoinProtocol { init(name: String, coinType: CoinType){ self.name = name self.coinType = coinType - } + } func getName() -> String { return self.name @@ -79,4 +79,21 @@ class Coin: CoinProtocol { } return nil } + + + + func multiSignTransaction(txData: [String: Any], privateKeys: [String]) -> String? { + let opJson = Utils.objToJson(from: txData) + var signatures = [String]() + + for privateKey in privateKeys { + let signature = AnySigner.signJSON(opJson!, key: privateKey.data(using: .utf8)!, coin: self.coinType) + signatures.append(signature) + } + if signatures.isEmpty { + return nil + } else { + return signatures.joined(separator: ",") + } +} } diff --git a/ios/Classes/SwiftTrustdartPlugin.swift b/ios/Classes/SwiftTrustdartPlugin.swift index e803fa4..b6d71d8 100644 --- a/ios/Classes/SwiftTrustdartPlugin.swift +++ b/ios/Classes/SwiftTrustdartPlugin.swift @@ -15,7 +15,7 @@ public class SwiftTrustdartPlugin: NSObject, FlutterPlugin { let wallet = WalletHandler().generateMnemonic(strength: 128, passphrase: call.arguments as! String) let (isValid, err) = WalletHandler.validate(walletError: WalletError(code: .noWallet, message: "Could not generate wallet", details: nil), wallet) if isValid { - result(wallet) + result(wallet) }else { result(err.details) } @@ -72,6 +72,26 @@ public class SwiftTrustdartPlugin: NSObject, FlutterPlugin { if isValid { let txHash = WalletHandler().getCoin(coin!).signTransaction(path: path!, txData: txData!, mnemonic: mnemonic!, passphrase: passphrase!) + let (isValid, err) = WalletHandler.validate(walletError: WalletError(code: .txHashNull, message: "Failed to sign transaction.", details: nil), txHash) + if isValid { + result(txHash) + }else { + result(err.details) + } + }else { + result(err.details) + } + + case "multiSignTransaction": + let args = call.arguments as! [String: Any] + let coin: String? = args["coin"] as? String + let txData: [String: Any]? = args["txData"] as? [String: Any] + let privateKeys: [String]? = args["privateKeys"] as? [String] + let (isValid, err) = WalletHandler.validate(walletError: WalletError(code: .argumentsNull, message: "[coin] are required.", details: nil), coin) + + if isValid { + let txHash = WalletHandler().getCoin(coin!).multiSignTransaction(txData: txData!, privateKeys: privateKeys ?? [] ) + let (isValid, err) = WalletHandler.validate(walletError: WalletError(code: .txHashNull, message: "Failed to sign transaction.", details: nil), txHash) if isValid { result(txHash) @@ -81,6 +101,7 @@ public class SwiftTrustdartPlugin: NSObject, FlutterPlugin { }else { result(err.details) } + case "signDataWithPrivateKey": let args = call.arguments as! [String: Any] let coin: String? = args["coin"] as? String diff --git a/ios/Classes/coins/BTC.swift b/ios/Classes/coins/BTC.swift index 6479c0a..86a0aa1 100644 --- a/ios/Classes/coins/BTC.swift +++ b/ios/Classes/coins/BTC.swift @@ -48,7 +48,7 @@ class BTC: Coin { $0.privateKey = [privateKey!.data] $0.plan = BitcoinTransactionPlan.with { $0.amount = txData["amount"] as! Int64 - $0.fee = txData["fees"] as! Int64 + $0.fee = txData["fees"] as! Int64 $0.change = txData["change"] as! Int64 $0.utxos = unspent } @@ -61,4 +61,43 @@ class BTC: Coin { } } + + + + override func multiSignTransaction(txData: [String : Any], privateKeys: [String]) -> String? { + let utxos: [[String: Any]] = txData["utxos"] as! [[String: Any]] + var unspent: [BitcoinUnspentTransaction] = [] + + for utx in utxos { + unspent.append(BitcoinUnspentTransaction.with { + $0.outPoint.hash = Data.reverse(hexString: utx["txid"] as! String) + $0.outPoint.index = utx["vout"] as! UInt32 + $0.outPoint.sequence = UINT32_MAX + $0.amount = utx["value"] as! Int64 + $0.script = Data(hexString: utx["script"] as! String)! + }) + } + let privateKeyDataArray = privateKeys.compactMap { privateKey in + return Data(hexString: privateKey) + } + + let input: BitcoinSigningInput = BitcoinSigningInput.with { + $0.hashType = BitcoinScript.hashTypeForCoin(coinType: .bitcoin) + $0.amount = txData["amount"] as! Int64 + $0.toAddress = txData["toAddress"] as! String + $0.changeAddress = txData["changeAddress"] as! String // can be same sender address + $0.privateKey = privateKeyDataArray + $0.plan = BitcoinTransactionPlan.with { + $0.amount = txData["amount"] as! Int64 + $0.fee = txData["fees"] as! Int64 + $0.change = txData["change"] as! Int64 + $0.utxos = unspent + } + } + + let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .bitcoin) + return output.encoded.hexString + + } + } diff --git a/ios/Classes/protocols/CoinProtocol.swift b/ios/Classes/protocols/CoinProtocol.swift index fb73e4e..9f08df2 100644 --- a/ios/Classes/protocols/CoinProtocol.swift +++ b/ios/Classes/protocols/CoinProtocol.swift @@ -18,4 +18,6 @@ protocol CoinProtocol { func validateAddress(address: String) -> Bool func signDataWithPrivateKey(path: String, mnemonic: String, passphrase: String, txData: String) -> String? func signTransaction(path: String, txData: [String: Any], mnemonic: String, passphrase: String) -> String? + func multiSignTransaction(txData: [String: Any], privateKeys: [String]) -> String? + } diff --git a/lib/trustdart.dart b/lib/trustdart.dart index 0d224cd..5561c07 100644 --- a/lib/trustdart.dart +++ b/lib/trustdart.dart @@ -171,6 +171,21 @@ class Trustdart { } } + static Future multiSignTransaction( + String coin, Map txData, List? privateKeys) async { + try { + final String txHash = await _channel.invokeMethod( + 'multiSignTransaction', { + 'coin': coin, + 'txData': txData, + 'privateKeys': privateKeys + }); + return txHash; + } catch (e) { + return ''; + } + } + static Future signDataWithPrivateKey( String mnemonic, String coin, String path, String txData, [String passphrase = ""]) async {