Skip to content

Commit 9aa4add

Browse files
committed
Use Either for error handling
We already export an `Either` type, which can be reused by clients of this library.
1 parent de51b68 commit 9aa4add

File tree

7 files changed

+116
-154
lines changed

7 files changed

+116
-154
lines changed

build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ plugins {
1212
val currentOs = org.gradle.internal.os.OperatingSystem.current()
1313

1414
group = "fr.acinq.bitcoin"
15-
version = "0.17.0-SNAPSHOT"
15+
version = "0.17.0-EITHER-SNAPSHOT"
1616

1717
repositories {
1818
google()

src/commonMain/kotlin/fr/acinq/bitcoin/Bitcoin.kt

+51-99
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package fr.acinq.bitcoin
1818

19+
import fr.acinq.bitcoin.utils.Either
1920
import kotlin.jvm.JvmStatic
2021

2122
public const val MaxBlockSize: Int = 1000000
@@ -26,63 +27,14 @@ public fun <T> List<T>.updated(i: Int, t: T): List<T> = when (i) {
2627
else -> this.take(i) + t + this.drop(i + 1)
2728
}
2829

29-
public sealed class AddressToPublicKeyScriptResult {
30-
31-
public abstract val result: List<ScriptElt>?
32-
33-
public fun isSuccess(): Boolean = result != null
34-
35-
public fun isFailure(): Boolean = !isSuccess()
36-
37-
public data class Success(val script: List<ScriptElt>) : AddressToPublicKeyScriptResult() {
38-
override val result: List<ScriptElt> = script
39-
}
40-
41-
public sealed class Failure : AddressToPublicKeyScriptResult() {
42-
override val result: List<ScriptElt>? = null
43-
44-
public object ChainHashMismatch : Failure() {
45-
override fun toString(): String = "chain hash mismatch"
46-
}
47-
48-
public object InvalidAddress : Failure() {
49-
override fun toString(): String = "invalid base58 or bech32 address "
50-
}
51-
52-
public object InvalidBech32Address : Failure() {
53-
override fun toString(): String = "invalid bech32 address"
54-
}
55-
56-
public data class InvalidWitnessVersion(val version: Int) : Failure() {
57-
override fun toString(): String = "invalid witness version $version"
58-
}
59-
}
60-
}
61-
62-
public sealed class AddressFromPublicKeyScriptResult {
63-
public abstract val result: String?
64-
public fun isSuccess(): Boolean = result != null
65-
public fun isFailure(): Boolean = !isSuccess()
66-
67-
public data class Success(val address: String) : AddressFromPublicKeyScriptResult() {
68-
override val result: String = address
69-
}
70-
71-
public sealed class Failure : AddressFromPublicKeyScriptResult() {
72-
override val result: String? = null
73-
74-
public object InvalidChainHash : Failure() {
75-
override fun toString(): String = "invalid chain hash"
76-
}
77-
78-
public object InvalidScript : Failure() {
79-
override fun toString(): String = "invalid script"
80-
}
81-
82-
public data class GenericError(val t: Throwable) : Failure() {
83-
override fun toString(): String = "generic failure: ${t.message}"
84-
}
85-
}
30+
public sealed class BitcoinException(message: String, cause: Throwable?) : Exception(message, cause) {
31+
public data object InvalidChainHash : BitcoinException("invalid chain hash", null)
32+
public data object ChainHashMismatch : BitcoinException("chain hash mismatch", null)
33+
public data object InvalidScript : BitcoinException("invalid script", null)
34+
public data object InvalidAddress : BitcoinException("invalid address", null)
35+
public data object InvalidBech32Address : BitcoinException("invalid address", null)
36+
public data class InvalidWitnessVersion(val version: Int) : BitcoinException("invalid witness version $version", null)
37+
public data class GenericError(override val message: String, override val cause: Throwable?) : BitcoinException(message, cause)
8638
}
8739

8840
public object Bitcoin {
@@ -121,77 +73,77 @@ public object Bitcoin {
12173
* @param pubkeyScript public key script
12274
*/
12375
@JvmStatic
124-
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: List<ScriptElt>): AddressFromPublicKeyScriptResult {
76+
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: List<ScriptElt>): Either<BitcoinException, String> {
12577
try {
12678
return when {
12779
Script.isPay2pkh(pubkeyScript) -> {
12880
val prefix = when (chainHash) {
12981
Block.LivenetGenesisBlock.hash -> Base58.Prefix.PubkeyAddress
13082
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.PubkeyAddressTestnet
131-
else -> return AddressFromPublicKeyScriptResult.Failure.InvalidChainHash
83+
else -> return Either.Left(BitcoinException.InvalidChainHash)
13284
}
133-
AddressFromPublicKeyScriptResult.Success(Base58Check.encode(prefix, (pubkeyScript[2] as OP_PUSHDATA).data))
85+
Either.Right(Base58Check.encode(prefix, (pubkeyScript[2] as OP_PUSHDATA).data))
13486
}
13587

13688
Script.isPay2sh(pubkeyScript) -> {
13789
val prefix = when (chainHash) {
13890
Block.LivenetGenesisBlock.hash -> Base58.Prefix.ScriptAddress
13991
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.ScriptAddressTestnet
140-
else -> return AddressFromPublicKeyScriptResult.Failure.InvalidChainHash
92+
else -> return Either.Left(BitcoinException.InvalidChainHash)
14193
}
142-
AddressFromPublicKeyScriptResult.Success(Base58Check.encode(prefix, (pubkeyScript[1] as OP_PUSHDATA).data))
94+
Either.Right(Base58Check.encode(prefix, (pubkeyScript[1] as OP_PUSHDATA).data))
14395
}
14496

14597
Script.isNativeWitnessScript(pubkeyScript) -> {
14698
val hrp = Bech32.hrp(chainHash)
14799
val witnessScript = (pubkeyScript[1] as OP_PUSHDATA).data.toByteArray()
148100
when (pubkeyScript[0]) {
149101
is OP_0 -> when {
150-
Script.isPay2wpkh(pubkeyScript) || Script.isPay2wsh(pubkeyScript) -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 0, witnessScript))
151-
else -> AddressFromPublicKeyScriptResult.Failure.InvalidScript
102+
Script.isPay2wpkh(pubkeyScript) || Script.isPay2wsh(pubkeyScript) -> Either.Right(Bech32.encodeWitnessAddress(hrp, 0, witnessScript))
103+
else -> return Either.Left(BitcoinException.InvalidScript)
152104
}
153105

154-
is OP_1 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 1, witnessScript))
155-
is OP_2 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 2, witnessScript))
156-
is OP_3 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 3, witnessScript))
157-
is OP_4 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 4, witnessScript))
158-
is OP_5 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 5, witnessScript))
159-
is OP_6 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 6, witnessScript))
160-
is OP_7 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 7, witnessScript))
161-
is OP_8 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 8, witnessScript))
162-
is OP_9 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 9, witnessScript))
163-
is OP_10 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 10, witnessScript))
164-
is OP_11 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 11, witnessScript))
165-
is OP_12 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 12, witnessScript))
166-
is OP_13 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 13, witnessScript))
167-
is OP_14 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 14, witnessScript))
168-
is OP_15 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 15, witnessScript))
169-
is OP_16 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 16, witnessScript))
170-
else -> AddressFromPublicKeyScriptResult.Failure.InvalidScript
106+
is OP_1 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 1, witnessScript))
107+
is OP_2 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 2, witnessScript))
108+
is OP_3 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 3, witnessScript))
109+
is OP_4 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 4, witnessScript))
110+
is OP_5 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 5, witnessScript))
111+
is OP_6 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 6, witnessScript))
112+
is OP_7 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 7, witnessScript))
113+
is OP_8 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 8, witnessScript))
114+
is OP_9 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 9, witnessScript))
115+
is OP_10 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 10, witnessScript))
116+
is OP_11 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 11, witnessScript))
117+
is OP_12 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 12, witnessScript))
118+
is OP_13 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 13, witnessScript))
119+
is OP_14 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 14, witnessScript))
120+
is OP_15 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 15, witnessScript))
121+
is OP_16 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 16, witnessScript))
122+
else -> return Either.Left(BitcoinException.InvalidScript)
171123
}
172124
}
173125

