mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 22:36:34 +01:00
Merge pull request #516 from torkelrogstad/2019-06-12-trezor-test
Generate test vectors from Trezor
This commit is contained in:
commit
09caeb4854
6 changed files with 887 additions and 8 deletions
|
@ -82,6 +82,20 @@ object JsonSerializers {
|
|||
TransactionInputWrites
|
||||
implicit val uInt32Writes: Writes[UInt32] = UInt32Writes
|
||||
implicit val transactionWrites: Writes[Transaction] = TransactionWrites
|
||||
implicit val xpubFormat: Format[ExtPublicKey] = new Format[ExtPublicKey] {
|
||||
override def reads(json: JsValue): JsResult[ExtPublicKey] =
|
||||
SerializerUtil.processJsStringOpt(ExtPublicKey.fromString(_).toOption)(
|
||||
json)
|
||||
|
||||
override def writes(key: ExtPublicKey): JsValue = JsString(key.toString)
|
||||
}
|
||||
|
||||
implicit val xprivForamt: Format[ExtPrivateKey] = new Format[ExtPrivateKey] {
|
||||
override def reads(json: JsValue): JsResult[ExtPrivateKey] =
|
||||
SerializerUtil.processJsStringOpt(ExtPrivateKey.fromString(_).toOption)(
|
||||
json)
|
||||
override def writes(key: ExtPrivateKey): JsValue = JsString(key.toString)
|
||||
}
|
||||
|
||||
// Transaction Models
|
||||
implicit val rpcScriptPubKeyReads: Reads[RpcScriptPubKey] =
|
||||
|
|
399
wallet-test/src/test/resources/trezor-addresses.json
Normal file
399
wallet-test/src/test/resources/trezor-addresses.json
Normal file
|
@ -0,0 +1,399 @@
|
|||
// the content here was generated from this mnemonic: stage boring net gather radar radio arrest eye ask risk girl country
|
||||
[
|
||||
{
|
||||
"xpub": "xpub6D36zpm3tLPy3dBCpiScEpmmgsivFBcHxX5oXmPBW982BmLiEkjBEDdswxFUoeXpp272QuSpNKZ3f2TdEMkAHyCz1M7P3gFkYJJVEsM66SE",
|
||||
"coin": "Bitcoin",
|
||||
"account": 0,
|
||||
"pathType": "legacy",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/44'/0'/0'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "1G1amKQmmsYT7GjRZS8ATBoYFuYQr3f3Kh"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/0'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "1CDaNirLJWHxxT2NT2JDhwYQHVMhQ8gSht"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/0'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "1E33jxV1xT5AQf19kQJJtFGb6roJyqBpgB"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/0'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "1ifTATKPcALmrpkFnJYDMBB7ebmqKNFU3"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/0'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "1A6Dd4XR39r2xcoGahrmGz5uMCQ17QzRCc"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/0'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "1GdP2HfBsrgYv9vrQyjZdvSPSpAxevVc9L"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "xpub6D36zpm3tLPy6iLv781yHuCGs26Lqkf6pUfK9HZbJXZ8y3iGNodq1ULjdR6qxgsbooxmogteLntjx4AFZPogLwJ7bv3yYAebdx599CByRCz",
|
||||
"coin": "Bitcoin",
|
||||
"account": 1,
|
||||
"pathType": "legacy",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/44'/0'/1'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "1GJU8neesqmnqVmeumD437V6mA5UCwQnDi"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/1'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "1G571d8djxUU1pV1zDJVwZX346GsQxrWvY"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/1'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "1LMcqjZMaVUDgYfssKTt1S3bECw8sUiyUH"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/1'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "1HxSpE3SqafLJJyFB287dQZeggPnyS4qZn"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/1'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "18WepEHrUPXBJcqppM4xcX3yv67BJBV34W"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/1'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "1As8dgw5BuNMUuSXEgQDx3CdGv45mKX3fB"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "xpub6D36zpm3tLPy88RHj2VLg13qTejH9QAMcsRNuJBXx5fsvAVWJFma6HVT4QR8QCUH6TKqa45KwqRRRR7aSCwWd7t53nPz1GNqBg4KxKTM5po",
|
||||
"coin": "Bitcoin",
|
||||
"account": 2,
|
||||
"pathType": "legacy",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/44'/0'/2'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "1Di2FwGqvR4hi4RaonDNLLokDV32LZn7QE"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/2'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "18FWkjsm1FDwFJFtJCLiM7c6fioTPL1DhP"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/2'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "1DhbEDmu61Noa7k2nNVudDAM2JLtbXthc7"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/2'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "1FJvUondBDde5r5Dr9E6aMUPNHFhrETBGs"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/2'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "1Ew26qaDcoBbsFTYnZghB44B4LydmM6EAs"
|
||||
},
|
||||
{
|
||||
"path": "m/44'/0'/2'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "16dRQGjd8bBVrXDYUyKFYWv6j1hSRK3jw"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "zpub6qXZXyMKeDrNXGGaMFqy6VJghvXY4MVyxdoAxja7MBywVLwcvMJa5EUN28sXDn8RmshqMT7DQhzZZ2ituUnPz2bi1ZL2BFb7LzgQPqaHhSP",
|
||||
"coin": "Bitcoin",
|
||||
"account": 0,
|
||||
"pathType": "segwit",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/84'/0'/0'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "bc1q2fy4xu2sthp0rn05ru72suwypjeus6ckqha5nn"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/0'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "bc1qckswz80t5y46eduanr3t3t75k7fxg73vj4u44u"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/0'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "bc1qr2v342xx7nfuqkux68z8w38cwhp0dz6kdz6707"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/0'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "bc1qeh4w6gcalmkhr6kwukl204dge9y6rx7eescnym"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/0'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "bc1qz4hc79d2nmhs7evmuxg7lagt4qndpxkg4zhfn5"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/0'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "bc1qwnnplpjgec7yjjh4ckfpm37y29n93edkzumh3x"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "zpub6qXZXyMKeDrNYd8T9kN7tx27MD5LjLWf8jz9cpAYDuNY84KriyeyKF42nMsFxmPz5VwxZZks8nKNyTsLYtPJWEqYvRpskbCQgS2NSxhzQas",
|
||||
"coin": "Bitcoin",
|
||||
"account": 1,
|
||||
"pathType": "segwit",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/84'/0'/1'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "bc1qglsnna835p65y0rw77nytgjeq60lfp8tpq87ax"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/1'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "bc1qe4mln2pde6afge9ku3t4rehqd6aa3sasrmmumx"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/1'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "bc1q30k83vrg6v2kkkywa7h5ug5d8lyuh6ps2hmkvd"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/1'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "bc1qp4s3wefys5gv9zfk3kqyc2cln8wdw493g54mds"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/1'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "bc1qdjjfpe2x3d396tdyhuktkjmnd08fznfzswy6vj"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/1'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "bc1qsd2uwgfkf3sa00mz4s9ye8lenc0d3wr6sy334j"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "zpub6qXZXyMKeDrNbhzS9E7rFzgACFHYbrCRt4H7nQfHVxYcPY7Ycpv7aAqAMchJC9B2BU6MJoo24t5j5tE9pC36YyVXX9Yab997P7LqnMyouHz",
|
||||
"coin": "Bitcoin",
|
||||
"account": 2,
|
||||
"pathType": "segwit",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/84'/0'/2'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "bc1qa924lwqvd8grfskq50m0pz8n38e8a6j396ju79"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/2'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "bc1qkfh4s4la84j37kwrzfhzzk7ssmhmrvu4m8jz0y"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/2'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "bc1qhs2r9n5x0007ja9zy26rer6dx23uypp6gfzzgk"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/2'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "bc1q667d6n68q865afg7xa7arft4fn8arnp05wdhq5"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/2'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "bc1q6ml85j0cfmzdt75h9fevjdj0afuu8398cmm0de"
|
||||
},
|
||||
{
|
||||
"path": "m/84'/0'/2'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "bc1qa9vytfte4wqj924p3rv8tnxa3mpzxftdftcasw"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "ypub6XXo8YrYWEi6GvfDaXzjzooA7mGdpjAd4bFXrGoixH42GBkCNQUspsRzbL7EfrBokgp76ixB8zv61rd1S9XwU1AtSyy6Nsfo2VS1KLPHgHy",
|
||||
"coin": "Bitcoin",
|
||||
"account": 0,
|
||||
"pathType": "p2sh-segwit",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/49'/0'/0'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "3D5QZsM6ChmANAZiqhYDinMd15eSBcHdUy"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/0'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "3FPUdLjhSiPQ6cvsBasmXPppYkp7mMJCKj"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/0'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "3HisVck33jvwdidJRwictPWFmpQxYtXCEW"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/0'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "3DRtzN2DG7NQVd16XHeiAtdwxissYnZoiM"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/0'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "33wUvLyenVSj3G1chYj7SQ29YgUaTTXGXv"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/0'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "3DDrvx2FGrnJo4AfTmjWp69bLYs8b42Geq"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "ypub6XXo8YrYWEi6LJHpM8SvfQ5JPXum2grFp9WNTZP6isDihG9apqXE3TtrFKv29D8WzzuMcyRFSizRRKGPrDRJtrVDq9Qjxz5RhGnqsjJrSv6",
|
||||
"coin": "Bitcoin",
|
||||
"account": 1,
|
||||
"pathType": "p2sh-segwit",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/49'/0'/1'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "3NBCdmG7M9ygPjMVu4SEVvjS84mkXVJrB1"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/1'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "34igwpwzCGioP5w3XpbQu371Pcnn1buyMy"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/1'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "332sSJULbyUxS3iAP8T3HaD4weBvvcBdqX"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/1'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "39nAgGFEkrmjfZvXePTj2DPtvd72d29oXz"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/1'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "3JwL1TpeukwGFTtxYGhzwhRsARdCoV8mVm"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/1'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "32CUGYBfMZ9fmsHzpTADQUuU6mJeKzrezm"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"xpub": "ypub6XXo8YrYWEi6P35ZKSLvfrfPRq9J7QC77TTJWZA2JdgPen9Srt9gAzWRtWGzLTPrKy1fd9fDKref5dorMUiXzaJCrwCVBDYkjRMLevE8ym6",
|
||||
"coin": "Bitcoin",
|
||||
"account": 2,
|
||||
"pathType": "p2sh-segwit",
|
||||
"addresses": [
|
||||
{
|
||||
"path": "m/49'/0'/2'/1/0",
|
||||
"chain": "Change",
|
||||
"addressIndex": 0,
|
||||
"address": "3233QX7LkdHwVcAbL9DtcLX6aWUoBF6ucw"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/2'/1/1",
|
||||
"chain": "Change",
|
||||
"addressIndex": 1,
|
||||
"address": "3BgoEtBf2j7QtiHsRYwfHVxEHE85f2Gjw5"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/2'/1/2",
|
||||
"chain": "Change",
|
||||
"addressIndex": 2,
|
||||
"address": "3HgnuyHiqjLsBtwTpFXX7rgZRHDuE7qmS1"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/2'/0/0",
|
||||
"chain": "External",
|
||||
"addressIndex": 0,
|
||||
"address": "3HBcdc89nCTJiqbj48nY8eGmNqbjc6GJbT"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/2'/0/1",
|
||||
"chain": "External",
|
||||
"addressIndex": 1,
|
||||
"address": "3JbAdrcBjxPmzevSVCHJpx9Joaqr1t2KAn"
|
||||
},
|
||||
{
|
||||
"path": "m/49'/0'/2'/0/2",
|
||||
"chain": "External",
|
||||
"addressIndex": 2,
|
||||
"address": "32jnXCPvXXMfmKtruBBD48CnQLQfVbfK6U"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,306 @@
|
|||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import org.bitcoins.core.crypto.MnemonicCode
|
||||
import scala.io.Source
|
||||
import play.api.libs.json.JsValue
|
||||
import play.api.libs.json.Json
|
||||
import org.bitcoins.core.crypto.ExtPublicKey
|
||||
import org.bitcoins.core.hd.HDCoinType
|
||||
import org.bitcoins.core.hd.HDPurpose
|
||||
import org.bitcoins.core.hd.HDPath
|
||||
import org.bitcoins.core.hd.HDChain
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.rpc.serializers.JsonSerializers._
|
||||
import play.api.libs.json.Reads
|
||||
import play.api.libs.json.JsResult
|
||||
import org.bitcoins.rpc.serializers.SerializerUtil
|
||||
import play.api.libs.json.JsError
|
||||
import play.api.libs.json.JsSuccess
|
||||
import org.bitcoins.core.hd.HDCoin
|
||||
import org.bitcoins.core.hd.HDChainType
|
||||
import org.bitcoins.core.hd.HDPurposes
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
import org.bitcoins.testkit.BitcoinSAppConfig
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import akka.actor.ActorSystem
|
||||
import scala.concurrent.Future
|
||||
import org.bitcoins.wallet.api.InitializeWalletSuccess
|
||||
import org.scalatest.AsyncFlatSpec
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.scalatest.FutureOutcome
|
||||
import org.bitcoins.testkit.fixtures.EmptyFixture
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.hd.HDChainType.Change
|
||||
import org.bitcoins.core.hd.HDChainType.External
|
||||
import org.bitcoins.wallet.models.AddressDb
|
||||
import org.bitcoins.wallet.models.AccountDb
|
||||
import _root_.akka.actor.Address
|
||||
import org.scalatest.compatible.Assertion
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
|
||||
|
||||
val mnemonic = MnemonicCode.fromWords(
|
||||
Vector(
|
||||
"stage",
|
||||
"boring",
|
||||
"net",
|
||||
"gather",
|
||||
"radar",
|
||||
"radio",
|
||||
"arrest",
|
||||
"eye",
|
||||
"ask",
|
||||
"risk",
|
||||
"girl",
|
||||
"country"
|
||||
)
|
||||
)
|
||||
|
||||
lazy val json: JsValue = {
|
||||
val stream = {
|
||||
val classLoader = getClass().getClassLoader()
|
||||
classLoader.getResourceAsStream("trezor-addresses.json")
|
||||
}
|
||||
val rawText = Source
|
||||
.fromInputStream(stream)
|
||||
.getLines
|
||||
.drop(1) // first line is a comment
|
||||
.mkString
|
||||
Json.parse(rawText)
|
||||
}
|
||||
|
||||
implicit val hdpathReads = new Reads[HDPath] {
|
||||
override def reads(json: JsValue): JsResult[HDPath] =
|
||||
json
|
||||
.validate[String]
|
||||
.flatMap(HDPath.fromString(_) match {
|
||||
case None => JsError(s"Could not read $json")
|
||||
case Some(value) => JsSuccess(value)
|
||||
})
|
||||
}
|
||||
|
||||
implicit val hdcoinReads = new Reads[HDCoinType] {
|
||||
override def reads(json: JsValue): JsResult[HDCoinType] =
|
||||
json.validate[String].map(_.toLowerCase).map {
|
||||
case "bitcoin" => HDCoinType.Bitcoin
|
||||
case "testnet" => HDCoinType.Testnet
|
||||
}
|
||||
}
|
||||
|
||||
implicit val hdpurposeReads = new Reads[HDPurpose] {
|
||||
override def reads(json: JsValue): JsResult[HDPurpose] =
|
||||
json.validate[String].map {
|
||||
case "legacy" => HDPurposes.Legacy
|
||||
case "segwit" => HDPurposes.SegWit
|
||||
case "p2sh-segwit" => HDPurposes.NestedSegWit
|
||||
}
|
||||
}
|
||||
|
||||
implicit val hdchainType = new Reads[HDChainType] {
|
||||
override def reads(json: JsValue): JsResult[HDChainType] =
|
||||
json.validate[String].map(_.toLowerCase).map {
|
||||
case "change" => HDChainType.Change
|
||||
case "external" => HDChainType.External
|
||||
}
|
||||
}
|
||||
|
||||
private case class TestAddress(
|
||||
path: HDPath,
|
||||
chain: HDChainType,
|
||||
addressIndex: Int,
|
||||
address: BitcoinAddress)
|
||||
|
||||
private object TestAddress {
|
||||
implicit val reads = Json.reads[TestAddress]
|
||||
}
|
||||
|
||||
private case class TestVector(
|
||||
xpub: ExtPublicKey,
|
||||
coin: HDCoinType,
|
||||
account: Int,
|
||||
pathType: HDPurpose,
|
||||
addresses: Vector[TestAddress])
|
||||
|
||||
private object TestVector {
|
||||
implicit val reads = Json.reads[TestVector]
|
||||
}
|
||||
|
||||
private lazy val vectors = json.validate[Vector[TestVector]] match {
|
||||
case JsError(errors) => fail(errors.head.toString)
|
||||
case JsSuccess(value, _) => value
|
||||
}
|
||||
|
||||
private lazy val legacyVectors =
|
||||
vectors.filter(_.pathType == HDPurposes.Legacy)
|
||||
|
||||
private lazy val segwitVectors =
|
||||
vectors.filter(_.pathType == HDPurposes.SegWit)
|
||||
|
||||
private lazy val nestedVectors =
|
||||
vectors.filter(_.pathType == HDPurposes.NestedSegWit)
|
||||
|
||||
private def configForPurpose(purpose: HDPurpose): Config = {
|
||||
val purposeStr = purpose match {
|
||||
case HDPurposes.Legacy => "legacy"
|
||||
case HDPurposes.SegWit => "segwit"
|
||||
case HDPurposes.NestedSegWit => "nested-segwit"
|
||||
case other => fail(s"unexpected purpose: $other")
|
||||
}
|
||||
val confStr = s"""bitcoin-s.wallet.defaultAccountType = $purposeStr
|
||||
|bitcoin-s.network = mainnet""".stripMargin
|
||||
ConfigFactory.parseString(confStr)
|
||||
}
|
||||
|
||||
private def getWallet(config: WalletAppConfig): Future[Wallet] =
|
||||
Wallet
|
||||
.initializeWithMnemonic(mnemonic)(
|
||||
config, // to make sure we're not passing in the wrong conf by accident
|
||||
implicitly[ExecutionContext])
|
||||
.map {
|
||||
case InitializeWalletSuccess(wallet: Wallet) =>
|
||||
wallet
|
||||
case err => fail(s"didn't get wallet: $err")
|
||||
}
|
||||
|
||||
private case class AccountAndAddrsAndVector(
|
||||
account: AccountDb,
|
||||
addrs: Seq[AddressDb],
|
||||
vector: TestVector)
|
||||
|
||||
/** Asserts that the given addresses are gthe same as in the given vector */
|
||||
private def assertSameAddresses(
|
||||
addrs: Seq[AddressDb],
|
||||
vector: TestVector): Seq[Assertion] = {
|
||||
assert(vector.addresses.length == addrs.length)
|
||||
|
||||
val sortedAddresses = addrs.sortBy(_.path.toString)
|
||||
val sortedVectors = vector.addresses.sortBy(_.path.toString)
|
||||
sortedAddresses
|
||||
.zip(sortedVectors)
|
||||
.map {
|
||||
case (foundAddress, expectedAddress) =>
|
||||
assert(foundAddress.address == expectedAddress.address)
|
||||
}
|
||||
}
|
||||
|
||||
private def testAccountType(purpose: HDPurpose): Future[Assertion] = {
|
||||
val confOverride = configForPurpose(purpose)
|
||||
implicit val conf: WalletAppConfig =
|
||||
BitcoinSAppConfig.getTestConfig(confOverride)
|
||||
|
||||
val vectors = purpose match {
|
||||
case HDPurposes.Legacy => legacyVectors
|
||||
case HDPurposes.SegWit => segwitVectors
|
||||
case HDPurposes.NestedSegWit => nestedVectors
|
||||
case other => fail(s"unknown purpose: $other")
|
||||
}
|
||||
|
||||
/** Creates the wallet accounts needed for this test */
|
||||
def createNeededAccounts(
|
||||
wallet: Wallet,
|
||||
existing: Vector[AccountDb]): Future[Unit] = {
|
||||
val accountsToCreate = existing.length until vectors.length
|
||||
|
||||
FutureUtil
|
||||
.sequentially(accountsToCreate) { _ =>
|
||||
wallet.createNewAccount(purpose)
|
||||
}
|
||||
.map(_ => ())
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the given list of accounts and test vectors, and
|
||||
* fetches all the
|
||||
* addresses needed to verify the test vector
|
||||
*/
|
||||
def getAcccountsWithAddressesAndVectors(
|
||||
wallet: Wallet,
|
||||
accountsWithVectors: Seq[(AccountDb, TestVector)]): Future[
|
||||
Seq[AccountAndAddrsAndVector]] =
|
||||
FutureUtil.sequentially(accountsWithVectors) {
|
||||
case (acc, vec) =>
|
||||
val addrFutures: Future[Seq[AddressDb]] =
|
||||
FutureUtil.sequentially(vec.addresses) { vector =>
|
||||
val addrFut = vector.chain match {
|
||||
case Change => wallet.getNewChangeAddress(acc)
|
||||
case External =>
|
||||
wallet.getNewAddress(acc)
|
||||
}
|
||||
addrFut.flatMap(wallet.addressDAO.findAddress).map {
|
||||
case Some(addr) => addr
|
||||
case None =>
|
||||
fail(s"Did not find address we just generated in DAO!")
|
||||
}
|
||||
}
|
||||
addrFutures.map(AccountAndAddrsAndVector(acc, _, vec))
|
||||
}
|
||||
|
||||
for {
|
||||
wallet <- getWallet(conf)
|
||||
existingAccounts <- wallet.listAccounts(purpose)
|
||||
_ <- createNeededAccounts(wallet, existingAccounts)
|
||||
accounts <- wallet.listAccounts(purpose)
|
||||
|
||||
// we want to find all accounts for the given account type,
|
||||
// and match it with its corresponding test vector
|
||||
accountsWithVectors = {
|
||||
assert(accounts.length == vectors.length)
|
||||
val accountsWithVectors = vectors.map { vec =>
|
||||
accounts.find(_.hdAccount.index == vec.account) match {
|
||||
case None =>
|
||||
fail(
|
||||
s"Did not find account in wallet with index ${vec.account}. Accounts: ${accounts.mkString}")
|
||||
case Some(account) =>
|
||||
assert(account.xpub == vec.xpub)
|
||||
account -> vec
|
||||
}
|
||||
}
|
||||
accountsWithVectors
|
||||
}
|
||||
|
||||
// here we generate addresses matching the ones found
|
||||
// in the accompanying test vector for each account
|
||||
// at the end we group them all together
|
||||
accountsWithAddrsWithVecs <- getAcccountsWithAddressesAndVectors(
|
||||
wallet,
|
||||
accountsWithVectors)
|
||||
} yield {
|
||||
// lastly we loop over all accounts, addresses and vectors
|
||||
// and verify that they are all the same
|
||||
val assertions: Seq[Assertion] = {
|
||||
val nestedAssertions: Seq[Seq[Assertion]] =
|
||||
accountsWithAddrsWithVecs.map {
|
||||
case AccountAndAddrsAndVector(account, addresses, vec) =>
|
||||
val acctIdx = account.hdAccount.index
|
||||
val vec = vectors.find(_.xpub == account.xpub) match {
|
||||
case None =>
|
||||
fail(s"Did not find test vector for account $acctIdx")
|
||||
case Some(v) => v
|
||||
}
|
||||
|
||||
assertSameAddresses(addresses, vec)
|
||||
}
|
||||
nestedAssertions.flatten
|
||||
}
|
||||
|
||||
assert(assertions.forall(_.isCompleted))
|
||||
}
|
||||
}
|
||||
|
||||
it must "act the same way as Trezor for legacy accounts" in { _ =>
|
||||
testAccountType(HDPurposes.Legacy)
|
||||
}
|
||||
|
||||
it must "act the same way as Trezor for segwit accounts" in { _ =>
|
||||
testAccountType(HDPurposes.SegWit)
|
||||
}
|
||||
|
||||
// TODO: implement this when nested segwit addresses are implemented
|
||||
// in the wallet
|
||||
it must "act the same way as Trezor for nested segwit accounts" ignore { _ =>
|
||||
testAccountType(HDPurposes.NestedSegWit)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package org.bitcoins.wallet.util
|
||||
|
||||
import org.bitcoins.core.hd._
|
||||
import play.api.libs.json._
|
||||
import scala.sys.process._
|
||||
|
||||
/** This program connects to a running Trezor, and gets
|
||||
* xpubs and addresses from legacy, segwit and nested
|
||||
* segwit accounts for mainnet. The intention here was
|
||||
* to also do this for testnet, but for some reason
|
||||
* my emulator refuses to work when specifying a
|
||||
* testnet BIP32 path. Something to look into in the
|
||||
* future.
|
||||
*
|
||||
* To replicate this:
|
||||
* 1) Boot up a Trezor emulator (https://github.com/trezor/trezor-firmware/blob/master/core/docs/emulator.md)
|
||||
* 2) Run Trezor bridge (instructions in link above)
|
||||
* 3) Go to trezor.io/start, restore wallet from seed
|
||||
* 4) Leave emulator open
|
||||
* 5) Run this program. Result gets printed to stdout.
|
||||
*/
|
||||
object GetAddresses extends App {
|
||||
|
||||
import scala.language.implicitConversions
|
||||
implicit def string2Json(str: String): JsString = JsString(str)
|
||||
implicit def int2Json(int: Int): JsNumber = JsNumber(int)
|
||||
implicit def seq2Json[T](xs: Seq[T])(implicit conv: T => JsValue): JsArray =
|
||||
JsArray(xs.map(conv))
|
||||
|
||||
def printerr(x: Any): Unit = System.err.println(x.toString())
|
||||
|
||||
val accountInfo = for {
|
||||
constant <- HDPurposes.all
|
||||
coin <- List(HDCoinType.Bitcoin /*, HDCoinType.Testnet*/ )
|
||||
accountIndex <- 0 until 3
|
||||
} yield {
|
||||
val accountPath = BIP32Path(
|
||||
BIP32Node(constant.constant, hardened = true),
|
||||
BIP32Node(coin.toInt, hardened = true),
|
||||
BIP32Node(accountIndex, hardened = true)
|
||||
)
|
||||
|
||||
val pathType =
|
||||
constant match {
|
||||
case HDPurposes.Legacy => "legacy"
|
||||
case HDPurposes.NestedSegWit => "p2sh-segwit"
|
||||
case HDPurposes.SegWit => "segwit"
|
||||
case other => throw new RuntimeException(s"Unexpected purpose $other")
|
||||
}
|
||||
|
||||
val trezorPathType =
|
||||
constant match {
|
||||
case HDPurposes.Legacy => "address"
|
||||
case HDPurposes.NestedSegWit => "p2shsegwit"
|
||||
case HDPurposes.SegWit => "segwit"
|
||||
case other => throw new RuntimeException(s"Unexpected purpose $other")
|
||||
}
|
||||
|
||||
val xpubCmd =
|
||||
s"""trezorctl get-public-node -n $accountPath -t $trezorPathType"""
|
||||
printerr(s"Executing cmd: $xpubCmd")
|
||||
val xpub = xpubCmd.!!.split("\n").last.split(": ").last
|
||||
|
||||
val addresses = for {
|
||||
chainType <- List[HDChainType](HDChainType.Change, HDChainType.External)
|
||||
addressIndex <- 0 until 3
|
||||
} yield {
|
||||
val path = BIP32Path(
|
||||
BIP32Node(constant.constant, hardened = true),
|
||||
BIP32Node(coin.toInt, hardened = true),
|
||||
BIP32Node(accountIndex, hardened = true),
|
||||
BIP32Node(chainType.index, hardened = false),
|
||||
BIP32Node(addressIndex, hardened = false)
|
||||
)
|
||||
|
||||
val addressCmd = s"trezorctl get-address -n $path -t $trezorPathType"
|
||||
printerr(s"Executing cmd: $addressCmd")
|
||||
val address = addressCmd.!!.split("\n").head
|
||||
|
||||
val json = Json.toJson(
|
||||
Map[String, JsValue](
|
||||
"path" -> path.toString,
|
||||
"chain" -> chainType.toString,
|
||||
"addressIndex" -> addressIndex,
|
||||
"address" -> address
|
||||
)
|
||||
)
|
||||
json
|
||||
}
|
||||
|
||||
val json = JsObject(
|
||||
Map[String, JsValue](
|
||||
"coin" -> coin.toString,
|
||||
"pathType" -> pathType,
|
||||
"account" -> accountIndex,
|
||||
"xpub" -> xpub,
|
||||
"addresses" -> addresses
|
||||
)
|
||||
)
|
||||
json
|
||||
}
|
||||
|
||||
println(Json.stringify(JsArray(accountInfo)))
|
||||
}
|
|
@ -98,6 +98,48 @@ sealed abstract class Wallet
|
|||
}
|
||||
}
|
||||
|
||||
override def createNewAccount(): Future[WalletApi] =
|
||||
createNewAccount(DEFAULT_HD_COIN.purpose)
|
||||
|
||||
// todo: check if there's addresses in the most recent
|
||||
// account before creating new
|
||||
override def createNewAccount(purpose: HDPurpose): Future[Wallet] = {
|
||||
|
||||
accountDAO
|
||||
.findAll()
|
||||
.map(_.filter(_.hdAccount.purpose == purpose))
|
||||
.map(_.sortBy(_.hdAccount.index))
|
||||
// we want to the most recently created account,
|
||||
// to know what the index of our new account
|
||||
// should be.
|
||||
.map(_.lastOption)
|
||||
.flatMap { mostRecentOpt =>
|
||||
val accountIndex = mostRecentOpt match {
|
||||
case None => 0 // no accounts present in wallet
|
||||
case Some(account) => account.hdAccount.index + 1
|
||||
}
|
||||
logger.info(
|
||||
s"Creating new account at index $accountIndex for purpose $purpose")
|
||||
val newAccount = HDAccount(DEFAULT_HD_COIN, accountIndex)
|
||||
val xpub = {
|
||||
val xpriv = xprivForPurpose(newAccount.purpose)
|
||||
xpriv.deriveChildPubKey(newAccount) match {
|
||||
case Failure(exception) =>
|
||||
// this won't happen, because we're deriving from a privkey
|
||||
// this should really be changed in the method signature
|
||||
logger.error(s"Unexpected error when deriving xpub: $exception")
|
||||
throw exception
|
||||
case Success(xpub) => xpub
|
||||
}
|
||||
}
|
||||
val newAccountDb = AccountDb(xpub, newAccount)
|
||||
val accountCreationF = accountDAO.create(newAccountDb)
|
||||
accountCreationF.map(created =>
|
||||
logger.debug(s"Created new account ${created.hdAccount}"))
|
||||
accountCreationF
|
||||
}
|
||||
.map(_ => this)
|
||||
}
|
||||
}
|
||||
|
||||
// todo: create multiple wallets, need to maintain multiple databases
|
||||
|
|
|
@ -122,14 +122,9 @@ trait LockedWalletApi extends WalletApi {
|
|||
|
||||
def listAccounts(): Future[Vector[AccountDb]]
|
||||
|
||||
/**
|
||||
* Tries to create a new accoun in this wallet. Fails if the
|
||||
* most recent account has no transaction history, as per
|
||||
* BIP44
|
||||
*
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
|
||||
*/
|
||||
// def createNewAccount: Future[Try[WalletApi]]
|
||||
/** Lists all wallet accounts with the given type */
|
||||
def listAccounts(purpose: HDPurpose): Future[Vector[AccountDb]] =
|
||||
listAccounts().map(_.filter(_.hdAccount.purpose == purpose))
|
||||
|
||||
}
|
||||
|
||||
|
@ -186,4 +181,23 @@ trait UnlockedWalletApi extends LockedWalletApi {
|
|||
} yield tx
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to create a new account in this wallet. Fails if the
|
||||
* most recent account has no transaction history, as per
|
||||
* BIP44
|
||||
*
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
|
||||
*/
|
||||
def createNewAccount(purpose: HDPurpose): Future[WalletApi]
|
||||
|
||||
/**
|
||||
* Tries to create a new account in this wallet for the default
|
||||
* account type. Fails if the
|
||||
* most recent account has no transaction history, as per
|
||||
* BIP44
|
||||
*
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
|
||||
*/
|
||||
def createNewAccount(): Future[WalletApi]
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue