1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 10:39:19 +01:00

electrum wallet: export xpub (#586)

This commit is contained in:
Fabrice Drouin 2018-05-04 15:06:27 +02:00 committed by GitHub
parent 7c12a7fecf
commit d3244c8499
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 40 additions and 14 deletions

View File

@ -33,6 +33,8 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: BinaryData)(implicit
override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address) override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)
def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse]
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long) = { override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long) = {
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0)
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match { (wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match {

View File

@ -266,6 +266,11 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
case Event(GetData, data) => stay replying GetDataResponse(data) case Event(GetData, data) => stay replying GetDataResponse(data)
case Event(GetXpub ,_) => {
val (xpub, path) = computeXpub(master, chainHash)
stay replying GetXpubResponse(xpub, path)
}
case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected"))) case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected")))
} }
@ -294,6 +299,9 @@ object ElectrumWallet {
case object GetBalance extends Request case object GetBalance extends Request
case class GetBalanceResponse(confirmed: Satoshi, unconfirmed: Satoshi) extends Response case class GetBalanceResponse(confirmed: Satoshi, unconfirmed: Satoshi) extends Response
case object GetXpub extends Request
case class GetXpubResponse(xpub: String, path: String) extends Response
case object GetCurrentReceiveAddress extends Request case object GetCurrentReceiveAddress extends Request
case class GetCurrentReceiveAddressResponse(address: String) extends Response case class GetCurrentReceiveAddressResponse(address: String) extends Response
@ -369,32 +377,43 @@ object ElectrumWallet {
*/ */
def computeScriptHashFromPublicKey(key: PublicKey): BinaryData = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse def computeScriptHashFromPublicKey(key: PublicKey): BinaryData = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse
def accountPath(chainHash: BinaryData) : List[Long] = chainHash match {
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => hardened(49) :: hardened(1) :: hardened(0) :: Nil
case Block.LivenetGenesisBlock.hash => hardened(49) :: hardened(0) :: hardened(0) :: Nil
}
/** /**
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
* *
* @param master master key * @param master master key
* @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet * @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet
*/ */
def accountKey(master: ExtendedPrivateKey, chainHash: BinaryData) = chainHash match { def accountKey(master: ExtendedPrivateKey, chainHash: BinaryData) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 0L :: Nil)
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 0L :: Nil)
case Block.LivenetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(0) :: hardened(0) :: 0L :: Nil)
}
/**
* Compute the wallet's xpub
* @param master master key
* @param chainHash chain hash
* @return a (xpub, path) tuple where xpub is the encoded account public key, and path is the derivation path for the account key
*/
def computeXpub(master: ExtendedPrivateKey, chainHash: BinaryData) : (String, String) = {
val xpub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, accountPath(chainHash)))
// we use the tpub/xpub prefix instead of upub/ypub because it is more widely understood
val prefix = chainHash match {
case Block.LivenetGenesisBlock.hash => DeterministicWallet.xpub
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.tpub
}
(DeterministicWallet.encode(xpub, prefix), xpub.path.toString())
}
/** /**
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
* *
* @param master master key * @param master master key
* @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet * @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet
*/ */
def changeKey(master: ExtendedPrivateKey, chainHash: BinaryData) = chainHash match { def changeKey(master: ExtendedPrivateKey, chainHash: BinaryData) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 1L :: Nil)
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 1L :: Nil)
case Block.LivenetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(0) :: hardened(0) :: 1L :: Nil)
}
def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum) def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum)

View File

@ -20,7 +20,7 @@ import akka.actor.{Actor, ActorSystem, Props}
import akka.testkit.{TestFSMRef, TestKit, TestProbe} import akka.testkit.{TestFSMRef, TestKit, TestProbe}
import fr.acinq.bitcoin.{BinaryData, Block, MnemonicCode, Satoshi} import fr.acinq.bitcoin.{BinaryData, Block, MnemonicCode, Satoshi}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ScriptHashSubscription, ScriptHashSubscriptionResponse} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ScriptHashSubscription, ScriptHashSubscriptionResponse}
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.{NewWalletReceiveAddress, WalletEvent, WalletParameters, WalletReady} import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.scalatest.FunSuiteLike import org.scalatest.FunSuiteLike
import org.scalatest.junit.JUnitRunner import org.scalatest.junit.JUnitRunner
@ -63,12 +63,17 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
awaitCond(wallet.stateName == ElectrumWallet.RUNNING) awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
assert(listener.expectMsgType[WalletReady].timestamp == header1.timestamp) assert(listener.expectMsgType[WalletReady].timestamp == header1.timestamp)
listener.expectMsgType[NewWalletReceiveAddress] listener.expectMsgType[NewWalletReceiveAddress]
listener.send(wallet, GetXpub)
val GetXpubResponse(xpub, path) = listener.expectMsgType[GetXpubResponse]
assert(xpub == "tpubDCY62b4okoTERzMurvrtoCMgkswfLufejmhwfShqAKDBN2PPNUWpwx72cvyt4R8enGstorHvXNGS8StbkAsPb7XSbYFER8Wo6zPf1Z6m9w4")
assert(path == "m/49'/1'/0'")
} }
test("tell wallet is ready when a new block comes in, even if nothing else has changed") { test("tell wallet is ready when a new block comes in, even if nothing else has changed") {
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(header2)) sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(header2))
assert(listener.expectMsgType[WalletReady].timestamp == header2.timestamp) assert(listener.expectMsgType[WalletReady].timestamp == header2.timestamp)
listener.expectMsgType[NewWalletReceiveAddress] val NewWalletReceiveAddress(address) = listener.expectMsgType[NewWalletReceiveAddress]
assert(address == "2NDjBqJugL3gCtjWTToDgaWWogq9nYuYw31")
} }
test("tell wallet is ready when it is reconnected, even if nothing has changed") { test("tell wallet is ready when it is reconnected, even if nothing has changed") {