174-
else -> AddressFromPublicKeyScriptResult.Failure.InvalidScript
126+
else -> return Either.Left(BitcoinException.InvalidScript)
175127
}
176128
} catch (t: Throwable) {
177-
return AddressFromPublicKeyScriptResult.Failure.GenericError(t)
129+
return Either.Left(BitcoinException.GenericError("", t))
178130
}
179131
}
180132

181133
@JvmStatic
182-
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: ByteArray): AddressFromPublicKeyScriptResult {
134+
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: ByteArray): Either<BitcoinException, String> {
183135
return runCatching { Script.parse(pubkeyScript) }.fold(
184136
onSuccess = {
185137
addressFromPublicKeyScript(chainHash, it)
186138
},
187139
onFailure = {
188-
AddressFromPublicKeyScriptResult.Failure.InvalidScript
140+
Either.Left(BitcoinException.InvalidScript)
189141
}
190142
)
191143
}
192144

193145
@JvmStatic
194-
public fun addressToPublicKeyScript(chainHash: BlockHash, address: String): AddressToPublicKeyScriptResult {
146+
public fun addressToPublicKeyScript(chainHash: BlockHash, address: String): Either<BitcoinException, List<ScriptElt>> {
195147
val witnessVersions = mapOf(
196148
0.toByte() to OP_0,
197149
1.toByte() to OP_1,
@@ -216,36 +168,36 @@ public object Bitcoin {
216168
onSuccess = {
217169
when {
218170
it.first == Base58.Prefix.PubkeyAddressTestnet && (chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
219-
AddressToPublicKeyScriptResult.Success(Script.pay2pkh(it.second))
171+
Either.Right(Script.pay2pkh(it.second))
220172

221173
it.first == Base58.Prefix.PubkeyAddress && chainHash == Block.LivenetGenesisBlock.hash ->
222-
AddressToPublicKeyScriptResult.Success(Script.pay2pkh(it.second))
174+
Either.Right(Script.pay2pkh(it.second))
223175

224176
it.first == Base58.Prefix.ScriptAddressTestnet && (chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
225-
AddressToPublicKeyScriptResult.Success(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))
177+
Either.Right(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))
226178

227179
it.first == Base58.Prefix.ScriptAddress && chainHash == Block.LivenetGenesisBlock.hash ->
228-
AddressToPublicKeyScriptResult.Success(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))
180+
Either.Right(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))
229181

230-
else -> AddressToPublicKeyScriptResult.Failure.ChainHashMismatch
182+
else -> Either.Left(BitcoinException.ChainHashMismatch)
231183
}
232184
},
233185
onFailure = { _ ->
234186
runCatching { Bech32.decodeWitnessAddress(address) }.fold(
235187
onSuccess = {
236188
val witnessVersion = witnessVersions[it.second]
237189
when {
238-
witnessVersion == null -> AddressToPublicKeyScriptResult.Failure.InvalidWitnessVersion(it.second.toInt())
239-
it.third.size != 20 && it.third.size != 32 -> AddressToPublicKeyScriptResult.Failure.InvalidBech32Address
240-
it.first == "bc" && chainHash == Block.LivenetGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
241-
it.first == "tb" && chainHash == Block.TestnetGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
242-
it.first == "tb" && chainHash == Block.SignetGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
243-
it.first == "bcrt" && chainHash == Block.RegtestGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
244-
else -> AddressToPublicKeyScriptResult.Failure.ChainHashMismatch
190+
witnessVersion == null -> Either.Left(BitcoinException.InvalidWitnessVersion(it.second.toInt()))
191+
it.third.size != 20 && it.third.size != 32 -> Either.Left(BitcoinException.InvalidBech32Address)
192+
it.first == "bc" && chainHash == Block.LivenetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
193+
it.first == "tb" && chainHash == Block.TestnetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
194+
it.first == "tb" && chainHash == Block.SignetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
195+
it.first == "bcrt" && chainHash == Block.RegtestGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
196+
else -> Either.Left(BitcoinException.ChainHashMismatch)
245197
}
246198
},
247199
onFailure = {
248-
AddressToPublicKeyScriptResult.Failure.InvalidAddress
200+
Either.Left(BitcoinException.InvalidAddress)
249201
}
250202
)
251203
}

src/commonMain/kotlin/fr/acinq/bitcoin/utils/Either.kt

+16-6
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,23 @@ public sealed class Either<out L, out R> {
3434

3535
public inline fun <X> map(f: (R) -> X): Either<L, X> = transform({ it }, f)
3636

37-
public data class Left<out L, Nothing>(val value: L) : Either<L, Nothing>() {
37+
public data class Left<out L>(val value: L) : Either<L, Nothing>() {
3838
override val isLeft: Boolean = true
3939
override val isRight: Boolean = false
40-
override val left: L? = value
40+
override val left: L = value
4141
override val right: Nothing? = null
4242
}
4343

44-
public data class Right<Nothing, out R>(val value: R) : Either<Nothing, R>() {
44+
public data class Right<out R>(val value: R) : Either<Nothing, R>() {
4545
override val isLeft: Boolean = false
4646
override val isRight: Boolean = true
4747
override val left: Nothing? = null
48-
override val right: R? = value
48+
override val right: R = value
4949
}
5050
}
5151

52-
@Suppress("UNCHECKED_CAST")
5352
public inline fun <L, R, X> Either<L, R>.flatMap(f: (R) -> Either<L, X>): Either<L, X> = when (this) {
54-
is Either.Left -> this as Either<L, X>
53+
is Either.Left -> this
5554
is Either.Right -> f(this.value)
5655
}
5756

@@ -64,3 +63,14 @@ public fun <L, R> Either<L, R>.getOrDefault(defaultValue: R): R = when (this) {
6463
is Either.Left -> defaultValue
6564
is Either.Right -> this.value
6665
}
66+
67+
public fun <R> Result<R>.toEither(): Either<Throwable, R> = try {
68+
Either.Right(getOrThrow())
69+
} catch (t: Throwable) {
70+
Either.Left(t)
71+
}
72+
73+
public fun <L : Throwable, R> Either<L, R>.toResult(): Result<R> = when (this) {
74+
is Either.Left -> Result.failure(this.value)
75+
is Either.Right -> Result.success(this.value)
76+
}

src/commonTest/kotlin/fr/acinq/bitcoin/BIP86TestsCommon.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class BIP86TestsCommon {
2323
assertEquals(outputKey.value, ByteVector32("a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"))
2424
val script = listOf(OP_1, OP_PUSHDATA(outputKey.value))
2525
assertEquals(Script.write(script).byteVector(), ByteVector("5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"))
26-
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script), AddressFromPublicKeyScriptResult.Success("bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"))
26+
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script).right, "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr")
2727

2828
val key1 = DeterministicWallet.derivePrivateKey(accountKey, listOf(0L, 1L))
2929
assertEquals(key1.secretkeybytes, DeterministicWallet.derivePrivateKey(master, KeyPath("m/86'/0'/0'/0/1")).secretkeybytes)
@@ -33,7 +33,7 @@ class BIP86TestsCommon {
3333
assertEquals(outputKey1.value, ByteVector32("a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"))
3434
val script1 = listOf(OP_1, OP_PUSHDATA(outputKey1.value))
3535
assertEquals(Script.write(script1).byteVector(), ByteVector("5120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"))
36-
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script1), AddressFromPublicKeyScriptResult.Success("bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"))
36+
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script1).right, "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh")
3737

3838
val key2 = DeterministicWallet.derivePrivateKey(accountKey, listOf(1L, 0L))
3939
assertEquals(key2.secretkeybytes, DeterministicWallet.derivePrivateKey(master, KeyPath("m/86'/0'/0'/1/0")).secretkeybytes)
@@ -43,7 +43,7 @@ class BIP86TestsCommon {
4343
assertEquals(outputKey2.value, ByteVector32("882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"))
4444
val script2 = listOf(OP_1, OP_PUSHDATA(outputKey2.value))
4545
assertEquals(Script.write(script2).byteVector(), ByteVector("5120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"))
46-
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script2), AddressFromPublicKeyScriptResult.Success("bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"))
46+
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script2).right, "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7")
4747
}
4848

4949
@Test

0 commit comments

Comments
 (0)