Skip to content

Commit

Permalink
Merge pull request #53 from EjaraApp/multiSig
Browse files Browse the repository at this point in the history
btc multi sign
  • Loading branch information
baahkusi authored Sep 15, 2023
2 parents 96869a0 + 3e0e8d7 commit c868a8c
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 9 deletions.
19 changes: 17 additions & 2 deletions android/src/main/kotlin/africa/ejara/trustdart/Coin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<String, Any>,
privateKeys: ArrayList<String>
): String? {
val opJson = JSONObject(txData).toString()
val signatures = mutableListOf<String>()

for (privateKey in privateKeys) {
val signature = AnySigner.signJSON(opJson, privateKey.toByteArray(), coinType!!.value())
signatures.add(signature)
}
return signatures.joinToString(",")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -164,6 +164,35 @@ class TrustdartPlugin : FlutterPlugin, MethodCallHandler {
validator.details.errorDetails
)
}
"multiSignTransaction" -> {
val coin: String? = call.argument("coin")
val txData: Map<String, Any>? = call.argument("txData")
val privateKeys: ArrayList<String>? = 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")
Expand Down
57 changes: 56 additions & 1 deletion android/src/main/kotlin/africa/ejara/trustdart/coins/BTC.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {

Expand Down Expand Up @@ -77,4 +80,56 @@ class BTC : Coin("BTC", CoinType.BITCOIN) {
return Numeric.toHexString(output.encoded.toByteArray())
}

}

override fun multiSignTransaction(
txData: Map<String, Any>,
privateKeys: ArrayList<String>
): String? {
val utxos: List<Map<String, Any>> = txData["utxos"] as List<Map<String, Any>>

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<ByteString> = 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())
}

};

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String>?
Expand All @@ -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<String, Any>, mnemonic: String, passphrase: String): String?
fun multiSignTransaction(
txData: Map<String, Any>,
privateKeys: ArrayList<String>
): String?
}
14 changes: 13 additions & 1 deletion example/lib/operations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,13 @@ Map<String, dynamic> 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
Expand Down Expand Up @@ -365,6 +371,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,
Expand Down
19 changes: 18 additions & 1 deletion ios/Classes/Coin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Coin: CoinProtocol {
init(name: String, coinType: CoinType){
self.name = name
self.coinType = coinType
}
}

func getName() -> String {
return self.name
Expand Down Expand Up @@ -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: ",")
}
}
}
23 changes: 22 additions & 1 deletion ios/Classes/SwiftTrustdartPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
41 changes: 40 additions & 1 deletion ios/Classes/coins/BTC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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

}

}
2 changes: 2 additions & 0 deletions ios/Classes/protocols/CoinProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

}
15 changes: 15 additions & 0 deletions lib/trustdart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ class Trustdart {
}
}

static Future<String> multiSignTransaction(
String coin, Map txData, List<String>? privateKeys) async {
try {
final String txHash = await _channel.invokeMethod(
'multiSignTransaction', <String, dynamic>{
'coin': coin,
'txData': txData,
'privateKeys': privateKeys
});
return txHash;
} catch (e) {
return '';
}
}

static Future<String> signDataWithPrivateKey(
String mnemonic, String coin, String path, String txData,
[String passphrase = ""]) async {
Expand Down

0 comments on commit c868a8c

Please sign in to comment.