1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 06:47:46 +01:00

Electrum: consistently retrieve wallet transactions (#1010)

* Electrum: update mainnet and testnet servers list

* Electrum: request missing history transactions on reconnect

Upon connection/reconnection, ask for transactions that are included in our history but
which we don't have and don't have a pending request for.

* Electrum: add disconnect/reconnect tests

Simulate disconnections and check that wallet eventually gets it history and transactions.
This commit is contained in:
Fabrice Drouin 2019-05-23 16:36:54 +02:00 committed by Pierre-Marie Padiou
parent 92e2d21eab
commit 648f57276a
6 changed files with 303 additions and 86 deletions

View file

@ -16,11 +16,6 @@
"t": "50001",
"version": "1.4"
},
"MEADS.hopto.org": {
"pruning": "-",
"s": "50002",
"version": "1.4"
},
"VPS.hsmiths.com": {
"pruning": "-",
"s": "50002",
@ -69,23 +64,12 @@
"t": "50001",
"version": "1.4"
},
"btc.smsys.me": {
"pruning": "-",
"s": "995",
"version": "1.4"
},
"btc.xskyx.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"cashyes.zapto.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"currentlane.lovebitco.in": {
"pruning": "-",
"s": "50002",
@ -121,11 +105,6 @@
"t": "50001",
"version": "1.4"
},
"elec.luggs.co": {
"pruning": "-",
"s": "443",
"version": "1.4"
},
"electrum-server.ninja": {
"pruning": "-",
"s": "50002",
@ -289,17 +268,12 @@
"t": "50001",
"version": "1.4"
},
"kirsche.emzy.de": {
"electrum.emzy.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"luggscoqbymhvnkp.onion": {
"pruning": "-",
"t": "80",
"version": "1.4"
},
"ndnd.selfhost.eu": {
"pruning": "-",
"s": "50002",
@ -396,5 +370,60 @@
"s": "50002",
"t": "50001",
"version": "1.4"
},
"bitcoin.dragon.zone": {
"pruning": "-",
"s": "50004",
"t": "50003",
"version": "1.4"
},
"ecdsa.net" : {
"pruning": "-",
"s": "110",
"t": "50001",
"version": "1.4"
},
"btc.usebsv.com": {
"pruning": "-",
"s": "50006",
"version": "1.4"
},
"e2.keff.org": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"electrum.hodlister.co": {
"pruning": "-",
"s": "50002",
"version": "1.4"
},
"electrum3.hodlister.co": {
"pruning": "-",
"s": "50002",
"version": "1.4"
},
"electrum5.hodlister.co": {
"pruning": "-",
"s": "50002",
"version": "1.4"
},
"electrumx.electricnewyear.net": {
"pruning": "-",
"s": "50002",
"version": "1.4"
},
"fortress.qtornado.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"green-gold.westeurope.cloudapp.azure.com": {
"pruning": "-",
"s": "56002",
"t": "56001",
"version": "1.4"
}
}

View file

@ -1,31 +1,38 @@
{
"electrumx.kekku.li": {
"pruning": "-",
"s": "51002",
"version": "1.2"
},
"hsmithsxurybd7uh.onion": {
"pruning": "-",
"s": "53012",
"t": "53011",
"version": "1.2"
"version": "1.4"
},
"testnet.hsmiths.com": {
"pruning": "-",
"s": "53012",
"t": "53011",
"version": "1.2"
"version": "1.4"
},
"testnet.qtornado.com": {
"pruning": "-",
"s": "51002",
"t": "51001",
"version": "1.2"
"version": "1.4"
},
"testnet1.bauerj.eu": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.2"
"version": "1.4"
},
"tn.not.fyi": {
"pruning": "-",
"s": "55002",
"t": "55001",
"version": "1.4"
},
"bitcoin.cluelessperson.com": {
"pruning": "-",
"s": "51002",
"t": "51001",
"version": "1.4"
}
}

View file

