Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

btc multi sign #53

Merged
merged 4 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -102,7 +102,13 @@ Map<String, dynamic> operations = {
"amount": 3000,
"fees": 1000,
"changeAddress": "15o5bzVX58t1NRvLchBUGuHscCs1sumr2R",
"change": 500
"change": 500,
"privateKeys": [
"a321c4996143e0add05864bbb694ceb399fbe5d0884d721b1a04755f9f7497a9",
"bbc27228ddcb9209d7fd6f36b02f7dfa6252af40bb2f1cbc7a557da8027ff866",
Rakeally marked this conversation as resolved.
Show resolved Hide resolved
"619c335025c7f4012e556c2a58b2506e30b8511b53ade95ea316fd8c3286feb9",
"eae04f225475e7630e58efdbefe50a003efd7e2ade3e67e171e023e9278b6ea4"
]
},
'TRX': {
"cmd": "TRC20", // can be TRC20 | TRX | TRC10 | CONTRACT | FREEZE
Expand Down Expand Up @@ -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,
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 @@ -171,6 +171,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