@ -336,7 +336,7 @@ object Blockchain extends Logging {
*/
def getDifficulty(blockchain: Blockchain, height: Int, headerDb: HeaderDb): Option[Long] = {
blockchain.chainHash match {
case Block.LivenetGenesisBlock.hash | Block.RegtestGenesisBlock.hash =>
case Block.LivenetGenesisBlock.hash =>
(height % RETARGETING_PERIOD) match {
case 0 =>
for {

View file

@ -26,7 +26,7 @@ import org.json4s.jackson.JsonMethods
/**
*
* @param hash block hash
* @param target difficulty target for the next block
* @param nextBits difficulty target for the next block
*/
case class CheckPoint(hash: ByteVector32, nextBits: Long)

View file

@ -167,7 +167,6 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.
data.accountKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
data.changeKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
advertiseTransactions(data)
// tell everyone we're ready
goto(RUNNING) using persistAndNotify(data)
} else {
client ! ElectrumClient.GetHeaders(data.blockchain.tip.height + 1, RETARGETING_PERIOD)
@ -237,7 +236,9 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.
}
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if data.status.get(scriptHash) == Some(status) =>
stay using persistAndNotify(data) // we already have it
val missing = data.missingTransactions(scriptHash)
missing.foreach(txid => client ! GetTransaction(txid))
stay using persistAndNotify(data.copy(pendingHistoryRequests = data.pendingTransactionRequests ++ missing))
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if !data.accountKeyMap.contains(scriptHash) && !data.changeKeyMap.contains(scriptHash) =>
log.warning(s"received status=$status for scriptHash=$scriptHash which does not match any of our keys")
@ -379,10 +380,17 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.
case None =>
// missing parents
log.info(s"couldn't connect txid=${tx.txid}")
val data1 = data.copy(pendingTransactions = data.pendingTransactions :+ tx)
val data1 = data.copy(pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = data.pendingTransactions :+ tx)
stay using persistAndNotify(data1)
}
case Event(ServerError(GetTransaction(txid), error), data) if data.pendingTransactionRequests.contains(txid) =>
// server tells us that txid belongs to our wallet history, but cannot provide tx ?
log.error(s"server cannot find history tx $txid: $error")
sender ! PoisonPill
goto(DISCONNECTED) using data
case Event(response@GetMerkleResponse(txid, _, height, _), data) =>
data.blockchain.getHeader(height).orElse(params.walletDb.getHeader(height)) match {
case Some(header) if header.hashMerkleRoot == response.root =>
@ -730,6 +738,16 @@ object ElectrumWallet {
WalletReady(confirmed, unconfirmed, blockchain.tip.height, blockchain.tip.header.time)
}
/**
* @scriptHash script hash
* @return the ids of transactions that belong to our wallet history for this script hash but that we don't have
* and have no pending requests for.
*/
def missingTransactions(scriptHash: ByteVector32): Set[ByteVector32] = {
val txids = history.getOrElse(scriptHash, List()).map(_.tx_hash).filterNot(txhash => transactions.contains(txhash)).toSet
txids -- pendingTransactionRequests
}
/**
*
* @return the current receive key. In most cases it will be a key that has not

View file

@ -22,7 +22,10 @@ import java.sql.DriverManager
import akka.actor.{ActorRef, ActorSystem, Terminated}
import akka.testkit
import akka.testkit.{TestActor, TestFSMRef, TestKit, TestProbe}
import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, MnemonicCode, Satoshi, Transaction, TxOut}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, Crypto, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.bitcoind.rpc.Error
import fr.acinq.eclair.blockchain.electrum.ElectrumClient._
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb
@ -34,6 +37,8 @@ import scala.concurrent.duration._
class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
import ElectrumWalletSimulatedClientSpec._
val sender = TestProbe()
val entropy = ByteVector32(ByteVector.fill(32)(1))
@ -45,7 +50,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
val genesis = Block.RegtestGenesisBlock.header
// initial headers that we will sync when we connect to our mock server
val headers = makeHeaders(genesis, 2016 + 2000)
var headers = makeHeaders(genesis, 2016 + 2000)
val client = TestProbe()
client.ignoreMsg {
@ -65,27 +70,22 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
}
})
val wallet = TestFSMRef(new ElectrumWallet(seed, client.ref, WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = Satoshi(5000))))
val walletParameters = WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = Satoshi(5000))
val wallet = TestFSMRef(new ElectrumWallet(seed, client.ref, walletParameters))
// wallet sends a receive address notification as soon as it is created
listener.expectMsgType[NewWalletReceiveAddress]
def makeHeader(previousHeader: BlockHeader, timestamp: Long): BlockHeader = {
var template = previousHeader.copy(hashPreviousBlock = previousHeader.hash, time = timestamp, nonce = 0)
while (!BlockHeader.checkProofOfWork(template)) {
template = template.copy(nonce = template.nonce + 1)
def reconnect: WalletReady = {
sender.send(wallet, ElectrumClient.ElectrumReady(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP)
while (listener.msgAvailable) {
listener.receiveOne(100 milliseconds)
}
template
}
def makeHeader(previousHeader: BlockHeader): BlockHeader = makeHeader(previousHeader, previousHeader.time + 1)
def makeHeaders(previousHeader: BlockHeader, count: Int): Vector[BlockHeader] = {
@tailrec
def loop(acc: Vector[BlockHeader]): Vector[BlockHeader] = if (acc.length == count) acc else loop(acc :+ makeHeader(acc.last))
loop(Vector(makeHeader(previousHeader)))
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header))
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
val ready = listener.expectMsgType[WalletReady]
ready
}
test("wait until wallet is ready") {
@ -102,7 +102,9 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
test("tell wallet is ready when a new block comes in, even if nothing else has changed") {
val last = wallet.stateData.blockchain.tip
assert(last.header == headers.last)
val header = makeHeader(last.header)
headers = headers :+ header
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header))
assert(listener.expectMsgType[WalletReady].timestamp == header.time)
val NewWalletReceiveAddress(address) = listener.expectMsgType[NewWalletReceiveAddress]
@ -116,6 +118,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
// reconnect wallet
val last = wallet.stateData.blockchain.tip
assert(last.header == headers.last)
sender.send(wallet, ElectrumClient.ElectrumReady(2016, headers(2015), InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height, last.header))
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
@ -128,7 +131,9 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
test("don't send the same ready message more then once") {
// listener should be notified
val last = wallet.stateData.blockchain.tip
assert(last.header == headers.last)
val header = makeHeader(last.header)
headers = headers :+ header
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header))
assert(listener.expectMsgType[WalletReady].timestamp == header.time)
listener.expectMsgType[NewWalletReceiveAddress]
@ -158,10 +163,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
watcher.expectTerminated(probe.ref)
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
sender.send(wallet, ElectrumClient.ElectrumReady(last.height, last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP)
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height, last.header))
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
reconnect
}
@ -206,31 +208,10 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
watcher.expectTerminated(probe.ref)
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
sender.send(wallet, ElectrumClient.ElectrumReady(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP)
while (listener.msgAvailable) {
listener.receiveOne(100 milliseconds)
}
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header))
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
val ready = listener.expectMsgType[WalletReady]
val ready = reconnect
assert(ready.unconfirmedBalance == Satoshi(0))
}
test("disconnect if server sent a block with an invalid difficulty target") {
val last = wallet.stateData.blockchain.bestchain.last
val chunk = makeHeaders(last.header, 2015 - (last.height % 2016))
for (i <- 0 until chunk.length) {
wallet ! HeaderSubscriptionResponse(last.height + i + 1, chunk(i))
}
awaitCond(wallet.stateData.blockchain.tip.header == chunk.last)
val bad = {
var template = makeHeader(chunk.last)
template
}
wallet ! HeaderSubscriptionResponse(wallet.stateData.blockchain.tip.height + 1, bad)
}
test("clear status when we have pending history requests") {
while (client.msgAvailable) {
client.receiveOne(100 milliseconds)
@ -245,5 +226,187 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
wallet ! ElectrumDisconnected
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
assert(wallet.stateData.status.get(scriptHash).isEmpty)
reconnect
}
test("handle pending transaction requests") {
while (client.msgAvailable) {
client.receiveOne(100 milliseconds)
}
val key = wallet.stateData.accountKeys(1)
val scriptHash = computeScriptHashFromPublicKey(key.publicKey)
wallet ! ScriptHashSubscriptionResponse(scriptHash, ByteVector32(ByteVector.fill(32)(2)).toHex)
client.expectMsg(GetScriptHashHistory(scriptHash))
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(100000), ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0)
wallet ! GetScriptHashHistoryResponse(scriptHash, TransactionHistoryItem(2, tx.txid) :: Nil)
// wallet will generate a new address and the corresponding subscription
client.expectMsgType[ScriptHashSubscription]
while (listener.msgAvailable) {
listener.receiveOne(100 milliseconds)
}
client.expectMsg(GetTransaction(tx.txid))
assert(wallet.stateData.pendingTransactionRequests == Set(tx.txid))
}
test("handle disconnect/reconnect events") {
val data = {
val master = DeterministicWallet.generate(seed)
val accountMaster = ElectrumWallet.accountKey(master, walletParameters.chainHash)
val changeMaster = ElectrumWallet.changeKey(master, walletParameters.chainHash)
val firstAccountKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
val firstChangeKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
val data1 = Data(walletParameters, Blockchain.fromGenesisBlock(Block.RegtestGenesisBlock.hash, Block.RegtestGenesisBlock.header), firstAccountKeys, firstChangeKeys)
val amount1 = Satoshi(1000000)
val amount2 = Satoshi(1500000)
// transactions that send funds to our wallet
val wallettxs = Seq(
addOutputs(emptyTx, amount1, data1.accountKeys(0).publicKey),
addOutputs(emptyTx, amount2, data1.accountKeys(1).publicKey),
addOutputs(emptyTx, amount2, data1.accountKeys(2).publicKey),
addOutputs(emptyTx, amount2, data1.accountKeys(3).publicKey)
)
val data2 = wallettxs.foldLeft(data1)(addTransaction)
// a tx that spend from our wallet to our wallet, plus change to our wallet
val tx1 = {
val tx = Transaction(version = 2,
txIn = TxIn(OutPoint(wallettxs(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = walletOutput(wallettxs(0).txOut(0).amount - Satoshi(50000), data2.accountKeys(2).publicKey) :: walletOutput(Satoshi(50000), data2.changeKeys(0).publicKey) :: Nil,
lockTime = 0)
data2.signTransaction(tx)
}
// a tx that spend from our wallet to a random address, plus change to our wallet
val tx2 = {
val tx = Transaction(version = 2,
txIn = TxIn(OutPoint(wallettxs(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(wallettxs(1).txOut(0).amount - Satoshi(50000), Script.pay2wpkh(fr.acinq.eclair.randomKey.publicKey)) :: walletOutput(Satoshi(50000), data2.changeKeys(1).publicKey) :: Nil,
lockTime = 0)
data2.signTransaction(tx)
}
val data3 = Seq(tx1, tx2).foldLeft(data2)(addTransaction)
data3
}
// simulated electrum server that disconnects after a given number of messages
var counter = 0
var disconnectAfter = 10 // disconnect when counter % disconnectAfter == 0
client.setAutoPilot(new testkit.TestActor.AutoPilot {
override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = {
counter = msg match {
case _:ScriptHashSubscription => counter
case _ => counter + 1
}
msg match {
case ScriptHashSubscription(scriptHash, replyTo) =>
// we skip over these otherwise we would never converge (there are at least 20 such messages sent when we're
// reconnected, one for each account/change key)
replyTo ! ScriptHashSubscriptionResponse(scriptHash, data.status.getOrElse(scriptHash, ""))
TestActor.KeepRunning
case msg if counter % disconnectAfter == 0 =>
// disconnect
sender ! ElectrumClient.ElectrumDisconnected
// and reconnect
sender ! ElectrumClient.ElectrumReady(headers.length, headers.last, InetSocketAddress.createUnresolved("0.0.0.0", 9735))
sender ! ElectrumClient.HeaderSubscriptionResponse(headers.length, headers.last)
TestActor.KeepRunning
case request@GetTransaction(txid) =>
data.transactions.get(txid) match {
case Some(tx) => sender ! GetTransactionResponse(tx)
case None =>
sender ! ServerError(request, Error(0, s"unknwown tx $txid"))
}
TestActor.KeepRunning
case GetScriptHashHistory(scriptHash) =>
sender ! GetScriptHashHistoryResponse(scriptHash, data.history.getOrElse(scriptHash, List()))
TestActor.KeepRunning
case GetHeaders(start, count, _) =>
sender ! GetHeadersResponse(start, headers.drop(start - 1).take(count), 2016)
TestActor.KeepRunning
case HeaderSubscription(actor) => actor ! HeaderSubscriptionResponse(headers.length, headers.last)
TestActor.KeepRunning
case _ =>
TestActor.KeepRunning
}
}
})
val sender = TestProbe()
wallet ! ElectrumClient.ElectrumDisconnected
wallet ! ElectrumClient.ElectrumReady(headers.length, headers.last, InetSocketAddress.createUnresolved("0.0.0.0", 9735))
wallet ! ElectrumClient.HeaderSubscriptionResponse(headers.length, headers.last)
data.status.foreach { case (scriptHash, status) => sender.send(wallet, ScriptHashSubscriptionResponse(scriptHash, status)) }
val expected = data.transactions.keySet
awaitCond {
wallet.stateData.transactions.keySet == expected
}
}
}
object ElectrumWalletSimulatedClientSpec {
def makeHeader(previousHeader: BlockHeader, timestamp: Long): BlockHeader = {
var template = previousHeader.copy(hashPreviousBlock = previousHeader.hash, time = timestamp, nonce = 0)
while (!BlockHeader.checkProofOfWork(template)) {
template = template.copy(nonce = template.nonce + 1)
}
template
}
def makeHeader(previousHeader: BlockHeader): BlockHeader = makeHeader(previousHeader, previousHeader.time + 1)
def makeHeaders(previousHeader: BlockHeader, count: Int): Vector[BlockHeader] = {
@tailrec
def loop(acc: Vector[BlockHeader]): Vector[BlockHeader] = if (acc.length == count) acc else loop(acc :+ makeHeader(acc.last))
loop(Vector(makeHeader(previousHeader)))
}
val emptyTx = Transaction(version = 2, txIn = Nil, txOut = Nil, lockTime = 0)
def walletOutput(amount: Satoshi, key: PublicKey) = TxOut(amount, ElectrumWallet.computePublicKeyScript(key))
def addOutputs(tx: Transaction, amount: Satoshi, keys: PublicKey*): Transaction = keys.foldLeft(tx) { case (t, k) => t.copy(txOut = t.txOut :+ walletOutput(amount, k)) }
def addToHistory(history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], scriptHash: ByteVector32, item : TransactionHistoryItem): Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]] = {
history.get(scriptHash) match {
case None => history + (scriptHash -> List(item))
case Some(items) if items.contains(item) => history
case _ => history.updated(scriptHash, history(scriptHash) :+ item)
}
}
def updateStatus(data: ElectrumWallet.Data) : ElectrumWallet.Data = {
val status1 = data.history.mapValues(items => {
val status = items.map(i => s"${i.tx_hash}:${i.height}:").mkString("")
Crypto.sha256(ByteVector.view(status.getBytes())).toString()
})
data.copy(status = status1)
}
def addTransaction(data: ElectrumWallet.Data, tx: Transaction) : ElectrumWallet.Data = {
data.transactions.get(tx.txid) match {
case Some(_) => data
case None =>
val history1 = tx.txOut.filter(o => data.isMine(o)).foldLeft(data.history) { case (a, b) =>
addToHistory(a, Crypto.sha256(b.publicKeyScript).reverse, TransactionHistoryItem(100000, tx.txid))
}
val data1 = data.copy(history = history1, transactions = data.transactions + (tx.txid -> tx))
val history2 = tx.txIn.filter(i => data1.isMine(i)).foldLeft(data1.history) { case (a, b) =>
addToHistory(a, ElectrumWallet.computeScriptHashFromPublicKey(extractPubKeySpentFrom(b).get), TransactionHistoryItem(100000, tx.txid))
}
val data2 = data1.copy(history = history2)
updateStatus(data2)
}
}
}