Pulled down work from adaptor-dlc onto master (#2339)

* Pulled down work from adaptor-dlc onto master

* Reverted some accidental deletions

* Removed unused import

* Added scaladocs

* Responded to Ben's review

* Added some scaladocs and invariants

* Responded to chris' review

* Responded to more review

* Added some comments
This commit is contained in:
Nadav Kohen 2020-12-11 17:32:35 -06:00 committed by GitHub
parent bd3584eb43
commit 49a281acaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 7277 additions and 1748 deletions

View file

@ -69,7 +69,6 @@ class SerializedPSBTTest extends BitcoinSUnitTest {
assert(decoded.global.tx == SerializedTransaction.decodeRawTransaction(tx))
assert(decoded.global.version == UInt32.zero)
println(Json.prettyPrint(decoded.toJson))
assert(decoded.inputs.size == 1)
assert(decoded.inputs.head.bip32Paths.isEmpty)
assert(decoded.inputs.head.finalizedScriptSig.nonEmpty)

View file

@ -0,0 +1,318 @@
package org.bitcoins.commons.dlc
import org.bitcoins.commons.jsonmodels.dlc.CETCalculator._
import org.bitcoins.commons.jsonmodels.dlc.{
CETCalculator,
OutcomeValueFunction,
OutcomeValuePoint,
RoundingIntervals
}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
class CETCalculatorTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "CETCalculator"
private val baseGen: Gen[Int] = Gen.choose(2, 256)
it should "correctly split into ranges" in {
val func = OutcomeValueFunction(
Vector(
OutcomeValuePoint(0, Satoshis(-1000), isEndpoint = true),
OutcomeValuePoint(10, Satoshis(-1000), isEndpoint = true),
OutcomeValuePoint(20, Satoshis(0), isEndpoint = false),
OutcomeValuePoint(30, Satoshis(3000), isEndpoint = true),
OutcomeValuePoint(40, Satoshis(4000), isEndpoint = true),
OutcomeValuePoint(50, Satoshis(4000), isEndpoint = true),
OutcomeValuePoint(70, Satoshis(0), isEndpoint = false),
OutcomeValuePoint(80, Satoshis(1000), isEndpoint = true),
OutcomeValuePoint(90, Satoshis(1000), isEndpoint = true),
OutcomeValuePoint(100, Satoshis(11000), isEndpoint = false),
OutcomeValuePoint(110, Satoshis(9000), isEndpoint = true)
))
val expected = Vector(
StartZero(0, 20),
StartFunc(21, 39),
StartFuncConst(40, 50),
StartFunc(51, 69),
StartZero(70, 70),
StartFunc(71, 79),
StartFuncConst(80, 90),
StartFunc(91, 98),
StartTotal(99, 108),
StartFunc(109, 110)
)
val ranges = CETCalculator.splitIntoRanges(0,
110,
Satoshis(10000),
func,
RoundingIntervals.noRounding)
assert(ranges == expected)
}
it should "correctly compute front groupings" in {
val expected = Vector(
Vector(1, 0, 9, 5),
Vector(1, 0, 9, 6),
Vector(1, 0, 9, 7),
Vector(1, 0, 9, 8),
Vector(1, 0, 9, 9),
Vector(1, 1),
Vector(1, 2),
Vector(1, 3),
Vector(1, 4),
Vector(1, 5),
Vector(1, 6),
Vector(1, 7),
Vector(1, 8),
Vector(1, 9)
)
val frontGroupings =
CETCalculator.frontGroupings(Vector(1, 0, 9, 5), 10)
assert(frontGroupings == expected)
forAll(baseGen) { base =>
val edgeCase =
CETCalculator.frontGroupings(Vector(1, 0, 0, 0), base)
assert(edgeCase == Vector(Vector(1)))
}
}
it should "correctly compute back groupings" in {
val expected = Vector(
Vector(1, 0, 0),
Vector(1, 0, 1),
Vector(1, 0, 2),
Vector(1, 0, 3),
Vector(1, 0, 4),
Vector(1, 0, 5),
Vector(1, 0, 6),
Vector(1, 0, 7),
Vector(1, 0, 8),
Vector(1, 0, 9, 0),
Vector(1, 0, 9, 1),
Vector(1, 0, 9, 2),
Vector(1, 0, 9, 3),
Vector(1, 0, 9, 4),
Vector(1, 0, 9, 5)
)
val backGroupings =
CETCalculator.backGroupings(Vector(1, 0, 9, 5), 10)
assert(backGroupings == expected)
forAll(baseGen) { base =>
val edgeCase =
CETCalculator.backGroupings(Vector(1, base - 1, base - 1, base - 1),
base)
assert(edgeCase == Vector(Vector(1)))
}
}
it should "correctly compute first digit groupings" in {
forAll(baseGen, baseGen) {
case (digit1, digit2) =>
val singleDigitGroupings =
CETCalculator.middleGrouping(digit1, digit2)
if (digit1 >= digit2 + 1) {
assert(singleDigitGroupings == Vector.empty)
} else {
assert(
singleDigitGroupings == (digit1 + 1).until(digit2).map(Vector(_)))
}
}
}
it should "correctly compute all groupings" in {
val edgeCase = CETCalculator.groupByIgnoringDigits(start = 123,
end = 123,
base = 10,
numDigits = 3)
assert(edgeCase == Vector(Vector(1, 2, 3)))
val prefix = Vector(0, 1, 2, 0)
val smallExpected = Vector(
Vector(10, 11),
Vector(10, 12),
Vector(10, 13),
Vector(10, 14),
Vector(10, 15),
Vector(11),
Vector(12),
Vector(13, 0),
Vector(13, 1),
Vector(13, 2)
)
val smallGroupings = CETCalculator.groupByIgnoringDigits(start = 171,
end = 210,
base = 16,
numDigits = 2)
assert(smallGroupings == smallExpected)
val smallExpectedWithPrefix = smallExpected.map(prefix ++ _)
val smallGroupingsWithPrefix =
CETCalculator.groupByIgnoringDigits(start = 73899,
end = 73938,
base = 16,
numDigits = 6)
assert(smallGroupingsWithPrefix == smallExpectedWithPrefix)
val expected = Vector(
Vector(1, 2, 3, 4),
Vector(1, 2, 3, 5),
Vector(1, 2, 3, 6),
Vector(1, 2, 3, 7),
Vector(1, 2, 3, 8),
Vector(1, 2, 3, 9),
Vector(1, 2, 4),
Vector(1, 2, 5),
Vector(1, 2, 6),
Vector(1, 2, 7),
Vector(1, 2, 8),
Vector(1, 2, 9),
Vector(1, 3),
Vector(1, 4),
Vector(1, 5),
Vector(1, 6),
Vector(1, 7),
Vector(1, 8),
Vector(1, 9),
Vector(2),
Vector(3),
Vector(4, 0),
Vector(4, 1),
Vector(4, 2),
Vector(4, 3, 0),
Vector(4, 3, 1),
Vector(4, 3, 2, 0),
Vector(4, 3, 2, 1)
)
val groupings = CETCalculator.groupByIgnoringDigits(start = 1234,
end = 4321,
base = 10,
numDigits = 4)
assert(groupings == expected)
val expectedWithPrefix = expected.map(prefix ++ _)
val groupingsWithPrefix =
CETCalculator.groupByIgnoringDigits(start = 1201234,
end = 1204321,
base = 10,
numDigits = 8)
assert(groupingsWithPrefix == expectedWithPrefix)
}
it should "correctly handle endpoint optimization" in {
val expected = Vector(
Vector(2, 7),
Vector(2, 8),
Vector(2, 9),
Vector(3),
Vector(4)
)
val groupings = CETCalculator.groupByIgnoringDigits(2700, 4999, 10, 4)
assert(groupings == expected)
}
it should "correctly handle total optimization" in {
val expected = Vector(Vector(5))
val groupings = CETCalculator.groupByIgnoringDigits(5000, 5999, 10, 4)
assert(groupings == expected)
}
it should "correctly compute all needed CETs" in {
val func = OutcomeValueFunction(
Vector(
OutcomeValuePoint(0, Satoshis(-1000), isEndpoint = true),
OutcomeValuePoint(10, Satoshis(-1000), isEndpoint = true),
OutcomeValuePoint(20, Satoshis(0), isEndpoint = false),
OutcomeValuePoint(30, Satoshis(3000), isEndpoint = true),
OutcomeValuePoint(40, Satoshis(4000), isEndpoint = true),
OutcomeValuePoint(50, Satoshis(4000), isEndpoint = true),
OutcomeValuePoint(70, Satoshis(0), isEndpoint = false),
OutcomeValuePoint(80, Satoshis(1000), isEndpoint = true),
OutcomeValuePoint(90, Satoshis(1000), isEndpoint = true),
OutcomeValuePoint(100, Satoshis(11000), isEndpoint = false),
OutcomeValuePoint(110, Satoshis(9000), isEndpoint = true)
))
val firstZeroRange = Vector(
Vector(0, 0) -> Satoshis(0),
Vector(0, 1) -> Satoshis(0),
Vector(0, 2, 0) -> Satoshis(0)
)
val firstFuncRange = 21.until(40).toVector.map { num =>
Vector(0, num / 10, num % 10) -> func(num)
}
val firstConstRange = Vector(
Vector(0, 4) -> Satoshis(4000),
Vector(0, 5, 0) -> Satoshis(4000)
)
val secondFuncRange = 51.until(80).toVector.map { num =>
Vector(0, num / 10, num % 10) -> func(num)
}
val secondConstRange = Vector(
Vector(0, 8) -> Satoshis(1000),
Vector(0, 9, 0) -> Satoshis(1000)
)
val thirdFuncRange = 91.until(99).toVector.map { num =>
Vector(0, num / 10, num % 10) -> func(num)
}
val firstTotalRange = Vector(
Vector(0, 9, 9) -> Satoshis(10000),
Vector(1, 0, 0) -> Satoshis(10000),
Vector(1, 0, 1) -> Satoshis(10000),
Vector(1, 0, 2) -> Satoshis(10000),
Vector(1, 0, 3) -> Satoshis(10000),
Vector(1, 0, 4) -> Satoshis(10000),
Vector(1, 0, 5) -> Satoshis(10000),
Vector(1, 0, 6) -> Satoshis(10000),
Vector(1, 0, 7) -> Satoshis(10000),
Vector(1, 0, 8) -> Satoshis(10000)
)
val fourthFuncRange = Vector(
Vector(1, 0, 9) -> func(109),
Vector(1, 1, 0) -> func(110)
)
val expected =
firstZeroRange ++
firstFuncRange ++
firstConstRange ++
secondFuncRange ++
secondConstRange ++
thirdFuncRange ++
firstTotalRange ++
fourthFuncRange
val cetOutcomes =
CETCalculator.computeCETs(base = 10,
numDigits = 3,
function = func,
totalCollateral = Satoshis(10000),
rounding = RoundingIntervals.noRounding,
min = 0,
max = 110)
assert(cetOutcomes == expected)
}
}

View file

@ -1,11 +1,6 @@
package org.bitcoins.commons.dlc
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
ContractInfo,
DLCAccept,
DLCOffer,
OracleInfo
}
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.{
CETSignatures,
DLCPublicKeys,
@ -15,24 +10,21 @@ import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.BlockStamp.{BlockHeight, BlockTime}
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto.{
DummyECDigitalSignature,
ECPublicKey,
Sha256DigestBE
}
import org.bitcoins.crypto._
import org.bitcoins.testkit.core.gen.{LnMessageGen, TLVGen}
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import scodec.bits.ByteVector
class DLCMessageTest extends BitcoinSAsyncTest {
behavior of "DLCMessage"
it must "not allow a DLCTimeout where the contract times out before it matures" in {
assertThrows[IllegalArgumentException](
DLCTimeouts(UInt32(5), BlockHeight(4), BlockHeight(2)))
DLCTimeouts(BlockHeight(4), BlockHeight(2)))
assertThrows[IllegalArgumentException](
DLCTimeouts(UInt32(5), BlockTime(UInt32(4)), BlockTime(UInt32(2))))
DLCTimeouts(BlockTime(UInt32(4)), BlockTime(UInt32(2))))
}
val dummyPubKey: ECPublicKey = ECPublicKey.freshPublicKey
@ -41,8 +33,8 @@ class DLCMessageTest extends BitcoinSAsyncTest {
val dummyAddress: BitcoinAddress = BitcoinAddress(
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
val dummyHash: Sha256DigestBE = Sha256DigestBE(
"00000000000000000008bba30d4d0fb53dcbffb601557de9f16d257d4f1985b7")
val dummyStr: String =
"00000000000000000008bba30d4d0fb53dcbffb601557de9f16d257d4f1985b7"
val dummySig: PartialSignature =
PartialSignature(dummyPubKey, DummyECDigitalSignature)
@ -50,14 +42,13 @@ class DLCMessageTest extends BitcoinSAsyncTest {
it must "not allow a negative collateral for a DLCOffer" in {
assertThrows[IllegalArgumentException](
DLCOffer(
ContractInfo.empty,
OracleInfo.dummy,
DLCPublicKeys(dummyPubKey, dummyPubKey2, dummyAddress),
OracleAndContractInfo(OracleInfo.dummy, ContractInfo.empty),
DLCPublicKeys(dummyPubKey, dummyAddress),
Satoshis(-1),
Vector.empty,
dummyAddress,
SatoshisPerVirtualByte.one,
DLCTimeouts(UInt32(5), BlockHeight(1), BlockHeight(2))
DLCTimeouts(BlockHeight(1), BlockHeight(2))
))
}
@ -65,18 +56,13 @@ class DLCMessageTest extends BitcoinSAsyncTest {
assertThrows[IllegalArgumentException](
DLCAccept(
Satoshis(-1),
DLCPublicKeys(dummyPubKey, dummyPubKey2, dummyAddress),
DLCPublicKeys(dummyPubKey, dummyAddress),
Vector.empty,
dummyAddress,
CETSignatures(Map(dummyHash -> dummySig), dummySig),
Sha256DigestBE(ByteVector.low(32))
CETSignatures(Vector(EnumOutcome(dummyStr) -> ECAdaptorSignature.dummy),
dummySig),
Sha256Digest.empty
)
)
}
it must "not allow duplicate keys in a DLCPublicKeys" in {
assertThrows[IllegalArgumentException](
DLCPublicKeys(dummyPubKey, dummyPubKey, dummyAddress)
)
}
}

View file

@ -0,0 +1,266 @@
package org.bitcoins.commons.dlc
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.{DLCState, DLCStatus}
import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator, TLVGen}
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.scalacheck.Gen
class DLCStatusTest extends BitcoinSAsyncTest {
behavior of "DLCStatus"
it must "have json symmetry in DLCStatus.Offered" in {
forAllParallel(NumberGenerator.bool, TLVGen.dlcOfferTLV) {
case (isInit, offerTLV) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val status =
DLCStatus.Offered(offer.paramHash,
isInit,
offer.tempContractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral)
assert(status.state == DLCState.Offered)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.Accepted" in {
forAllParallel(NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector) {
case (isInit, offerTLV, contractId) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val status =
DLCStatus.Accepted(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral
)
assert(status.state == DLCState.Accepted)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.Signed" in {
forAllParallel(NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector) {
case (isInit, offerTLV, contractId) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val status =
DLCStatus.Signed(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral
)
assert(status.state == DLCState.Signed)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.Broadcasted" in {
forAllParallel(NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector,
CryptoGenerators.doubleSha256DigestBE) {
case (isInit, offerTLV, contractId, fundingTxId) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val status =
DLCStatus.Broadcasted(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral,
fundingTxId
)
assert(status.state == DLCState.Broadcasted)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.Confirmed" in {
forAllParallel(NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector,
CryptoGenerators.doubleSha256DigestBE) {
case (isInit, offerTLV, contractId, fundingTxId) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val status =
DLCStatus.Confirmed(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral,
fundingTxId
)
assert(status.state == DLCState.Confirmed)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.Claimed" in {
forAllParallel(
NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector,
CryptoGenerators.doubleSha256DigestBE,
CryptoGenerators.doubleSha256DigestBE,
Gen.listOf(CryptoGenerators.schnorrDigitalSignature)
) {
case (isInit, offerTLV, contractId, fundingTxId, closingTxId, sigs) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val rand =
scala.util.Random.nextInt(offer.contractInfo.allOutcomes.size)
val outcome = offer.contractInfo.allOutcomes(rand)
val status =
DLCStatus.Claimed(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral,
fundingTxId,
closingTxId,
sigs.toVector,
outcome
)
assert(status.state == DLCState.Claimed)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.RemoteClaimed" in {
forAllParallel(
NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector,
CryptoGenerators.doubleSha256DigestBE,
CryptoGenerators.doubleSha256DigestBE,
CryptoGenerators.schnorrDigitalSignature
) {
case (isInit, offerTLV, contractId, fundingTxId, closingTxId, sig) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val rand =
scala.util.Random.nextInt(offer.contractInfo.allOutcomes.size)
val outcome = offer.contractInfo.allOutcomes(rand)
val status =
DLCStatus.RemoteClaimed(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral,
fundingTxId,
closingTxId,
sig,
outcome
)
assert(status.state == DLCState.RemoteClaimed)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
it must "have json symmetry in DLCStatus.Refunded" in {
forAllParallel(
NumberGenerator.bool,
TLVGen.dlcOfferTLV,
NumberGenerator.bytevector,
CryptoGenerators.doubleSha256DigestBE,
CryptoGenerators.doubleSha256DigestBE
) {
case (isInit, offerTLV, contractId, fundingTxId, closingTxId) =>
val offer = DLCOffer.fromTLV(offerTLV)
val totalCollateral = offer.contractInfo.max
val status =
DLCStatus.Refunded(
offer.paramHash,
isInit,
offer.tempContractId,
contractId,
offer.oracleInfo,
offer.contractInfo,
offer.timeouts,
offer.feeRate,
totalCollateral,
offer.totalCollateral,
fundingTxId,
closingTxId
)
assert(status.state == DLCState.Refunded)
assert(DLCStatus.fromJson(status.toJson) == status)
}
}
}

View file

@ -0,0 +1,332 @@
package org.bitcoins.commons.dlc
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
import scala.math.BigDecimal.RoundingMode
class OutcomeValueFunctionTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "OutcomeValueFunction"
private val numGen = Gen.choose[Double](0, 1000).map(BigDecimal(_))
private val intGen = Gen.choose[Int](0, 1000)
def nPoints(n: Int): Gen[Vector[OutcomeValuePoint]] = {
val valueGen = Gen.choose[Long](0, 10000)
val pointGen = for {
outcome <- numGen
value <- valueGen
} yield OutcomeValuePoint(outcome, Satoshis(value), isEndpoint = true)
Gen
.listOfN(n, pointGen)
.suchThat(points =>
points.map(_.outcome).distinct.length == points.length)
.map(_.toVector.sortBy(_.outcome))
}
it should "agree on lines and degree 1 polynomials" in {
forAll(nPoints(2), Gen.listOfN(1000, numGen)) {
case (Vector(point1, point2), outcomes) =>
val line = OutcomeValueLine(point1, point2)
val polyDegOne = OutcomeValuePolynomial(Vector(point1, point2))
outcomes.foreach { outcome =>
assert(line(outcome) == polyDegOne(outcome))
}
case _ => fail()
}
}
it should "agree on lines and y = mx + b" in {
val twoNums = for {
num1 <- intGen
num2 <- intGen.suchThat(_ != num1)
} yield {
if (num1 < num2) {
(num1, num2)
} else {
(num2, num1)
}
}
forAll(intGen, intGen, Gen.listOfN(1000, numGen), twoNums) {
case (slope, yIntercept, outcomes, (x1, x2)) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value = slope * outcome + yIntercept
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val point1 =
OutcomeValuePoint(x1, expectedPayout(x1), isEndpoint = true)
val point2 =
OutcomeValuePoint(x2, expectedPayout(x2), isEndpoint = true)
val line = OutcomeValueLine(point1, point2)
outcomes.foreach { outcome =>
assert(line(outcome) == expectedPayout(outcome))
}
}
}
it should "agree on quadratics and degree 2 polynomials" in {
forAll(nPoints(3), Gen.listOfN(1000, numGen)) {
case (Vector(point1, point2, point3), outcomes) =>
val midPoint2 = point2.copy(isEndpoint = false)
val parabola = OutcomeValueQuadratic(point1, midPoint2, point3)
val polyDegTwo =
OutcomeValuePolynomial(Vector(point1, midPoint2, point3))
outcomes.foreach { outcome =>
assert(parabola(outcome) == polyDegTwo(outcome))
}
case _ => fail()
}
}
it should "agree on quadratics and y = ax^2 + bx + c" in {
val threeNums = for {
num1 <- intGen
num2 <- intGen.suchThat(_ != num1)
num3 <- intGen.suchThat(x => x != num1 && x != num2)
} yield {
val nums = Vector(num1, num2, num3).sorted
(nums(0), nums(1), nums(2))
}
forAll(intGen, intGen, intGen, Gen.listOfN(1000, numGen), threeNums) {
case (a, b, c, outcomes, (x1, x2, x3)) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value = a * outcome * outcome + b * outcome + c
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val point1 =
OutcomeValuePoint(x1, expectedPayout(x1), isEndpoint = true)
val point2 =
OutcomeValuePoint(x2, expectedPayout(x2), isEndpoint = false)
val point3 =
OutcomeValuePoint(x3, expectedPayout(x3), isEndpoint = true)
val parabola = OutcomeValueQuadratic(point1, point2, point3)
outcomes.foreach { outcome =>
assert(parabola(outcome) == expectedPayout(outcome))
}
}
}
it should "agree on degenerate quadratics and lines" in {
val threeNums = for {
num1 <- intGen
num2 <- intGen.suchThat(_ != num1)
num3 <- intGen.suchThat(x => x != num1 && x != num2)
} yield {
val nums = Vector(num1, num2, num3).sorted
(nums(0), nums(1), nums(2))
}
forAll(intGen, intGen, Gen.listOfN(1000, numGen), threeNums) {
case (slope, yIntercept, outcomes, (x1, x2, x3)) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value = slope * outcome + yIntercept
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val point1 =
OutcomeValuePoint(x1, expectedPayout(x1), isEndpoint = true)
val point2 =
OutcomeValuePoint(x2, expectedPayout(x2), isEndpoint = false)
val point3 =
OutcomeValuePoint(x3, expectedPayout(x3), isEndpoint = true)
val line = OutcomeValueLine(point1, point3)
val parabola = OutcomeValueQuadratic(point1, point2, point3)
outcomes.foreach { outcome =>
assert(line(outcome) == parabola(outcome))
}
}
}
it should "agree on cubics and degree 3 polynomials" in {
forAll(nPoints(4), Gen.listOfN(1000, numGen)) {
case (Vector(point1, point2, point3, point4), outcomes) =>
val midPoint2 = point2.copy(isEndpoint = false)
val midPoint3 = point3.copy(isEndpoint = false)
val cubic = OutcomeValueCubic(point1, midPoint2, midPoint3, point4)
val polyDegThree =
OutcomeValuePolynomial(Vector(point1, midPoint2, midPoint3, point4))
outcomes.foreach { outcome =>
assert(cubic(outcome) == polyDegThree(outcome))
}
case _ => fail()
}
}
it should "agree on cubics and y = ax^3 + bx^2 + cx + d" in {
val fourNums = for {
num1 <- intGen
num2 <- intGen.suchThat(_ != num1)
num3 <- intGen.suchThat(x => x != num1 && x != num2)
num4 <- intGen.suchThat(x => x != num1 && x != num2 && x != num3)
} yield {
val nums = Vector(num1, num2, num3, num4).sorted
(nums(0), nums(1), nums(2), nums(3))
}
forAll(intGen,
intGen,
intGen,
intGen,
Gen.listOfN(1000, numGen),
fourNums) {
case (a, b, c, d, outcomes, (x1, x2, x3, x4)) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value =
a * outcome * outcome * outcome + b * outcome * outcome + c * outcome + d
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val point1 =
OutcomeValuePoint(x1, expectedPayout(x1), isEndpoint = true)
val point2 =
OutcomeValuePoint(x2, expectedPayout(x2), isEndpoint = false)
val point3 =
OutcomeValuePoint(x3, expectedPayout(x3), isEndpoint = false)
val point4 =
OutcomeValuePoint(x4, expectedPayout(x4), isEndpoint = true)
val cubic = OutcomeValueCubic(point1, point2, point3, point4)
outcomes.foreach { outcome =>
assert(cubic(outcome) == expectedPayout(outcome))
}
}
}
it should "agree on degenerate cubics and lines" in {
val fourNums = for {
num1 <- intGen
num2 <- intGen.suchThat(_ != num1)
num3 <- intGen.suchThat(x => x != num1 && x != num2)
num4 <- intGen.suchThat(x => x != num1 && x != num2 && x != num3)
} yield {
val nums = Vector(num1, num2, num3, num4).sorted
(nums(0), nums(1), nums(2), nums(3))
}
forAll(intGen, intGen, Gen.listOfN(1000, numGen), fourNums) {
case (slope, yIntercept, outcomes, (x1, x2, x3, x4)) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value = slope * outcome + yIntercept
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val point1 =
OutcomeValuePoint(x1, expectedPayout(x1), isEndpoint = true)
val point2 =
OutcomeValuePoint(x2, expectedPayout(x2), isEndpoint = false)
val point3 =
OutcomeValuePoint(x3, expectedPayout(x3), isEndpoint = false)
val point4 =
OutcomeValuePoint(x4, expectedPayout(x4), isEndpoint = true)
val line = OutcomeValueLine(point1, point4)
val cubic = OutcomeValueCubic(point1, point2, point3, point4)
outcomes.foreach { outcome =>
assert(line(outcome) == cubic(outcome))
}
}
}
it should "agree on degenerate cubics and quadratics" in {
val fourNums = for {
num1 <- intGen
num2 <- intGen.suchThat(_ != num1)
num3 <- intGen.suchThat(x => x != num1 && x != num2)
num4 <- intGen.suchThat(x => x != num1 && x != num2 && x != num3)
} yield {
val nums = Vector(num1, num2, num3, num4).sorted
(nums(0), nums(1), nums(2), nums(3))
}
forAll(intGen, intGen, intGen, Gen.listOfN(1000, numGen), fourNums) {
case (a, b, c, outcomes, (x1, x2, x3, x4)) =>
def expectedPayout(outcome: BigDecimal): Satoshis = {
val value = a * outcome * outcome + b * outcome + c
val rounded = value.setScale(0, RoundingMode.FLOOR).toLongExact
Satoshis(rounded)
}
val point1 =
OutcomeValuePoint(x1, expectedPayout(x1), isEndpoint = true)
val point2 =
OutcomeValuePoint(x2, expectedPayout(x2), isEndpoint = false)
val point3 =
OutcomeValuePoint(x3, expectedPayout(x3), isEndpoint = false)
val point4 =
OutcomeValuePoint(x4, expectedPayout(x4), isEndpoint = true)
val quadratic = OutcomeValueQuadratic(point1, point2, point4)
val cubic = OutcomeValueCubic(point1, point2, point3, point4)
outcomes.foreach { outcome =>
assert(quadratic(outcome) == cubic(outcome))
}
}
}
it should "parse points into component functions correctly and compute outputs" in {
val point0 = OutcomeValuePoint(0, Satoshis.zero, isEndpoint = true)
val point1 = OutcomeValuePoint(1, Satoshis.one, isEndpoint = true)
val line = OutcomeValueFunction(Vector(point0, point1))
val lineFunc = line.functionComponents
assert(lineFunc == Vector(OutcomeValueLine(point0, point1)))
val point2 = OutcomeValuePoint(2, Satoshis.zero, isEndpoint = false)
val point3 = OutcomeValuePoint(3, Satoshis(3), isEndpoint = true)
val quad = OutcomeValueFunction(Vector(point1, point2, point3))
val quadFunc = quad.functionComponents
assert(quadFunc == Vector(OutcomeValueQuadratic(point1, point2, point3)))
val point4 = OutcomeValuePoint(4, Satoshis(6), isEndpoint = false)
val point5 = OutcomeValuePoint(5, Satoshis(5), isEndpoint = false)
val point6 = OutcomeValuePoint(6, Satoshis(7), isEndpoint = true)
val cubicPoints = Vector(point3, point4, point5, point6)
val cubic = OutcomeValueFunction(cubicPoints)
val cubicFunc = cubic.functionComponents
assert(
cubicFunc == Vector(OutcomeValueCubic(point3, point4, point5, point6)))
val func = OutcomeValueFunction(
Vector(point0, point1, point2, point3, point4, point5, point6))
val allFuncs = func.functionComponents
assert(allFuncs == lineFunc ++ quadFunc ++ cubicFunc)
forAll(Gen.choose[Double](0, 6)) { outcome =>
val value = func(outcome)
if (0 <= outcome && outcome < 1) {
assert(value == line(outcome))
} else if (1 <= outcome && outcome < 3) {
assert(value == quad(outcome))
} else {
assert(value == cubic(outcome))
}
}
}
}

View file

@ -0,0 +1,168 @@
package org.bitcoins.commons.dlc
import org.bitcoins.commons.jsonmodels.dlc.RoundingIntervals
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
class RoundingIntervalsTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "RoundingIntervals"
it should "correctly not round" in {
val anyLongGen = Gen.choose(Long.MinValue, Long.MaxValue)
forAll(anyLongGen, anyLongGen) {
case (outcome, payout) =>
val roundedPayout =
RoundingIntervals.noRounding.round(outcome, Satoshis(payout))
assert(roundedPayout == Satoshis(payout))
}
}
it should "correctly round" in {
val roundingInterval = RoundingIntervals(
Vector(
BigDecimal(20) -> 2,
BigDecimal(30) -> 3,
BigDecimal(40) -> 4,
BigDecimal(50) -> 5,
BigDecimal(100) -> 10,
BigDecimal(1000) -> 100
))
assert(roundingInterval.round(15, Satoshis(12345)) == Satoshis(12345))
assert(roundingInterval.round(25, Satoshis(1234)) == Satoshis(1234))
assert(roundingInterval.round(25, Satoshis(12345)) == Satoshis(12346))
assert(roundingInterval.round(35, Satoshis(12345)) == Satoshis(12345))
assert(roundingInterval.round(35, Satoshis(12346)) == Satoshis(12345))
assert(roundingInterval.round(35, Satoshis(12347)) == Satoshis(12348))
assert(roundingInterval.round(45, Satoshis(12344)) == Satoshis(12344))
assert(roundingInterval.round(45, Satoshis(12345)) == Satoshis(12344))
assert(roundingInterval.round(45, Satoshis(12346)) == Satoshis(12348))
assert(roundingInterval.round(45, Satoshis(12347)) == Satoshis(12348))
assert(roundingInterval.round(55, Satoshis(12345)) == Satoshis(12345))
assert(roundingInterval.round(55, Satoshis(12346)) == Satoshis(12345))
assert(roundingInterval.round(55, Satoshis(12347)) == Satoshis(12345))
assert(roundingInterval.round(55, Satoshis(12348)) == Satoshis(12350))
assert(roundingInterval.round(55, Satoshis(12349)) == Satoshis(12350))
assert(roundingInterval.round(500, Satoshis(12340)) == Satoshis(12340))
assert(roundingInterval.round(500, Satoshis(12341)) == Satoshis(12340))
assert(roundingInterval.round(500, Satoshis(12344)) == Satoshis(12340))
assert(roundingInterval.round(500, Satoshis(12345)) == Satoshis(12350))
assert(roundingInterval.round(500, Satoshis(12346)) == Satoshis(12350))
assert(roundingInterval.round(500, Satoshis(12349)) == Satoshis(12350))
assert(roundingInterval.round(500, Satoshis(12350)) == Satoshis(12350))
assert(roundingInterval.round(5000, Satoshis(12300)) == Satoshis(12300))
assert(roundingInterval.round(5000, Satoshis(12301)) == Satoshis(12300))
assert(roundingInterval.round(5000, Satoshis(12349)) == Satoshis(12300))
assert(roundingInterval.round(5000, Satoshis(12350)) == Satoshis(12400))
assert(roundingInterval.round(5000, Satoshis(12351)) == Satoshis(12400))
assert(roundingInterval.round(5000, Satoshis(12399)) == Satoshis(12400))
assert(roundingInterval.round(5000, Satoshis(12400)) == Satoshis(12400))
}
it should "correctly round on negative payouts" in {
val roundingInterval = RoundingIntervals(
Vector(
BigDecimal(20) -> 2,
BigDecimal(30) -> 3,
BigDecimal(40) -> 4,
BigDecimal(50) -> 5,
BigDecimal(100) -> 10,
BigDecimal(1000) -> 100
))
assert(roundingInterval.round(15, Satoshis(-12345)) == Satoshis(-12345))
assert(roundingInterval.round(25, Satoshis(-1234)) == Satoshis(-1234))
assert(roundingInterval.round(25, Satoshis(-12345)) == Satoshis(-12344))
assert(roundingInterval.round(35, Satoshis(-12345)) == Satoshis(-12345))
assert(roundingInterval.round(35, Satoshis(-12346)) == Satoshis(-12345))
assert(roundingInterval.round(35, Satoshis(-12347)) == Satoshis(-12348))
assert(roundingInterval.round(45, Satoshis(-12344)) == Satoshis(-12344))
assert(roundingInterval.round(45, Satoshis(-12345)) == Satoshis(-12344))
assert(roundingInterval.round(45, Satoshis(-12346)) == Satoshis(-12344))
assert(roundingInterval.round(45, Satoshis(-12347)) == Satoshis(-12348))
assert(roundingInterval.round(55, Satoshis(-12345)) == Satoshis(-12345))
assert(roundingInterval.round(55, Satoshis(-12346)) == Satoshis(-12345))
assert(roundingInterval.round(55, Satoshis(-12347)) == Satoshis(-12345))
assert(roundingInterval.round(55, Satoshis(-12348)) == Satoshis(-12350))
assert(roundingInterval.round(55, Satoshis(-12349)) == Satoshis(-12350))
assert(roundingInterval.round(500, Satoshis(-12340)) == Satoshis(-12340))
assert(roundingInterval.round(500, Satoshis(-12341)) == Satoshis(-12340))
assert(roundingInterval.round(500, Satoshis(-12344)) == Satoshis(-12340))
assert(roundingInterval.round(500, Satoshis(-12345)) == Satoshis(-12340))
assert(roundingInterval.round(500, Satoshis(-12346)) == Satoshis(-12350))
assert(roundingInterval.round(500, Satoshis(-12349)) == Satoshis(-12350))
assert(roundingInterval.round(500, Satoshis(-12350)) == Satoshis(-12350))
assert(roundingInterval.round(5000, Satoshis(-12300)) == Satoshis(-12300))
assert(roundingInterval.round(5000, Satoshis(-12301)) == Satoshis(-12300))
assert(roundingInterval.round(5000, Satoshis(-12349)) == Satoshis(-12300))
assert(roundingInterval.round(5000, Satoshis(-12350)) == Satoshis(-12300))
assert(roundingInterval.round(5000, Satoshis(-12351)) == Satoshis(-12400))
assert(roundingInterval.round(5000, Satoshis(-12399)) == Satoshis(-12400))
assert(roundingInterval.round(5000, Satoshis(-12400)) == Satoshis(-12400))
}
it should "correctly merge two RoundingIntervals" in {
val roundingIntervals1 = RoundingIntervals(
Vector(
BigDecimal(2) -> 3,
BigDecimal(4) -> 2,
BigDecimal(5) -> 1,
BigDecimal(6) -> 2,
BigDecimal(7) -> 4,
BigDecimal(8) -> 5,
BigDecimal(9) -> 1,
BigDecimal(10) -> 4,
BigDecimal(12) -> 2,
BigDecimal(13) -> 6,
BigDecimal(14) -> 3,
BigDecimal(15) -> 7,
BigDecimal(16) -> 1
))
val roundingIntervals2 = RoundingIntervals(
Vector(
BigDecimal(1) -> 2,
BigDecimal(3) -> 4,
BigDecimal(4) -> 1,
BigDecimal(5) -> 2,
BigDecimal(6) -> 3,
BigDecimal(11) -> 5
))
val expected = RoundingIntervals(
Vector(
BigDecimal(2) -> 2,
BigDecimal(3) -> 3,
BigDecimal(4) -> 1,
BigDecimal(6) -> 2,
BigDecimal(7) -> 3,
BigDecimal(9) -> 1,
BigDecimal(10) -> 3,
BigDecimal(11) -> 4,
BigDecimal(12) -> 2,
BigDecimal(13) -> 5,
BigDecimal(14) -> 3,
BigDecimal(15) -> 5,
BigDecimal(16) -> 1
))
assert(roundingIntervals1.minRoundingWith(roundingIntervals2) == expected)
}
}

View file

@ -0,0 +1,33 @@
package org.bitcoins.commons.jsonmodels
import org.bitcoins.core.config.{BitcoinNetwork, BitcoinNetworks}
import org.bitcoins.crypto.DoubleSha256DigestBE
import ujson._
/** Basic information about the chain state of the Bitcoin-S server */
case class BitcoinSServerInfo(
network: BitcoinNetwork,
blockHeight: Int,
blockHash: DoubleSha256DigestBE) {
lazy val toJson: Value = {
Obj(
"network" -> Str(network.name),
"blockHeight" -> Num(blockHeight),
"blockHash" -> Str(blockHash.hex)
)
}
}
object BitcoinSServerInfo {
def fromJson(json: Value): BitcoinSServerInfo = {
val obj = json.obj
val network = BitcoinNetworks.fromString(obj("network").str)
val height = obj("blockHeight").num.toInt
val blockHash = DoubleSha256DigestBE(obj("blockHash").str)
BitcoinSServerInfo(network, height, blockHash)
}
}

View file

@ -0,0 +1,403 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.tlv.{
DLCOutcomeType,
EnumOutcome,
UnsignedNumericOutcome
}
import org.bitcoins.core.util.NumberUtil
import scala.annotation.tailrec
object CETCalculator {
/** Given a range and a payout function with which to build CETs,
* the first step is to split the range into sub-ranges which
* can be compressed or cannot be compressed, represented here
* as CETRanges.
*
* These ranges are inclusive in both indices.
*/
sealed trait CETRange {
def indexFrom: Long
def indexTo: Long
}
/** This range contains payouts all <= 0
* (Note that interpolated functions are allowed
* to be negative, but we set all negative values to 0).
*/
case class StartZero(indexFrom: Long, indexTo: Long) extends CETRange
/** This range contains payouts all >= totalCollateral */
case class StartTotal(indexFrom: Long, indexTo: Long) extends CETRange
/** This range contains payouts that all vary at every step and cannot be compressed */
case class StartFunc(indexFrom: Long, indexTo: Long) extends CETRange
/** This range contains some constant payout between 0 and totalCollateral (exclusive).
* To be clear, indexFrom and indexTo are still inclusive values.
*/
case class StartFuncConst(indexFrom: Long, indexTo: Long) extends CETRange
object CETRange {
/** Creates a new CETRange with a single element range */
def apply(
index: Long,
value: Satoshis,
totalCollateral: Satoshis): CETRange = {
if (value <= Satoshis.zero) {
StartZero(index, index)
} else if (value >= totalCollateral) {
StartTotal(index, index)
} else {
StartFunc(index, index)
}
}
}
/** Goes between from and to (inclusive) and evaluates function to split the
* interval [to, from] into CETRanges.
*/
def splitIntoRanges(
from: Long,
to: Long,
totalCollateral: Satoshis,
function: OutcomeValueFunction,
rounding: RoundingIntervals): Vector[CETRange] = {
var componentStart = from
var (currentFunc, componentIndex) = function.componentFor(from)
var prevFunc = currentFunc
val rangeBuilder = Vector.newBuilder[CETRange]
var currentRange: CETRange =
CETRange(from, currentFunc(from, rounding), totalCollateral)
var num = from
def newRange(value: Satoshis): Unit = {
rangeBuilder += currentRange
currentRange = CETRange(num, value, totalCollateral)
}
def updateComponent(): Unit = {
componentStart = num
prevFunc = currentFunc
componentIndex = componentIndex + 1
currentFunc = function.functionComponents(componentIndex)
}
@tailrec
def processConstantComponents(): Unit = {
currentFunc match {
case OutcomeValueConstant(_, rightEndpoint) =>
val componentEnd = rightEndpoint.outcome.toLongExact - 1
val funcValue = rightEndpoint.value
if (funcValue <= Satoshis.zero) {
currentRange match {
case StartZero(indexFrom, _) =>
currentRange = StartZero(indexFrom, componentEnd)
case _: StartTotal | _: StartFunc | _: StartFuncConst =>
rangeBuilder += currentRange
currentRange = StartZero(componentStart, componentEnd)
}
} else if (funcValue >= totalCollateral) {
currentRange match {
case StartTotal(indexFrom, _) =>
currentRange = StartTotal(indexFrom, componentEnd)
case _: StartZero | _: StartFunc | _: StartFuncConst =>
rangeBuilder += currentRange
currentRange = StartTotal(componentStart, componentEnd)
}
} else if (num != from && funcValue == prevFunc(num - 1, rounding)) {
currentRange match {
case StartFunc(indexFrom, indexTo) =>
rangeBuilder += StartFunc(indexFrom, indexTo - 1)
currentRange = StartFuncConst(indexTo, componentEnd)
case StartFuncConst(indexFrom, _) =>
currentRange = StartFuncConst(indexFrom, componentEnd)
case _: StartZero | _: StartTotal =>
throw new RuntimeException("Something has gone horribly wrong.")
}
} else {
rangeBuilder += currentRange
currentRange = StartFuncConst(componentStart, componentEnd)
}
num = componentEnd + 1
if (num != to) {
updateComponent()
processConstantComponents()
}
case _: OutcomeValueFunctionComponent => ()
}
}
processConstantComponents()
while (num <= to) {
if (num == currentFunc.rightEndpoint.outcome && num != to) {
updateComponent()
processConstantComponents()
}
val value = currentFunc(num, rounding)
if (value <= Satoshis.zero) {
currentRange match {
case StartZero(indexFrom, _) =>
currentRange = StartZero(indexFrom, num)
case _: StartTotal | _: StartFunc | _: StartFuncConst =>
newRange(value)
}
} else if (value >= totalCollateral) {
currentRange match {
case StartTotal(indexFrom, _) =>
currentRange = StartTotal(indexFrom, num)
case _: StartZero | _: StartFunc | _: StartFuncConst =>
newRange(value)
}
} else if (
num != from &&
(num - 1 >= componentStart && value == currentFunc(num - 1,
rounding)) ||
(num - 1 < componentStart && value == prevFunc(num - 1, rounding))
) {
currentRange match {
case StartFunc(indexFrom, indexTo) =>
rangeBuilder += StartFunc(indexFrom, indexTo - 1)
currentRange = StartFuncConst(num - 1, num)
case StartFuncConst(indexFrom, _) =>
currentRange = StartFuncConst(indexFrom, num)
case _: StartZero | _: StartTotal =>
throw new RuntimeException("Something has gone horribly wrong.")
}
} else {
currentRange match {
case StartFunc(indexFrom, _) =>
currentRange = StartFunc(indexFrom, num)
case _: StartZero | _: StartTotal | _: StartFuncConst =>
newRange(value)
}
}
num += 1
}
rangeBuilder += currentRange
rangeBuilder.result()
}
/** Searches for an outcome which contains a prefix of digits */
def searchForPrefix[Outcome](digits: Vector[Int], outcomes: Vector[Outcome])(
outcomeToPrefix: Outcome => Vector[Int]): Option[Outcome] = {
val indexOrOverByOne = NumberUtil.search(outcomes, digits, outcomeToPrefix)(
NumberUtil.lexicographicalOrdering[Int])
if (indexOrOverByOne == outcomes.length) {
if (digits.startsWith(outcomeToPrefix(outcomes.last))) {
Some(outcomes.last)
} else None
} else if (indexOrOverByOne == 0) {
if (digits.startsWith(outcomeToPrefix(outcomes.head))) {
Some(outcomes.head)
} else None
} else if (digits == outcomeToPrefix(outcomes(indexOrOverByOne))) {
Some(outcomes(indexOrOverByOne))
} else {
if (digits.startsWith(outcomeToPrefix(outcomes(indexOrOverByOne - 1)))) {
Some(outcomes(indexOrOverByOne - 1))
} else None
}
}
/** Searches for an UnsignedNumericOutcome corresponding to (prefixing) digits */
def searchForNumericOutcome(
digits: Vector[Int],
outcomes: Vector[DLCOutcomeType]): Option[UnsignedNumericOutcome] = {
searchForPrefix(digits, outcomes) {
case outcome: EnumOutcome =>
throw new IllegalArgumentException(
s"Expected Numeric Outcome, got $outcome")
case UnsignedNumericOutcome(digits) => digits
}.asInstanceOf[Option[UnsignedNumericOutcome]]
}
/** Computes the front groupings in the CETCompression
* (with endpoint optimization but without total optimization).
* This means the resulting outcomes cover [start, (prefix, digits[0], base-1, ..., base-1)].
*
* @param digits The unique digits of the range's start
* @param base The base the digits are represented in
*/
def frontGroupings(digits: Vector[Int], base: Int): Vector[Vector[Int]] = {
val nonZeroDigits =
digits.reverse.zipWithIndex.dropWhile(_._1 == 0) // Endpoint Optimization
if (nonZeroDigits.isEmpty) { // All digits are 0
Vector(Vector(0))
} else {
val fromFront = nonZeroDigits.init.flatMap {
case (lastImportantDigit, unimportantDigits) =>
val fixedDigits = digits.dropRight(unimportantDigits + 1)
(lastImportantDigit + 1).until(base).map { lastDigit =>
fixedDigits :+ lastDigit
}
}
nonZeroDigits.map(_._1).reverse +: fromFront // Add Endpoint
}
}
/** Computes the back groupings in the CETCompression
* (with endpoint optimization but without total optimization).
* This means the resulting outcomes cover [(prefix, digits[0], 0, ..., 0), end].
*
* @param digits The unique digits of the range's end
* @param base The base the digits are represented in
*/
def backGroupings(digits: Vector[Int], base: Int): Vector[Vector[Int]] = {
val nonMaxDigits =
digits.reverse.zipWithIndex.dropWhile(
_._1 == base - 1
) // Endpoint Optimization
if (nonMaxDigits.isEmpty) { // All digits are max
Vector(Vector(base - 1))
} else {
// Here we compute the back groupings in reverse so as to use the same iteration as in front groupings
val fromBack = nonMaxDigits.init.flatMap {
case (lastImportantDigit, unimportantDigits) =>
val fixedDigits = digits.dropRight(unimportantDigits + 1)
0.until(lastImportantDigit)
.reverse
.toVector
.map { lastDigit =>
fixedDigits :+ lastDigit
}
}
fromBack.reverse :+ nonMaxDigits.map(_._1).reverse // Add Endpoint
}
}
/** Computes the middle groupings in the CETCompression (without total optimization).
* This means the resulting outcomes cover
* [(prefix, firstDigitStart + 1, 0, ..., 0), (prefix, firstDigitEnd-1, base-1, ..., base-1)].
*
* @param firstDigitStart The first unique digit of the range's start
* @param firstDigitEnd The first unique digit of the range's end
*/
def middleGrouping(
firstDigitStart: Int,
firstDigitEnd: Int): Vector[Vector[Int]] = {
(firstDigitStart + 1).until(firstDigitEnd).toVector.map { firstDigit =>
Vector(firstDigit)
}
}
/** Splits off the shared prefix of start and end represented in the given base
* and returns the shared prefix and the unique digits of start and of end.
*/
def separatePrefix(
start: Long,
end: Long,
base: Int,
numDigits: Int): (Vector[Int], Vector[Int], Vector[Int]) = {
val startDigits = NumberUtil.decompose(start, base, numDigits)
val endDigits = NumberUtil.decompose(end, base, numDigits)
val prefixDigits = startDigits
.zip(endDigits)
.takeWhile { case (startDigit, endDigit) => startDigit == endDigit }
.map(_._1)
(prefixDigits,
startDigits.drop(prefixDigits.length),
endDigits.drop(prefixDigits.length))
}
/** Runs the compression algorithm with all optimizations on the interval [start, end]
* represented in the given base.
*/
def groupByIgnoringDigits(
start: Long,
end: Long,
base: Int,
numDigits: Int): Vector[Vector[Int]] = {
val (prefixDigits, startDigits, endDigits) =
separatePrefix(start, end, base, numDigits)
if (start == end) { // Special Case: Range Length 1
Vector(prefixDigits)
} else if (startDigits.forall(_ == 0) && endDigits.forall(_ == base - 1)) { // Total Optimization
Vector(prefixDigits)
} else if (prefixDigits.length == numDigits - 1) { // Special Case: Front Grouping = Back Grouping
startDigits.last.to(endDigits.last).toVector.map { lastDigit =>
prefixDigits :+ lastDigit
}
} else {
val front = frontGroupings(startDigits, base)
val middle = middleGrouping(startDigits.head, endDigits.head)
val back = backGroupings(endDigits, base)
val groupings = front ++ middle ++ back
groupings.map { digits =>
prefixDigits ++ digits
}
}
}
/** Computes the compressed set of outcomes and their corresponding payouts given
* a base, the number of digits to be signed, the payout function, totalCollateral
* and the range of outcomes to construct CETs for, [min, max].
*/
def computeCETs(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals,
min: Long,
max: Long): Vector[(Vector[Int], Satoshis)] = {
val ranges = splitIntoRanges(min, max, totalCollateral, function, rounding)
ranges.flatMap { range =>
range match {
case StartZero(indexFrom, indexTo) =>
groupByIgnoringDigits(indexFrom, indexTo, base, numDigits).map(
_ -> Satoshis.zero)
case StartTotal(indexFrom, indexTo) =>
groupByIgnoringDigits(indexFrom, indexTo, base, numDigits).map(
_ -> totalCollateral)
case StartFuncConst(indexFrom, indexTo) =>
groupByIgnoringDigits(indexFrom, indexTo, base, numDigits).map(
_ -> function(indexFrom, rounding))
case StartFunc(indexFrom, indexTo) =>
indexFrom.to(indexTo).map { num =>
NumberUtil.decompose(num, base, numDigits) -> function(num)
}
}
}
}
/** Computes the compressed set of outcomes and their corresponding payouts given
* a base, the number of digits to be signed, the payout function, and totalCollateral.
*/
def computeCETs(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals): Vector[(Vector[Int], Satoshis)] = {
val min = 0
val max = Math.pow(base, numDigits).toLong - 1
computeCETs(base, numDigits, function, totalCollateral, rounding, min, max)
}
}

View file

@ -0,0 +1,151 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.core.number.{UInt16, UInt32}
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.FundingInputV0TLV
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder.DualFundingInput
import org.bitcoins.core.wallet.utxo.{InputInfo, ScriptSignatureParams}
sealed trait DLCFundingInput {
def prevTx: Transaction
def prevTxVout: UInt32
def sequence: UInt32
def maxWitnessLen: UInt16
def redeemScriptOpt: Option[WitnessScriptPubKey]
def scriptSignature: ScriptSignature = {
redeemScriptOpt match {
case Some(redeemScript) => P2SHScriptSignature(redeemScript)
case None => EmptyScriptSignature
}
}
lazy val output: TransactionOutput = prevTx.outputs(prevTxVout.toInt)
lazy val outPoint: TransactionOutPoint =
TransactionOutPoint(prevTx.txId, prevTxVout)
lazy val input: TransactionInput = {
val scriptSig = redeemScriptOpt match {
case Some(redeemScript) => P2SHScriptSignature(redeemScript)
case None => EmptyScriptSignature
}
TransactionInput(outPoint, scriptSig, sequence)
}
lazy val outputReference: OutputReference = OutputReference(outPoint, output)
lazy val toTLV: FundingInputV0TLV = {
FundingInputV0TLV(
prevTx,
prevTxVout,
sequence,
maxWitnessLen,
redeemScriptOpt
)
}
lazy val toDualFundingInput: DualFundingInput =
DualFundingInput(scriptSignature, maxWitnessLen.toInt)
}
object DLCFundingInput {
def apply(
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
maxWitnessLen: UInt16,
redeemScriptOpt: Option[WitnessScriptPubKey]): DLCFundingInput = {
prevTx.outputs(prevTxVout.toInt).scriptPubKey match {
case _: P2SHScriptPubKey =>
redeemScriptOpt match {
case Some(redeemScript) =>
DLCFundingInputP2SHSegwit(prevTx,
prevTxVout,
sequence,
maxWitnessLen,
redeemScript)
case None =>
throw new IllegalArgumentException(
"P2SH input requires a redeem script")
}
case _: P2WPKHWitnessSPKV0 =>
require(
maxWitnessLen == UInt16(107) || maxWitnessLen == UInt16(108),
s"P2WPKH max witness length must be 107 or 108, got $maxWitnessLen")
DLCFundingInputP2WPKHV0(prevTx, prevTxVout, sequence)
case _: P2WSHWitnessSPKV0 =>
DLCFundingInputP2WSHV0(prevTx, prevTxVout, sequence, maxWitnessLen)
case spk: UnassignedWitnessScriptPubKey =>
throw new IllegalArgumentException(s"Unknown segwit version: $spk")
case spk: RawScriptPubKey =>
throw new IllegalArgumentException(s"Segwit input required: $spk")
}
}
def fromTLV(fundingInput: FundingInputV0TLV): DLCFundingInput = {
DLCFundingInput(
fundingInput.prevTx,
fundingInput.prevTxVout,
fundingInput.sequence,
fundingInput.maxWitnessLen,
fundingInput.redeemScriptOpt
)
}
def fromInputSigningInfo(
info: ScriptSignatureParams[InputInfo],
sequence: UInt32 = TransactionConstants.sequence): DLCFundingInput = {
DLCFundingInput(
info.prevTransaction,
info.outPoint.vout,
sequence,
maxWitnessLen = UInt16(info.maxWitnessLen),
InputInfo
.getRedeemScript(info.inputInfo)
.asInstanceOf[Option[WitnessScriptPubKey]]
)
}
}
case class DLCFundingInputP2WPKHV0(
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32)
extends DLCFundingInput {
require(output.scriptPubKey.isInstanceOf[P2WPKHWitnessSPKV0],
s"Funding input not P2WPKH: ${output.scriptPubKey}")
override val maxWitnessLen: UInt16 = UInt16(107)
override val redeemScriptOpt: Option[WitnessScriptPubKey] = None
}
case class DLCFundingInputP2WSHV0(
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
maxWitnessLen: UInt16)
extends DLCFundingInput {
require(output.scriptPubKey.isInstanceOf[P2WSHWitnessSPKV0],
s"Funding input not P2WSH: ${output.scriptPubKey}")
override val redeemScriptOpt: Option[WitnessScriptPubKey] = None
}
case class DLCFundingInputP2SHSegwit(
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
maxWitnessLen: UInt16,
redeemScript: WitnessScriptPubKey)
extends DLCFundingInput {
require(
output.scriptPubKey == P2SHScriptPubKey(redeemScript),
s"Funding input not correct P2SH: ${output.scriptPubKey}; expected ${P2SHScriptPubKey(redeemScript)}"
)
override val redeemScriptOpt: Option[WitnessScriptPubKey] = Some(redeemScript)
}

View file

@ -1,56 +1,26 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.crypto.{ExtPrivateKey, ExtPublicKey}
import org.bitcoins.core.hd.BIP32Path
import org.bitcoins.core.protocol.script.P2WPKHWitnessSPKV0
import org.bitcoins.core.protocol.{Bech32Address, BitcoinAddress}
import org.bitcoins.crypto.ECPublicKey
import org.bitcoins.crypto.{ECPrivateKey, ECPublicKey}
case class DLCPublicKeys(
fundingKey: ECPublicKey,
toLocalCETKey: ECPublicKey,
finalAddress: BitcoinAddress) {
require(
fundingKey != toLocalCETKey,
s"Cannot use same key for fundingKey and toLocalCETKey, got $fundingKey")
}
case class DLCPublicKeys(fundingKey: ECPublicKey, payoutAddress: BitcoinAddress)
object DLCPublicKeys {
def fromExtPubKeyAndIndex(
extPubKey: ExtPublicKey,
nextAddressIndex: Int,
def fromPrivKeys(
fundingPrivKey: ECPrivateKey,
payoutKey: ECPrivateKey,
network: BitcoinNetwork): DLCPublicKeys = {
val fundingPubKey: ECPublicKey =
extPubKey
.deriveChildPubKey(BIP32Path.fromString(s"m/0/$nextAddressIndex"))
.get
.key
val cetToLocalPubKey: ECPublicKey =
extPubKey
.deriveChildPubKey(BIP32Path.fromString(s"m/0/${nextAddressIndex + 1}"))
.get
.key
val finalPubKey: ECPublicKey =
extPubKey
.deriveChildPubKey(BIP32Path.fromString(s"m/0/${nextAddressIndex + 2}"))
.get
.key
DLCPublicKeys(
fundingKey = fundingPubKey,
toLocalCETKey = cetToLocalPubKey,
finalAddress = Bech32Address(P2WPKHWitnessSPKV0(finalPubKey), network)
)
fromPubKeys(fundingPrivKey.publicKey, payoutKey.publicKey, network)
}
def fromExtPrivKeyAndIndex(
extPrivKey: ExtPrivateKey,
nextAddressIndex: Int,
def fromPubKeys(
fundingKey: ECPublicKey,
payoutKey: ECPublicKey,
network: BitcoinNetwork): DLCPublicKeys = {
fromExtPubKeyAndIndex(extPrivKey.extPublicKey, nextAddressIndex, network)
DLCPublicKeys(fundingKey,
Bech32Address(P2WPKHWitnessSPKV0(payoutKey), network))
}
}

View file

@ -1,23 +1,51 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.core.protocol.script.ScriptWitnessV0
import org.bitcoins.core.protocol.tlv.{DLCOutcomeType, FundingSignaturesV0TLV}
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.util.MapWrapper
import org.bitcoins.crypto.Sha256DigestBE
import org.bitcoins.core.util.SeqWrapper
import org.bitcoins.crypto.ECAdaptorSignature
sealed trait DLCSignatures
case class FundingSignatures(
sigs: Map[TransactionOutPoint, Vector[PartialSignature]])
extends MapWrapper[TransactionOutPoint, Vector[PartialSignature]]
sigs: Vector[(TransactionOutPoint, ScriptWitnessV0)])
extends SeqWrapper[(TransactionOutPoint, ScriptWitnessV0)]
with DLCSignatures {
override protected def wrapped: Map[
TransactionOutPoint,
Vector[PartialSignature]] = sigs
override protected def wrapped: Vector[
(TransactionOutPoint, ScriptWitnessV0)] = sigs
def get(outPoint: TransactionOutPoint): Option[ScriptWitnessV0] = {
sigs.find(_._1 == outPoint).map(_._2)
}
def apply(outPoint: TransactionOutPoint): ScriptWitnessV0 = {
get(outPoint).get
}
def merge(other: FundingSignatures): FundingSignatures = {
FundingSignatures(sigs ++ other.sigs)
}
def toTLV: FundingSignaturesV0TLV = {
FundingSignaturesV0TLV(sigs.map(_._2))
}
}
case class CETSignatures(
outcomeSigs: Map[Sha256DigestBE, PartialSignature],
outcomeSigs: Vector[(DLCOutcomeType, ECAdaptorSignature)],
refundSig: PartialSignature)
extends DLCSignatures
extends DLCSignatures {
lazy val keys: Vector[DLCOutcomeType] = outcomeSigs.map(_._1)
lazy val adaptorSigs: Vector[ECAdaptorSignature] = outcomeSigs.map(_._2)
def apply(key: DLCOutcomeType): ECAdaptorSignature = {
outcomeSigs
.find(_._1 == key)
.map(_._2)
.getOrElse(
throw new IllegalArgumentException(s"No signature found for $key"))
}
}

View file

@ -0,0 +1,69 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.crypto.StringFactory
sealed abstract class DLCState
object DLCState extends StringFactory[DLCState] {
/** The state where an offer has been created but no
* accept message has yet been created/received.
*/
final case object Offered extends DLCState
/** The state where an offer has been accepted but
* no sign message has yet been created/received.
*/
final case object Accepted extends DLCState
/** The state where the initiating party has created
* a sign message in response to an accept message
* but the DLC funding transaction has not yet been
* broadcasted to the network.
*/
final case object Signed extends DLCState
/** The state where the accepting (non-initiating)
* party has broadcasted the DLC funding transaction
* to the blockchain, and it has not yet been confirmed.
*/
final case object Broadcasted extends DLCState
/** The state where the DLC funding transaction has been
* confirmed on-chain and no execution paths have yet been
* initiated.
*/
final case object Confirmed extends DLCState
/** The state where one of the CETs has been accepted by the network
* and executed by ourselves.
*/
final case object Claimed extends DLCState
/** The state where one of the CETs has been accepted by the network
* and executed by a remote party.
*/
final case object RemoteClaimed extends DLCState
/** The state where the DLC refund transaction has been
* accepted by the network.
*/
final case object Refunded extends DLCState
val all: Vector[DLCState] = Vector(Offered,
Accepted,
Signed,
Broadcasted,
Confirmed,
Claimed,
RemoteClaimed,
Refunded)
def fromString(str: String): DLCState = {
all.find(state => str.toLowerCase() == state.toString.toLowerCase) match {
case Some(state) => state
case None =>
throw new IllegalArgumentException(s"$str is not a valid DLCState")
}
}
}

View file

@ -6,12 +6,10 @@ import org.bitcoins.core.protocol.BlockTimeStamp
import org.bitcoins.crypto.{Factory, NetworkElement}
import scodec.bits.ByteVector
/** @param penaltyTimeout The CSV timeout in blocks used in all CETs
* @param contractMaturity The CLTV in milliseconds when a signature is expected
/** @param contractMaturity The CLTV in milliseconds when a signature is expected
* @param contractTimeout The CLTV timeout in milliseconds after which the refund tx is valid
*/
case class DLCTimeouts(
penaltyTimeout: UInt32,
contractMaturity: BlockTimeStamp,
contractTimeout: BlockTimeStamp
) extends NetworkElement {
@ -28,23 +26,21 @@ case class DLCTimeouts(
}
override def bytes: ByteVector = {
penaltyTimeout.bytes ++ contractMaturity.toUInt32.bytes ++ contractTimeout.toUInt32.bytes
contractMaturity.toUInt32.bytes ++ contractTimeout.toUInt32.bytes
}
}
object DLCTimeouts extends Factory[DLCTimeouts] {
/** The default CSV timeout in blocks used in all CETs */
final val DEFAULT_PENALTY_TIMEOUT: UInt32 = UInt32(5)
override def fromBytes(bytes: ByteVector): DLCTimeouts = {
require(bytes.size == 12, s"A DLCTimeouts is exactly 12 bytes, got $bytes")
require(bytes.size == 8, s"A DLCTimeouts is exactly 12 bytes, got $bytes")
val (maturityBytes, timeoutBytes) = bytes.splitAt(4)
val penalty = UInt32(bytes.take(4))
val contractMaturity =
BlockTimeStamp.fromUInt32(UInt32(bytes.slice(4, 8)))
BlockTimeStamp.fromUInt32(UInt32(maturityBytes))
val contractTimeout =
BlockTimeStamp.fromUInt32(UInt32(bytes.takeRight(4)))
DLCTimeouts(penalty, contractMaturity, contractTimeout)
BlockTimeStamp.fromUInt32(UInt32(timeoutBytes))
DLCTimeouts(contractMaturity, contractTimeout)
}
}

View file

@ -0,0 +1,283 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.util.NumberUtil
import scala.math.BigDecimal.RoundingMode
/** A DLC payout curve defined by piecewise interpolating points */
case class OutcomeValueFunction(points: Vector[OutcomeValuePoint]) {
require(points.init.zip(points.tail).forall {
case (p1, p2) => p1.outcome < p2.outcome
},
s"Points must be ascending: $points")
/** These points (and their indices in this.points) represent the endpoints
* between which interpolation happens.
* In other words these endpoints define the pieces of the piecewise function.
*/
lazy val endpoints: Vector[(OutcomeValuePoint, Int)] =
points.zipWithIndex.filter(_._1.isEndpoint)
/** This Vector contains the function pieces between the endpoints */
lazy val functionComponents: Vector[OutcomeValueFunctionComponent] = {
endpoints.init.zip(endpoints.tail).map { // All pairs of adjacent endpoints
case ((_, index), (_, nextIndex)) =>
OutcomeValueFunctionComponent(points.slice(index, nextIndex + 1))
}
}
private lazy val outcomes = endpoints.map(_._1.outcome)
/** Returns the function component on which the given oracle outcome is
* defined, along with its index
*/
def componentFor(
outcome: BigDecimal): (OutcomeValueFunctionComponent, Int) = {
val endpointIndex = NumberUtil.search(outcomes, outcome)
val (endpoint, _) = endpoints(endpointIndex)
if (
endpoint.outcome == outcome && endpointIndex != functionComponents.length
) {
(functionComponents(endpointIndex), endpointIndex)
} else {
(functionComponents(endpointIndex - 1), endpointIndex - 1)
}
}
def getPayout(outcome: BigDecimal): Satoshis = {
val (func, _) = componentFor(outcome)
func(outcome)
}
def getPayout(outcome: BigDecimal, rounding: RoundingIntervals): Satoshis = {
val (func, _) = componentFor(outcome)
func(outcome, rounding)
}
def apply(outcome: BigDecimal): Satoshis = getPayout(outcome)
def apply(outcome: BigDecimal, rounding: RoundingIntervals): Satoshis =
getPayout(outcome, rounding)
}
/** A point on a DLC payout curve to be used for interpolation
*
* @param outcome An element of the domain of possible events signed by the oracle
* @param value The payout to the local party corresponding to outcome
* @param isEndpoint True if this point defines a boundary between pieces in the curve
*/
case class OutcomeValuePoint(
outcome: BigDecimal,
value: Satoshis,
isEndpoint: Boolean)
/** A single piece of a larger piecewise function defined between left and right endpoints */
sealed trait OutcomeValueFunctionComponent {
def leftEndpoint: OutcomeValuePoint
def midpoints: Vector[OutcomeValuePoint]
def rightEndpoint: OutcomeValuePoint
require(leftEndpoint.isEndpoint, s"$leftEndpoint not an endpoint")
require(rightEndpoint.isEndpoint, s"$rightEndpoint not an endpoint")
require(midpoints.forall(!_.isEndpoint), s"$midpoints contained an endpoint")
midpoints.headOption match {
case Some(firstMidpoint) =>
require(leftEndpoint.outcome < firstMidpoint.outcome,
s"Points must be ascending: $this")
require(midpoints.init.zip(midpoints.tail).forall {
case (m1, m2) => m1.outcome < m2.outcome
},
s"Points must be ascending: $this")
require(rightEndpoint.outcome > midpoints.last.outcome,
s"Points must be ascending: $this")
case None =>
require(leftEndpoint.outcome < rightEndpoint.outcome,
s"Points must be ascending: $this")
}
def apply(outcome: BigDecimal): Satoshis
def apply(outcome: BigDecimal, rounding: RoundingIntervals): Satoshis = {
rounding.round(outcome, apply(outcome))
}
/** Returns the largest Long less than or equal to bd (floor function) */
protected def bigDecimalSats(bd: BigDecimal): Satoshis = {
Satoshis(bd.setScale(0, RoundingMode.FLOOR).toLongExact)
}
}
object OutcomeValueFunctionComponent {
def apply(
points: Vector[OutcomeValuePoint]): OutcomeValueFunctionComponent = {
points match {
case Vector(left, right) =>
if (left.value == right.value) {
OutcomeValueConstant(left, right)
} else {
OutcomeValueLine(left, right)
}
case Vector(left, mid, right) => OutcomeValueQuadratic(left, mid, right)
case Vector(left, mid1, mid2, right) =>
OutcomeValueCubic(left, mid1, mid2, right)
case _ => OutcomeValuePolynomial(points)
}
}
}
case class OutcomeValueConstant(
leftEndpoint: OutcomeValuePoint,
rightEndpoint: OutcomeValuePoint)
extends OutcomeValueFunctionComponent {
require(leftEndpoint.value == rightEndpoint.value,
"Constant function must have same values on endpoints")
override lazy val midpoints: Vector[OutcomeValuePoint] = Vector.empty
override def apply(outcome: BigDecimal): Satoshis = leftEndpoint.value
}
/** A Line between left and right endpoints defining a piece of a larger payout curve */
case class OutcomeValueLine(
leftEndpoint: OutcomeValuePoint,
rightEndpoint: OutcomeValuePoint)
extends OutcomeValueFunctionComponent {
override lazy val midpoints: Vector[OutcomeValuePoint] = Vector.empty
lazy val slope: BigDecimal = {
(rightEndpoint.value.toLong - leftEndpoint.value.toLong) / (rightEndpoint.outcome - leftEndpoint.outcome)
}
override def apply(outcome: BigDecimal): Satoshis = {
val value =
(outcome - leftEndpoint.outcome) * slope + leftEndpoint.value.toLong
bigDecimalSats(value)
}
}
/** A quadratic between left and right endpoints defining a piece of a larger payout curve.
* A quadratic equation defines a parabola: https://en.wikipedia.org/wiki/Quadratic_function
*/
case class OutcomeValueQuadratic(
leftEndpoint: OutcomeValuePoint,
midpoint: OutcomeValuePoint,
rightEndpoint: OutcomeValuePoint)
extends OutcomeValueFunctionComponent {
override lazy val midpoints: Vector[OutcomeValuePoint] = Vector(midpoint)
private lazy val (x01, x02, x12) =
(leftEndpoint.outcome - midpoint.outcome,
leftEndpoint.outcome - rightEndpoint.outcome,
midpoint.outcome - rightEndpoint.outcome)
private lazy val (x10, x20, x21) = (-x01, -x02, -x12)
private lazy val (y0, y1, y2) = (leftEndpoint.value.toLong,
midpoint.value.toLong,
rightEndpoint.value.toLong)
private lazy val (c0, c1, c2) =
(y0 / (x01 * x02), y1 / (x10 * x12), y2 / (x20 * x21))
override def apply(outcome: BigDecimal): Satoshis = {
val x0 = outcome - leftEndpoint.outcome
val x1 = outcome - midpoint.outcome
val x2 = outcome - rightEndpoint.outcome
val value = c0 * (x1 * x2) + c1 * (x0 * x2) + c2 * (x0 * x1)
bigDecimalSats(value)
}
}
/** A cubic between left and right endpoints defining a piece of a larger payout curve */
case class OutcomeValueCubic(
leftEndpoint: OutcomeValuePoint,
leftMidpoint: OutcomeValuePoint,
rightMidpoint: OutcomeValuePoint,
rightEndpoint: OutcomeValuePoint)
extends OutcomeValueFunctionComponent {
override lazy val midpoints: Vector[OutcomeValuePoint] =
Vector(leftMidpoint, rightMidpoint)
private lazy val (x01, x02, x03, x12, x13, x23) =
(leftEndpoint.outcome - leftMidpoint.outcome,
leftEndpoint.outcome - rightMidpoint.outcome,
leftEndpoint.outcome - rightEndpoint.outcome,
leftMidpoint.outcome - rightMidpoint.outcome,
leftMidpoint.outcome - rightEndpoint.outcome,
rightMidpoint.outcome - rightEndpoint.outcome)
private lazy val (x10, x20, x30, x21, x31, x32) =
(-x01, -x02, -x03, -x12, -x13, -x23)
private lazy val (y0, y1, y2, y3) = (leftEndpoint.value.toLong,
leftMidpoint.value.toLong,
rightMidpoint.value.toLong,
rightEndpoint.value.toLong)
private lazy val (c0, c1, c2, c3) =
(y0 / (x01 * x02 * x03),
y1 / (x10 * x12 * x13),
y2 / (x20 * x21 * x23),
y3 / (x30 * x31 * x32))
override def apply(outcome: BigDecimal): Satoshis = {
val x0 = outcome - leftEndpoint.outcome
val x1 = outcome - leftMidpoint.outcome
val x2 = outcome - rightMidpoint.outcome
val x3 = outcome - rightEndpoint.outcome
val value =
c0 * (x1 * x2 * x3) + c1 * (x0 * x2 * x3) + c2 * (x0 * x1 * x3) + c3 * (x0 * x1 * x2)
bigDecimalSats(value)
}
}
/** A polynomial interpolating points and defining a piece of a larger payout curve */
case class OutcomeValuePolynomial(points: Vector[OutcomeValuePoint])
extends OutcomeValueFunctionComponent {
override lazy val leftEndpoint: OutcomeValuePoint = points.head
override lazy val rightEndpoint: OutcomeValuePoint = points.last
override lazy val midpoints: Vector[OutcomeValuePoint] = points.tail.init
lazy val coefficients: Vector[BigDecimal] = {
points.map {
case OutcomeValuePoint(xi, yi, _) =>
val denom = points.foldLeft(BigDecimal(1)) {
case (prodSoFar, OutcomeValuePoint(xj, _, _)) =>
if (xj == xi) {
prodSoFar
} else {
prodSoFar * (xi - xj)
}
}
yi.toLong / denom
}
}
override def apply(outcome: BigDecimal): Satoshis = {
points.find(_.outcome == outcome) match {
case Some(point) => point.value
case None =>
val allProd = points.foldLeft(BigDecimal(1)) {
case (prodSoFar, OutcomeValuePoint(xj, _, _)) =>
prodSoFar * (outcome - xj)
}
val value = coefficients.zipWithIndex.foldLeft(BigDecimal(0)) {
case (sumSoFar, (coefficientI, i)) =>
sumSoFar + (coefficientI * allProd / (outcome - points(i).outcome))
}
bigDecimalSats(value)
}
}
}

View file

@ -0,0 +1,145 @@
package org.bitcoins.commons.jsonmodels.dlc
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.util.NumberUtil
import scala.annotation.tailrec
/** Specifies a list of intervals with corresponding rounding moduli.
* In particular, each element (outcome, roundingMod) of intervalStarts
* represents the beginning of a new interval at outcome with new modulus roundingMod.
*
* @see https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/NumericOutcome.md#rounding-intervals
*/
case class RoundingIntervals(intervalStarts: Vector[(BigDecimal, Long)]) {
if (intervalStarts.nonEmpty) {
require(intervalStarts.init.zip(intervalStarts.tail).forall {
case (i1, i2) => i1._1 < i2._1
},
s"Intervals must be ascending: $intervalStarts")
}
/** Returns the rounding interval (start, end, mod) containing the given outcome */
def intervalContaining(
outcome: BigDecimal): (BigDecimal, BigDecimal, Long) = {
// Using Long.MaxValue guarantees that index will point to index of right endpoint of interval
val index = NumberUtil.search(intervalStarts, (outcome, Long.MaxValue)) - 1
if (index == -1) {
val firstIntervalChange =
intervalStarts.map(_._1).headOption.getOrElse(BigDecimal(Long.MaxValue))
(Long.MinValue, firstIntervalChange, 1L)
} else if (index == intervalStarts.length - 1) {
val (intervalStart, roundingModulus) = intervalStarts.last
(intervalStart, Long.MaxValue, roundingModulus)
} else {
val (intervalStart, roundingModulus) = intervalStarts(index)
val (intervalEnd, _) = intervalStarts(index + 1)
(intervalStart, intervalEnd, roundingModulus)
}
}
/** Returns the rounding modulus which should be used at the given outcome */
def roundingModulusAt(outcome: BigDecimal): Long = {
intervalContaining(outcome)._3
}
def round(outcome: BigDecimal, computedPayout: Satoshis): Satoshis = {
val payoutLong = computedPayout.toLong
val roundingMod = roundingModulusAt(outcome)
val mod = if (payoutLong >= 0) {
payoutLong % roundingMod
} else {
// (negative number) % _ returns the negative modulus
payoutLong % roundingMod + roundingMod
}
val roundedPayout = if (mod >= roundingMod / 2.0) {
payoutLong + roundingMod - mod
} else {
payoutLong - mod
}
Satoshis(roundedPayout)
}
/** Returns a RoundingIntervals which does the maximum amount of rounding
* allowed by both this and other.
*/
def minRoundingWith(other: RoundingIntervals): RoundingIntervals = {
val builder = Vector.newBuilder[(BigDecimal, Long)]
@tailrec
def minMerge(
thisIntervals: Vector[(BigDecimal, Long)],
thisCurrentMod: Long,
otherIntervals: Vector[(BigDecimal, Long)],
otherCurrentMod: Long): Unit = {
if (thisIntervals.isEmpty) {
val otherEnd = otherIntervals.map {
case (startRange, otherMod) =>
(startRange, Math.min(thisCurrentMod, otherMod))
}
builder.++=(otherEnd)
} else if (otherIntervals.isEmpty) {
val thisEnd = thisIntervals.map {
case (startRange, thisMod) =>
(startRange, Math.min(thisMod, otherCurrentMod))
}
builder.++=(thisEnd)
} else {
val (thisNextStart, thisNextMod) = thisIntervals.head
val (otherNextStart, otherNextMod) = otherIntervals.head
if (thisNextStart < otherNextStart) {
builder.+=((thisNextStart, Math.min(thisNextMod, otherCurrentMod)))
minMerge(thisIntervals.tail,
thisNextMod,
otherIntervals,
otherCurrentMod)
} else if (thisNextStart > otherNextStart) {
builder.+=((otherNextStart, Math.min(otherNextMod, thisCurrentMod)))
minMerge(thisIntervals,
thisCurrentMod,
otherIntervals.tail,
otherNextMod)
} else {
builder.+=((thisNextStart, Math.min(thisNextMod, otherNextMod)))
minMerge(thisIntervals.tail,
thisNextMod,
otherIntervals.tail,
otherNextMod)
}
}
}
minMerge(thisIntervals = intervalStarts,
thisCurrentMod = 1L,
otherIntervals = other.intervalStarts,
otherCurrentMod = 1L)
RoundingIntervals(builder.result()).canonicalForm()
}
def canonicalForm(): RoundingIntervals = {
var currentMod: Long = 1L
val canonicalVec = intervalStarts.filter {
case (_, newMod) =>
if (newMod == currentMod) false
else {
currentMod = newMod
true
}
}
RoundingIntervals(canonicalVec)
}
}
object RoundingIntervals {
val noRounding: RoundingIntervals = RoundingIntervals(Vector.empty)
}

View file

@ -1,7 +1,5 @@
package org.bitcoins.commons.serializers
import java.time.Instant
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
@ -16,11 +14,20 @@ import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import upickle.default._
import java.io.File
import java.nio.file.Path
import java.time.Instant
object Picklers {
import org.bitcoins.crypto.DoubleSha256DigestBE
implicit val pathPickler: ReadWriter[Path] =
readwriter[String].bimap(_.toString, str => new File(str).toPath)
implicit val byteVectorPickler: ReadWriter[ByteVector] =
readwriter[String].bimap(_.toHex, str => ByteVector.fromValidHex(str))
implicit val bitcoinAddressPickler: ReadWriter[BitcoinAddress] =
readwriter[String]
@ -63,6 +70,9 @@ object Picklers {
implicit val sha256DigestBEPickler: ReadWriter[Sha256DigestBE] =
readwriter[String].bimap(_.hex, Sha256DigestBE.fromHex)
implicit val sha256DigestPickler: ReadWriter[Sha256Digest] =
readwriter[String].bimap(_.hex, Sha256Digest.fromHex)
implicit val doubleSha256DigestBEPickler: ReadWriter[DoubleSha256DigestBE] =
readwriter[String].bimap(_.hex, DoubleSha256DigestBE.fromHex)
@ -77,9 +87,15 @@ object Picklers {
implicit val oracleInfoPickler: ReadWriter[OracleInfo] =
readwriter[String].bimap(_.hex, OracleInfo.fromHex)
implicit val oracleAnnouncementPickler: ReadWriter[OracleAnnouncementTLV] =
readwriter[String].bimap(_.hex, OracleAnnouncementTLV.fromHex)
implicit val contractInfoPickler: ReadWriter[ContractInfo] =
readwriter[String].bimap(_.hex, ContractInfo.fromHex)
implicit val contractInfoTLVPickler: ReadWriter[ContractInfoTLV] =
readwriter[String].bimap(_.hex, ContractInfoTLV.fromHex)
implicit val schnorrDigitalSignaturePickler: ReadWriter[
SchnorrDigitalSignature] =
readwriter[String].bimap(_.hex, SchnorrDigitalSignature.fromHex)
@ -87,22 +103,24 @@ object Picklers {
implicit val partialSignaturePickler: ReadWriter[PartialSignature] =
readwriter[String].bimap(_.hex, PartialSignature.fromHex)
implicit val dlcOfferPickler: ReadWriter[DLCOffer] =
readwriter[String]
.bimap(_.toJsonStr, str => DLCOffer.fromJson(ujson.read(str)))
implicit val dlcOfferTLVPickler: ReadWriter[DLCOfferTLV] =
readwriter[String].bimap(_.hex, DLCOfferTLV.fromHex)
implicit val dlcAcceptPickler: ReadWriter[DLCAccept] =
readwriter[String]
.bimap(_.toJsonStr, str => DLCAccept.fromJson(ujson.read(str).obj))
implicit val lnMessageDLCOfferTLVPickler: ReadWriter[LnMessage[DLCOfferTLV]] =
readwriter[String].bimap(_.hex, LnMessageFactory(DLCOfferTLV).fromHex)
implicit val dlcSignPickler: ReadWriter[DLCSign] =
readwriter[String]
.bimap(_.toJsonStr, str => DLCSign.fromJson(ujson.read(str).obj))
implicit val dlcAcceptTLVPickler: ReadWriter[DLCAcceptTLV] =
readwriter[String].bimap(_.hex, DLCAcceptTLV.fromHex)
implicit val dlcMutualCloseSigPickler: ReadWriter[DLCMutualCloseSig] =
readwriter[String].bimap(
_.toJsonStr,
str => DLCMutualCloseSig.fromJson(ujson.read(str).obj))
implicit val lnMessageDLCAcceptTLVPickler: ReadWriter[
LnMessage[DLCAcceptTLV]] =
readwriter[String].bimap(_.hex, LnMessageFactory(DLCAcceptTLV).fromHex)
implicit val dlcSignTLVPickler: ReadWriter[DLCSignTLV] =
readwriter[String].bimap(_.hex, DLCSignTLV.fromHex)
implicit val lnMessageDLCSignTLVPickler: ReadWriter[LnMessage[DLCSignTLV]] =
readwriter[String].bimap(_.hex, LnMessageFactory(DLCSignTLV).fromHex)
implicit val blockStampPickler: ReadWriter[BlockStamp] =
readwriter[String].bimap(_.mkString, BlockStamp.fromString)
@ -114,13 +132,13 @@ object Picklers {
readwriter[String].bimap(_.hex, Transaction.fromHex)
implicit val extPubKeyPickler: ReadWriter[ExtPublicKey] =
readwriter[String].bimap(_.toString, ExtPublicKey.fromString(_))
readwriter[String].bimap(_.toString, ExtPublicKey.fromString)
implicit val transactionOutPointPickler: ReadWriter[TransactionOutPoint] =
readwriter[String].bimap(_.hex, TransactionOutPoint.fromHex)
implicit val coinSelectionAlgoPickler: ReadWriter[CoinSelectionAlgo] =
readwriter[String].bimap(_.toString, CoinSelectionAlgo.fromString(_))
readwriter[String].bimap(_.toString, CoinSelectionAlgo.fromString)
implicit val addressLabelTagPickler: ReadWriter[AddressLabelTag] =
readwriter[String].bimap(_.name, AddressLabelTag)

View file

@ -1,5 +1,7 @@
package org.bitcoins.cli
import java.io.File
import java.nio.file.Path
import java.time.{Instant, ZoneId, ZonedDateTime}
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
@ -16,17 +18,19 @@ import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.{
AesPassword,
SchnorrDigitalSignature,
SchnorrNonce,
Sha256DigestBE
}
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import scopt._
/** scopt readers for parsing CLI params and options */
object CliReaders {
implicit val pathReads: Read[Path] = new Read[Path] {
val arity = 1
val reads: String => Path = str => new File(str).toPath
}
implicit val npReads: Read[NetworkParameters] =
new Read[NetworkParameters] {
val arity: Int = 1
@ -45,6 +49,13 @@ object CliReaders {
}
}
implicit val byteVectorReads: Read[ByteVector] = new Read[ByteVector] {
override def arity: Int = 1
override def reads: String => ByteVector =
str => ByteVector.fromValidHex(str)
}
implicit val schnorrNonceReads: Read[SchnorrNonce] =
new Read[SchnorrNonce] {
override def arity: Int = 1
@ -145,31 +156,42 @@ object CliReaders {
val reads: String => OracleInfo = OracleInfo.fromHex
}
implicit val oracleAnnouncementReads: Read[OracleAnnouncementTLV] =
new Read[OracleAnnouncementTLV] {
val arity: Int = 1
val reads: String => OracleAnnouncementTLV = OracleAnnouncementTLV.fromHex
}
implicit val contractInfoReads: Read[ContractInfo] =
new Read[ContractInfo] {
val arity: Int = 1
val reads: String => ContractInfo = ContractInfo.fromHex
}
implicit val contractInfoTLVReads: Read[ContractInfoTLV] =
new Read[ContractInfoTLV] {
val arity: Int = 1
val reads: String => ContractInfoTLV = ContractInfoTLV.fromHex
}
implicit val blockStampReads: Read[BlockStamp] =
new Read[BlockStamp] {
val arity: Int = 1
private val dateRe = """(\d4)-(\d2)-(\d2)""".r
val reads: String => BlockStamp = str =>
str match {
case dateRe(year, month, day) =>
val time = ZonedDateTime.of(year.toInt,
month.toInt,
day.toInt,
0,
0,
0,
0,
ZoneId.of("UTC"))
BlockTime(time)
case _ => BlockStamp.fromString(str)
}
val reads: String => BlockStamp = {
case dateRe(year, month, day) =>
val time = ZonedDateTime.of(year.toInt,
month.toInt,
day.toInt,
0,
0,
0,
0,
ZoneId.of("UTC"))
BlockTime(time)
case str => BlockStamp.fromString(str)
}
}
implicit val psbtReads: Read[PSBT] =
@ -197,7 +219,7 @@ object CliReaders {
val arity: Int = 1
val reads: String => CoinSelectionAlgo =
CoinSelectionAlgo.fromString(_)
CoinSelectionAlgo.fromString
}
implicit val schnorrSigReads: Read[SchnorrDigitalSignature] =
@ -205,7 +227,7 @@ object CliReaders {
override def arity: Int = 1
override def reads: String => SchnorrDigitalSignature =
SchnorrDigitalSignature.fromHex
str => SchnorrDigitalSignature.fromHex(str.trim)
}
implicit val partialSigReads: Read[PartialSignature] =
@ -239,41 +261,52 @@ object CliReaders {
LockUnspentOutputParameter.fromJsonString
}
implicit val dlcOfferReads: Read[DLCOffer] = new Read[DLCOffer] {
implicit val sha256DigestReads: Read[Sha256Digest] =
new Read[Sha256Digest] {
val arity: Int = 1
val reads: String => Sha256Digest = Sha256Digest.fromHex
}
implicit val dlcOfferTLVReads: Read[DLCOfferTLV] = new Read[DLCOfferTLV] {
override def arity: Int = 1
// this will be a JSON string
override def reads: String => DLCOffer =
str => {
DLCOffer.fromJson(ujson.read(str))
}
override def reads: String => DLCOfferTLV = DLCOfferTLV.fromHex
}
implicit val dlcAcceptReads: Read[DLCAccept] = new Read[DLCAccept] {
override def arity: Int = 1
// this will be a JSON string
override def reads: String => DLCAccept =
str => {
DLCAccept.fromJson(ujson.read(str))
}
}
implicit val dlcSignReads: Read[DLCSign] = new Read[DLCSign] {
override def arity: Int = 1
// this will be a JSON string
override def reads: String => DLCSign =
str => {
DLCSign.fromJson(ujson.read(str))
}
}
implicit val dlcMutualCloseSigReads: Read[DLCMutualCloseSig] =
new Read[DLCMutualCloseSig] {
implicit val lnMessageDLCOfferTLVReads: Read[LnMessage[DLCOfferTLV]] =
new Read[LnMessage[DLCOfferTLV]] {
override def arity: Int = 1
override def reads: String => DLCMutualCloseSig =
str => DLCMutualCloseSig.fromJson(ujson.read(str))
override def reads: String => LnMessage[DLCOfferTLV] =
LnMessageFactory(DLCOfferTLV).fromHex
}
implicit val dlcAcceptTLVReads: Read[DLCAcceptTLV] = new Read[DLCAcceptTLV] {
override def arity: Int = 1
override def reads: String => DLCAcceptTLV = DLCAcceptTLV.fromHex
}
implicit val lnMessageDLCAcceptTLVReads: Read[LnMessage[DLCAcceptTLV]] =
new Read[LnMessage[DLCAcceptTLV]] {
override def arity: Int = 1
override def reads: String => LnMessage[DLCAcceptTLV] =
LnMessageFactory(DLCAcceptTLV).fromHex
}
implicit val dlcSignTLVReads: Read[DLCSignTLV] = new Read[DLCSignTLV] {
override def arity: Int = 1
override def reads: String => DLCSignTLV = DLCSignTLV.fromHex
}
implicit val lnMessageSignTLVReads: Read[LnMessage[DLCSignTLV]] =
new Read[LnMessage[DLCSignTLV]] {
override def arity: Int = 1
override def reads: String => LnMessage[DLCSignTLV] =
LnMessageFactory(DLCSignTLV).fromHex
}
}

View file

@ -1,5 +1,7 @@
package org.bitcoins.cli
import java.io.File
import java.nio.file.Path
import java.time.Instant
import org.bitcoins.cli.CliCommand._
@ -26,6 +28,7 @@ import org.bitcoins.crypto.{
SchnorrDigitalSignature,
Sha256DigestBE
}
import scodec.bits.ByteVector
import scopt.OParser
import ujson._
import upickle.{default => up}
@ -53,6 +56,9 @@ object ConsoleCli {
help('h', "help").text("Display this help message and exit"),
note(sys.props("line.separator") + "Commands:"),
note(sys.props("line.separator") + "===Blockchain ==="),
cmd("getinfo")
.action((_, conf) => conf.copy(command = GetInfo))
.text(s"Returns basic info about the current chain"),
cmd("getblockcount")
.action((_, conf) => conf.copy(command = GetBlockCount))
.text(s"Get the block height"),
@ -142,27 +148,25 @@ object ConsoleCli {
.action((_, conf) => conf.copy(command = IsEmpty))
.text("Checks if the wallet contains any data"),
cmd("createdlcoffer")
.hidden()
.action((_, conf) =>
conf.copy(
command = CreateDLCOffer(OracleInfo.dummy,
ContractInfo.empty,
command = CreateDLCOffer(OracleAnnouncementV0TLV.dummy,
ContractInfo.empty.toTLV,
Satoshis.zero,
None,
UInt32.zero,
UInt32.zero,
escaped = false)))
UInt32.zero)))
.text("Creates a DLC offer that another party can accept")
.children(
opt[OracleInfo]("oracleInfo")
arg[OracleAnnouncementTLV]("oracle")
.required()
.action((info, conf) =>
.action((oracle, conf) =>
conf.copy(command = conf.command match {
case offer: CreateDLCOffer =>
offer.copy(oracleInfo = info)
offer.copy(oracle = oracle)
case other => other
})),
opt[ContractInfo]("contractInfo")
arg[ContractInfoTLV]("contractInfo")
.required()
.action((info, conf) =>
conf.copy(command = conf.command match {
@ -170,7 +174,7 @@ object ConsoleCli {
offer.copy(contractInfo = info)
case other => other
})),
opt[Satoshis]("collateral")
arg[Satoshis]("collateral")
.required()
.action((collateral, conf) =>
conf.copy(command = conf.command match {
@ -178,7 +182,7 @@ object ConsoleCli {
offer.copy(collateral = collateral)
case other => other
})),
opt[SatoshisPerVirtualByte]("feerate")
arg[SatoshisPerVirtualByte]("feerate")
.optional()
.action((feeRate, conf) =>
conf.copy(command = conf.command match {
@ -186,7 +190,7 @@ object ConsoleCli {
offer.copy(feeRateOpt = Some(feeRate))
case other => other
})),
opt[UInt32]("locktime")
arg[UInt32]("locktime")
.required()
.action((locktime, conf) =>
conf.copy(command = conf.command match {
@ -194,72 +198,74 @@ object ConsoleCli {
offer.copy(locktime = locktime)
case other => other
})),
opt[UInt32]("refundlocktime")
arg[UInt32]("refundlocktime")
.required()
.action((refundLT, conf) =>
conf.copy(command = conf.command match {
case offer: CreateDLCOffer =>
offer.copy(refundLT = refundLT)
case other => other
})),
opt[Unit]("escaped")
.action((_, conf) =>
conf.copy(command = conf.command match {
case create: CreateDLCOffer =>
create.copy(escaped = true)
case other => other
}))
),
cmd("acceptdlcoffer")
.hidden()
.action((_, conf) =>
conf.copy(command = AcceptDLCOffer(null, escaped = false)))
.action((_, conf) => conf.copy(command = AcceptDLCOffer(null)))
.text("Accepts a DLC offer given from another party")
.children(
opt[DLCOffer]("offer")
arg[LnMessage[DLCOfferTLV]]("offer")
.required()
.action((offer, conf) =>
conf.copy(command = conf.command match {
case accept: AcceptDLCOffer =>
accept.copy(offer = offer)
case other => other
})),
opt[Unit]("escaped")
.action((_, conf) =>
}))
),
cmd("acceptdlcofferfromfile")
.action((_, conf) =>
conf.copy(command = AcceptDLCOfferFromFile(new File("").toPath)))
.text("Accepts a DLC offer given from another party")
.children(
arg[Path]("path")
.required()
.action((path, conf) =>
conf.copy(command = conf.command match {
case accept: AcceptDLCOffer =>
accept.copy(escaped = true)
case accept: AcceptDLCOfferFromFile =>
accept.copy(path = path)
case other => other
}))
),
cmd("signdlc")
.hidden()
.action((_, conf) =>
conf.copy(command = SignDLC(null, escaped = false)))
.action((_, conf) => conf.copy(command = SignDLC(null)))
.text("Signs a DLC")
.children(
opt[DLCAccept]("accept")
arg[LnMessage[DLCAcceptTLV]]("accept")
.required()
.action((accept, conf) =>
conf.copy(command = conf.command match {
case signDLC: SignDLC =>
signDLC.copy(accept = accept)
case other => other
})),
opt[Unit]("escaped")
.action((_, conf) =>
}))
),
cmd("signdlcfromfile")
.action((_, conf) =>
conf.copy(command = SignDLCFromFile(new File("").toPath)))
.text("Signs a DLC")
.children(
arg[Path]("path")
.required()
.action((path, conf) =>
conf.copy(command = conf.command match {
case signDLC: SignDLC =>
signDLC.copy(escaped = true)
case signDLC: SignDLCFromFile =>
signDLC.copy(path = path)
case other => other
}))
),
cmd("adddlcsigs")
.hidden()
.action((_, conf) => conf.copy(command = AddDLCSigs(null)))
.text("Adds DLC Signatures into the database")
.children(
opt[DLCSign]("sigs")
arg[LnMessage[DLCSignTLV]]("sigs")
.required()
.action((sigs, conf) =>
conf.copy(command = conf.command match {
@ -268,229 +274,88 @@ object ConsoleCli {
case other => other
}))
),
cmd("initdlcmutualclose")
.hidden()
cmd("adddlcsigsfromfile")
.action((_, conf) =>
conf.copy(command = InitDLCMutualClose(null, null, escaped = false)))
.text("Sign Mutual Close Tx for given oracle event")
conf.copy(command = AddDLCSigsFromFile(new File("").toPath)))
.text("Adds DLC Signatures into the database")
.children(
opt[Sha256DigestBE]("eventid")
arg[Path]("path")
.required()
.action((eventId, conf) =>
.action((path, conf) =>
conf.copy(command = conf.command match {
case initClose: InitDLCMutualClose =>
initClose.copy(eventId = eventId)
case other => other
})),
opt[SchnorrDigitalSignature]("oraclesig")
.required()
.action((sig, conf) =>
conf.copy(command = conf.command match {
case initClose: InitDLCMutualClose =>
initClose.copy(oracleSig = sig)
case other => other
})),
opt[Unit]("escaped")
.action((_, conf) =>
conf.copy(command = conf.command match {
case initClose: InitDLCMutualClose =>
initClose.copy(escaped = true)
case other => other
}))
),
cmd("acceptdlcmutualclose")
.hidden()
.action((_, conf) =>
conf.copy(command = AcceptDLCMutualClose(null, noBroadcast = false)))
.text("Sign Mutual Close Tx for given oracle event")
.children(
opt[DLCMutualCloseSig]("closesig")
.required()
.action((closeSig, conf) =>
conf.copy(command = conf.command match {
case acceptClose: AcceptDLCMutualClose =>
acceptClose.copy(mutualCloseSig = closeSig)
case other => other
})),
opt[Unit]("noBroadcast")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case acceptClose: AcceptDLCMutualClose =>
acceptClose.copy(noBroadcast = true)
case addDLCSigs: AddDLCSigsFromFile =>
addDLCSigs.copy(path = path)
case other => other
}))
),
cmd("getdlcfundingtx")
.hidden()
.action((_, conf) => conf.copy(command = GetDLCFundingTx(null)))
.text("Returns the Funding Tx corresponding to the DLC with the given eventId")
.text("Returns the Funding Tx corresponding to the DLC with the given contractId")
.children(
opt[Sha256DigestBE]("eventid")
opt[ByteVector]("contractId")
.required()
.action((eventId, conf) =>
.action((contractId, conf) =>
conf.copy(command = conf.command match {
case getDLCFundingTx: GetDLCFundingTx =>
getDLCFundingTx.copy(eventId = eventId)
getDLCFundingTx.copy(contractId = contractId)
case other => other
}))
),
cmd("broadcastdlcfundingtx")
.hidden()
.action((_, conf) => conf.copy(command = BroadcastDLCFundingTx(null)))
.text("Broadcasts the funding Tx corresponding to the DLC with the given eventId")
.text("Broadcasts the funding Tx corresponding to the DLC with the given contractId")
.children(
opt[Sha256DigestBE]("eventid")
arg[ByteVector]("contractId")
.required()
.action((eventId, conf) =>
.action((contractId, conf) =>
conf.copy(command = conf.command match {
case broadcastDLCFundingTx: BroadcastDLCFundingTx =>
broadcastDLCFundingTx.copy(eventId = eventId)
broadcastDLCFundingTx.copy(contractId = contractId)
case other => other
}))
),
cmd("executedlcunilateralclose")
.hidden()
cmd("executedlc")
.action((_, conf) =>
conf.copy(command =
ExecuteDLCUnilateralClose(null, null, noBroadcast = false)))
.text("Executes a unilateral close for the DLC with the given eventId")
ExecuteDLC(ByteVector.empty, Vector.empty, noBroadcast = false)))
.text("Executes the DLC with the given contractId")
.children(
opt[Sha256DigestBE]("eventid")
arg[ByteVector]("contractId")
.required()
.action((eventId, conf) =>
.action((contractId, conf) =>
conf.copy(command = conf.command match {
case executeDLCUnilateralClose: ExecuteDLCUnilateralClose =>
executeDLCUnilateralClose.copy(eventId = eventId)
case executeDLC: ExecuteDLC =>
executeDLC.copy(contractId = contractId)
case other => other
})),
opt[SchnorrDigitalSignature]("oraclesig")
arg[Seq[SchnorrDigitalSignature]]("oraclesigs")
.required()
.action((sig, conf) =>
.action((sigs, conf) =>
conf.copy(command = conf.command match {
case executeDLCUnilateralClose: ExecuteDLCUnilateralClose =>
executeDLCUnilateralClose.copy(oracleSig = sig)
case executeDLC: ExecuteDLC =>
executeDLC.copy(oracleSigs = sigs.toVector)
case other => other
})),
opt[Unit]("noBroadcast")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case executeDLCUnilateralClose: ExecuteDLCUnilateralClose =>
executeDLCUnilateralClose.copy(noBroadcast = true)
case other => other
}))
),
cmd("executedlcremoteunilateralclose")
.hidden()
.action((_, conf) =>
conf.copy(
command = ExecuteDLCRemoteUnilateralClose(null,
EmptyTransaction,
noBroadcast = false)))
.text("Executes a unilateral close for the DLC with the given eventId")
.children(
opt[Sha256DigestBE]("eventid")
.required()
.action((eventId, conf) =>
conf.copy(command = conf.command match {
case executeDLCRemoteUnilateralClose: ExecuteDLCRemoteUnilateralClose =>
executeDLCRemoteUnilateralClose.copy(eventId = eventId)
case other => other
})),
opt[Transaction]("forceCloseTx")
.required()
.action((cet, conf) =>
conf.copy(command = conf.command match {
case executeDLCRemoteUnilateralClose: ExecuteDLCRemoteUnilateralClose =>
executeDLCRemoteUnilateralClose.copy(cet = cet)
case other => other
})),
opt[Unit]("noBroadcast")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case executeDLCRemoteUnilateralClose: ExecuteDLCRemoteUnilateralClose =>
executeDLCRemoteUnilateralClose.copy(noBroadcast = true)
case other => other
}))
),
cmd("executedlcforceclose")
.hidden()
.action((_, conf) =>
conf.copy(
command = ExecuteDLCForceClose(null, null, noBroadcast = false)))
.text("Executes a force close for the DLC with the given eventId")
.children(
opt[Sha256DigestBE]("eventid")
.required()
.action((eventId, conf) =>
conf.copy(command = conf.command match {
case executeDLCForceClose: ExecuteDLCForceClose =>
executeDLCForceClose.copy(eventId = eventId)
case other => other
})),
opt[SchnorrDigitalSignature]("oraclesig")
.required()
.action((sig, conf) =>
conf.copy(command = conf.command match {
case executeDLCForceClose: ExecuteDLCForceClose =>
executeDLCForceClose.copy(oracleSig = sig)
case other => other
})),
opt[Unit]("noBroadcast")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case executeDLCForceClose: ExecuteDLCForceClose =>
executeDLCForceClose.copy(noBroadcast = true)
case other => other
}))
),
cmd("claimdlcremotefunds")
.hidden()
.action((_, conf) =>
conf.copy(command =
ClaimDLCRemoteFunds(null, EmptyTransaction, noBroadcast = false)))
.text("Claims the remote funds for the corresponding DLC")
.children(
opt[Sha256DigestBE]("eventid")
.required()
.action((eventId, conf) =>
conf.copy(command = conf.command match {
case claimDLCRemoteFunds: ClaimDLCRemoteFunds =>
claimDLCRemoteFunds.copy(eventId = eventId)
case other => other
})),
opt[Transaction]("forceclosetx")
.required()
.action((tx, conf) =>
conf.copy(command = conf.command match {
case claimDLCRemoteFunds: ClaimDLCRemoteFunds =>
claimDLCRemoteFunds.copy(forceCloseTx = tx)
case other => other
})),
opt[Unit]("noBroadcast")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case claimDLCRemoteFunds: ClaimDLCRemoteFunds =>
claimDLCRemoteFunds.copy(noBroadcast = true)
case executeDLC: ExecuteDLC =>
executeDLC.copy(noBroadcast = true)
case other => other
}))
),
cmd("executedlcrefund")
.hidden()
.action((_, conf) =>
conf.copy(command = ExecuteDLCRefund(null, noBroadcast = false)))
.text("Executes the Refund transaction for the given DLC")
.children(
opt[Sha256DigestBE]("eventid")
arg[ByteVector]("contractId")
.required()
.action((eventId, conf) =>
.action((contractId, conf) =>
conf.copy(command = conf.command match {
case executeDLCRefund: ExecuteDLCRefund =>
executeDLCRefund.copy(eventId = eventId)
executeDLCRefund.copy(contractId = contractId)
case other => other
})),
opt[Unit]("noBroadcast")
@ -502,38 +367,19 @@ object ConsoleCli {
case other => other
}))
),
cmd("claimdlcpenaltyfunds")
.hidden()
.action((_, conf) =>
conf.copy(command =
ClaimDLCPenaltyFunds(null, EmptyTransaction, noBroadcast = false)))
.text("Claims the penalty funds for the corresponding DLC")
.children(
opt[Sha256DigestBE]("eventid")
.required()
.action((eventId, conf) =>
conf.copy(command = conf.command match {
case claimDLCPenaltyFunds: ClaimDLCPenaltyFunds =>
claimDLCPenaltyFunds.copy(eventId = eventId)
case other => other
})),
opt[Transaction]("forceclosetx")
.required()
.action((tx, conf) =>
conf.copy(command = conf.command match {
case claimDLCPenaltyFunds: ClaimDLCPenaltyFunds =>
claimDLCPenaltyFunds.copy(forceCloseTx = tx)
case other => other
})),
opt[Unit]("noBroadcast")
.optional()
.action((_, conf) =>
conf.copy(command = conf.command match {
case claimDLCPenaltyFunds: ClaimDLCPenaltyFunds =>
claimDLCPenaltyFunds.copy(noBroadcast = true)
case other => other
}))
),
cmd("getdlcs")
.action((_, conf) => conf.copy(command = GetDLCs))
.text("Returns all dlcs in the wallet"),
cmd("getdlc")
.action((_, conf) => conf.copy(command = GetDLC(Sha256DigestBE.empty)))
.text("Gets a specific dlc in the wallet")
.children(arg[Sha256DigestBE]("paramhash")
.required()
.action((paramHash, conf) =>
conf.copy(command = conf.command match {
case _: GetDLC => GetDLC(paramHash)
case other => other
}))),
cmd("getbalance")
.action((_, conf) => conf.copy(command = GetBalance(false)))
.text("Get the wallet balance")
@ -1064,14 +910,14 @@ object ConsoleCli {
.text(s"Get oracle's staking address"),
cmd("listevents")
.action((_, conf) => conf.copy(command = ListEvents))
.text(s"Lists all oracle event TLVs"),
.text(s"Lists all event nonces"),
cmd("createevent")
.action((_, conf) =>
conf.copy(command = CreateEvent("", Instant.MIN, Seq.empty)))
.text("Registers an oracle event")
.children(
arg[String]("name")
.text("Name for this event")
arg[String]("label")
.text("Label for this event")
.required()
.action((label, conf) =>
conf.copy(command = conf.command match {
@ -1370,6 +1216,8 @@ object ConsoleCli {
}
val requestParam: RequestParam = command match {
case GetInfo =>
RequestParam("getinfo")
case GetUtxos =>
RequestParam("getutxos")
case ListReservedUtxos =>
@ -1389,70 +1237,50 @@ object ConsoleCli {
case IsEmpty =>
RequestParam("isempty")
// DLCs
case CreateDLCOffer(oracleInfo,
case GetDLCs => RequestParam("getdlcs")
case GetDLC(paramHash) =>
RequestParam("getdlc", Seq(up.writeJs(paramHash)))
case CreateDLCOffer(oracle,
contractInfo,
collateral,
feeRateOpt,
locktime,
refundLT,
escaped) =>
refundLT) =>
RequestParam(
"createdlcoffer",
Seq(
up.writeJs(oracleInfo),
up.writeJs(oracle),
up.writeJs(contractInfo),
up.writeJs(collateral),
up.writeJs(feeRateOpt),
up.writeJs(locktime),
up.writeJs(refundLT),
up.writeJs(escaped)
up.writeJs(refundLT)
)
)
case AcceptDLCOffer(offer, escaped) =>
RequestParam("acceptdlcoffer",
Seq(up.writeJs(offer), up.writeJs(escaped)))
case SignDLC(accept, escaped) =>
RequestParam("signdlc", Seq(up.writeJs(accept), up.writeJs(escaped)))
case AcceptDLCOffer(offer) =>
RequestParam("acceptdlcoffer", Seq(up.writeJs(offer)))
case AcceptDLCOfferFromFile(path) =>
RequestParam("acceptdlcofferfromfile", Seq(up.writeJs(path)))
case SignDLC(accept) =>
RequestParam("signdlc", Seq(up.writeJs(accept)))
case SignDLCFromFile(path) =>
RequestParam("signdlcfromfile", Seq(up.writeJs(path)))
case AddDLCSigs(sigs) =>
RequestParam("adddlcsigs", Seq(up.writeJs(sigs)))
case InitDLCMutualClose(eventId, oracleSig, escaped) =>
RequestParam(
"initdlcmutualclose",
Seq(up.writeJs(eventId), up.writeJs(oracleSig), up.writeJs(escaped)))
case AcceptDLCMutualClose(mutualCloseSig, noBroadcast) =>
RequestParam("acceptdlcmutualclose",
Seq(up.writeJs(mutualCloseSig), up.writeJs(noBroadcast)))
case ExecuteDLCUnilateralClose(eventId, oracleSig, noBroadcast) =>
RequestParam("executedlcunilateralclose",
Seq(up.writeJs(eventId),
up.writeJs(oracleSig),
case AddDLCSigsFromFile(path) =>
RequestParam("adddlcsigsfromfile", Seq(up.writeJs(path)))
case ExecuteDLC(contractId, oracleSigs, noBroadcast) =>
RequestParam("executedlc",
Seq(up.writeJs(contractId),
up.writeJs(oracleSigs),
up.writeJs(noBroadcast)))
case ExecuteDLCRemoteUnilateralClose(eventId, cet, noBroadcast) =>
RequestParam(
"executedlcremoteunilateralclose",
Seq(up.writeJs(eventId), up.writeJs(cet), up.writeJs(noBroadcast)))
case GetDLCFundingTx(eventId) =>
RequestParam("getdlcfundingtx", Seq(up.writeJs(eventId)))
case BroadcastDLCFundingTx(eventId) =>
RequestParam("broadcastdlcfundingtx", Seq(up.writeJs(eventId)))
case ExecuteDLCForceClose(eventId, oracleSig, noBroadcast) =>
RequestParam("executedlcforceclose",
Seq(up.writeJs(eventId),
up.writeJs(oracleSig),
up.writeJs(noBroadcast)))
case ClaimDLCRemoteFunds(eventId, forceCloseTx, noBroadcast) =>
RequestParam("claimdlcremotefunds",
Seq(up.writeJs(eventId),
up.writeJs(forceCloseTx),
up.writeJs(noBroadcast)))
case ExecuteDLCRefund(eventId, noBroadcast) =>
case GetDLCFundingTx(contractId) =>
RequestParam("getdlcfundingtx", Seq(up.writeJs(contractId)))
case BroadcastDLCFundingTx(contractId) =>
RequestParam("broadcastdlcfundingtx", Seq(up.writeJs(contractId)))
case ExecuteDLCRefund(contractId, noBroadcast) =>
RequestParam("executedlcrefund",
Seq(up.writeJs(eventId), up.writeJs(noBroadcast)))
case ClaimDLCPenaltyFunds(eventId, forceCloseTx, noBroadcast) =>
RequestParam("claimdlcpenaltyfunds",
Seq(up.writeJs(eventId),
up.writeJs(forceCloseTx),
up.writeJs(noBroadcast)))
Seq(up.writeJs(contractId), up.writeJs(noBroadcast)))
// Wallet
case GetBalance(isSats) =>
RequestParam("getbalance", Seq(up.writeJs(isSats)))
@ -1564,8 +1392,8 @@ object ConsoleCli {
RequestParam("getstakingaddress")
case ListEvents =>
RequestParam("listevents")
case GetEvent(tlv) =>
RequestParam("getevent", Seq(up.writeJs(tlv)))
case GetEvent(nonce) =>
RequestParam("getevent", Seq(up.writeJs(nonce)))
case CreateEvent(label, time, outcomes) =>
RequestParam(
"createevent",
@ -1673,12 +1501,10 @@ object ConsoleCli {
(getKey("result"), getKey("error")) match {
case (Some(result), None) =>
Success(jsValueToString(result))
case (None, None) =>
Success("")
case (None, Some(err)) =>
val msg = jsValueToString(err)
error(msg)
case (Some(_), Some(_)) =>
case (None, None) | (Some(_), Some(_)) =>
error(s"Got unexpected response: $rawBody")
}
}.flatten
@ -1704,7 +1530,7 @@ case class Config(
)
object Config {
val empty = Config()
val empty: Config = Config()
}
sealed abstract class CliCommand
@ -1712,91 +1538,59 @@ sealed abstract class CliCommand
object CliCommand {
case object NoCommand extends CliCommand
trait JsonResponse {
def escaped: Boolean
}
trait Broadcastable {
def noBroadcast: Boolean
}
case object GetInfo extends CliCommand
// DLC
case class CreateDLCOffer(
oracleInfo: OracleInfo,
contractInfo: ContractInfo,
oracle: OracleAnnouncementTLV,
contractInfo: ContractInfoTLV,
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,
refundLT: UInt32,
escaped: Boolean)
refundLT: UInt32)
extends CliCommand
with JsonResponse
case class AcceptDLCOffer(offer: DLCOffer, escaped: Boolean)
extends CliCommand
with JsonResponse
sealed trait AcceptDLCCliCommand extends CliCommand
case class SignDLC(accept: DLCAccept, escaped: Boolean)
extends CliCommand
with JsonResponse
case class AcceptDLCOffer(offer: LnMessage[DLCOfferTLV])
extends AcceptDLCCliCommand
case class AddDLCSigs(sigs: DLCSign) extends CliCommand
case class AcceptDLCOfferFromFile(path: Path) extends AcceptDLCCliCommand
case class InitDLCMutualClose(
eventId: Sha256DigestBE,
oracleSig: SchnorrDigitalSignature,
escaped: Boolean)
extends CliCommand
with JsonResponse
sealed trait SignDLCCliCommand extends CliCommand
case class AcceptDLCMutualClose(
mutualCloseSig: DLCMutualCloseSig,
case class SignDLC(accept: LnMessage[DLCAcceptTLV]) extends SignDLCCliCommand
case class SignDLCFromFile(path: Path) extends SignDLCCliCommand
sealed trait AddDLCSigsCliCommand extends CliCommand
case class AddDLCSigs(sigs: LnMessage[DLCSignTLV])
extends AddDLCSigsCliCommand
case class AddDLCSigsFromFile(path: Path) extends AddDLCSigsCliCommand
case class GetDLCFundingTx(contractId: ByteVector) extends CliCommand
case class BroadcastDLCFundingTx(contractId: ByteVector) extends CliCommand
case class ExecuteDLC(
contractId: ByteVector,
oracleSigs: Vector[SchnorrDigitalSignature],
noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case class GetDLCFundingTx(eventId: Sha256DigestBE) extends CliCommand
case class BroadcastDLCFundingTx(eventId: Sha256DigestBE) extends CliCommand
case class ExecuteDLCUnilateralClose(
eventId: Sha256DigestBE,
oracleSig: SchnorrDigitalSignature,
noBroadcast: Boolean)
case class ExecuteDLCRefund(contractId: ByteVector, noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case class ExecuteDLCRemoteUnilateralClose(
eventId: Sha256DigestBE,
cet: Transaction,
noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case class ExecuteDLCForceClose(
eventId: Sha256DigestBE,
oracleSig: SchnorrDigitalSignature,
noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case class ClaimDLCRemoteFunds(
eventId: Sha256DigestBE,
forceCloseTx: Transaction,
noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case class ExecuteDLCRefund(eventId: Sha256DigestBE, noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case class ClaimDLCPenaltyFunds(
eventId: Sha256DigestBE,
forceCloseTx: Transaction,
noBroadcast: Boolean)
extends CliCommand
with Broadcastable
case object GetDLCs extends CliCommand
case class GetDLC(paramHash: Sha256DigestBE) extends CliCommand
// Wallet
case class SendToAddress(

View file

@ -1,12 +1,15 @@
package org.bitcoins.gui
import org.bitcoins.cli.Config
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.gui.settings.Themes
import scalafx.beans.property.{DoubleProperty, StringProperty}
object GlobalData {
val currentBalance: DoubleProperty = DoubleProperty(0)
var network: BitcoinNetwork = _
val log: StringProperty = StringProperty("")
val statusText: StringProperty = StringProperty("")

View file

@ -2,6 +2,10 @@ package org.bitcoins.gui
import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.image.Image
import org.bitcoins.cli.CliCommand.GetInfo
import org.bitcoins.cli.ConsoleCli
import org.bitcoins.commons.jsonmodels.BitcoinSServerInfo
import org.bitcoins.core.config.{MainNet, RegTest, SigNet, TestNet3}
import org.bitcoins.gui.dlc.DLCPane
import org.bitcoins.gui.settings.SettingsPane
import scalafx.application.{JFXApp, Platform}
@ -13,6 +17,8 @@ import scalafx.scene.control.TabPane.TabClosingPolicy
import scalafx.scene.control._
import scalafx.scene.layout.{BorderPane, HBox, StackPane, VBox}
import scala.util.{Failure, Success}
object WalletGUI extends JFXApp {
// Catch unhandled exceptions on FX Application thread
Thread
@ -133,15 +139,40 @@ object WalletGUI extends JFXApp {
)
}
private val walletScene = new Scene {
private val walletScene = new Scene(1000, 600) {
root = rootView
stylesheets = GlobalData.currentStyleSheets
}
val info: BitcoinSServerInfo =
ConsoleCli.exec(GetInfo, GlobalData.consoleCliConfig) match {
case Failure(exception) =>
throw exception
case Success(str) =>
val json = ujson.read(str)
BitcoinSServerInfo.fromJson(json)
}
GlobalData.network = info.network
val (img, titleStr): (Image, String) = info.network match {
case MainNet =>
(new Image("/icons/bitcoin-s.png"), "Bitcoin-S Wallet")
case TestNet3 =>
(new Image("/icons/bitcoin-s-testnet.png"),
"Bitcoin-S Wallet - [testnet]")
case RegTest =>
(new Image("/icons/bitcoin-s-regtest.png"),
"Bitcoin-S Wallet - [regtest]")
case SigNet =>
(new Image("/icons/bitcoin-s-signet.png"), "Bitcoin-S Wallet - [signet]")
}
stage = new JFXApp.PrimaryStage {
title = "Bitcoin-S Wallet"
title = titleStr
scene = walletScene
icons.add(new Image("/icons/bitcoin-s.png"))
icons.add(img)
}
private val taskRunner = new TaskRunner(resultArea, glassPane)

View file

@ -1,10 +1,17 @@
package org.bitcoins.gui.dlc
import java.io.File
import java.nio.file.Files
import javafx.event.{ActionEvent, EventHandler}
import org.bitcoins.gui.{GlobalData, TaskRunner}
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.control._
import scalafx.scene.layout._
import scalafx.stage.FileChooser
import scalafx.stage.FileChooser.ExtensionFilter
import scala.util.Properties
class DLCPane(glassPane: VBox) {
@ -14,43 +21,78 @@ class DLCPane(glassPane: VBox) {
text <== GlobalData.statusText
}
private val resultArea = new TextArea {
prefHeight = 750
prefWidth = 800
private val resultTextArea = new TextArea {
editable = false
text = "Click on Offer or Accept to begin."
wrapText = true
}
private val exportButton = new Button("Export Result") {
alignmentInParent = Pos.BottomRight
onAction = _ => {
val txtFilter = new ExtensionFilter("Text Files", "*.txt")
val allExtensionFilter = new ExtensionFilter("All Files", "*")
val fileChooser = new FileChooser() {
extensionFilters.addAll(txtFilter, allExtensionFilter)
selectedExtensionFilter = txtFilter
initialDirectory = new File(Properties.userHome)
}
val selectedFile = fileChooser.showSaveDialog(null)
if (selectedFile != null) {
val bytes = resultTextArea.text.value.getBytes
Files.write(selectedFile.toPath, bytes)
()
}
}
}
private val resultArea = new BorderPane() {
padding = Insets(10)
center = resultTextArea
bottom = exportButton
}
BorderPane.setMargin(exportButton, Insets(10))
private val demoOracleArea = new TextArea {
prefHeight = 700
prefWidth = 400
editable = false
text =
"Click on Init Demo Oracle to generate example oracle and contract information"
wrapText = true
}
private val numOutcomesTF = new TextField {
promptText = "Number of Outcomes"
private val model =
new DLCPaneModel(resultTextArea, demoOracleArea)
model.setUp()
private val enumContractButton = new Button {
text = "Enum Contract"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onInitContract(true)
}
}
private val model =
new DLCPaneModel(resultArea, demoOracleArea, numOutcomesTF)
private val demoOracleButton = new Button {
text = "Init Demo Oracle"
private val numericContractButton = new Button {
text = "Numeric Contract"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onInitOracle()
override def handle(event: ActionEvent): Unit =
model.onInitContract(false)
}
}
private val oracleButtonHBox = new HBox {
children = Seq(numOutcomesTF, demoOracleButton)
alignment = Pos.Center
children = Seq(enumContractButton, numericContractButton)
spacing = 15
}
private val demoOracleVBox = new VBox {
padding = Insets(10)
children = Seq(demoOracleArea, oracleButtonHBox)
spacing = 15
}
@ -90,20 +132,6 @@ class DLCPane(glassPane: VBox) {
}
}
private val initCloseButton = new Button {
text = "Init Close"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onInitClose()
}
}
private val acceptCloseButton = new Button {
text = "Accept Close"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onAcceptClose()
}
}
private val refundButton = new Button {
text = "Refund"
onAction = new EventHandler[ActionEvent] {
@ -111,17 +139,10 @@ class DLCPane(glassPane: VBox) {
}
}
private val forceCloseButton = new Button {
text = "Force Close"
private val executeButton = new Button {
text = "Execute"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onForceClose()
}
}
private val punishButton = new Button {
text = "Punish"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onPunish()
override def handle(event: ActionEvent): Unit = model.onExecute()
}
}
@ -134,11 +155,7 @@ class DLCPane(glassPane: VBox) {
}
private val execButtonBar = new ButtonBar {
buttons = Seq(initCloseButton,
acceptCloseButton,
refundButton,
forceCloseButton,
punishButton)
buttons = Seq(refundButton, executeButton)
}
private val spaceRegion = new Region()
@ -146,7 +163,6 @@ class DLCPane(glassPane: VBox) {
private val buttonSpacer = new GridPane {
hgap = 10
prefHeight = 50
alignment = Pos.Center
add(initButtonBar, 0, 0)
@ -161,17 +177,28 @@ class DLCPane(glassPane: VBox) {
spacing = 10
}
private val tableView = new DLCTableView(model).tableView
private val textAreasAndTableViewVBox = new VBox {
children = Seq(textAreaHBox, tableView)
spacing = 10
}
val borderPane: BorderPane = new BorderPane {
top = buttonSpacer
center = textAreaHBox
center = textAreasAndTableViewVBox
bottom = statusLabel
}
resultArea.prefWidth <== (borderPane.width * 2) / 3
demoOracleVBox.prefWidth <== (borderPane.width / 3)
resultArea.prefHeight <== (borderPane.height * 2) / 3
demoOracleVBox.prefHeight <== (borderPane.height * 2) / 3
demoOracleArea.prefHeight <== demoOracleVBox.height * 0.9
spaceRegion.prefWidth <== (borderPane.width - initButtonBar.width - acceptButtonBar.width - execButtonBar.width - 100) / 2
spaceRegion2.prefWidth <== spaceRegion.prefWidth
tableView.prefHeight <== borderPane.height / 3
private val taskRunner = new TaskRunner(buttonSpacer, glassPane)
model.taskRunner = taskRunner

View file

@ -1,20 +1,23 @@
package org.bitcoins.gui.dlc
import org.bitcoins.cli.{CliCommand, ConsoleCli}
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.OracleInfo
import org.bitcoins.crypto.ECPrivateKey
import org.bitcoins.cli.CliCommand._
import org.bitcoins.cli.{CliCommand, Config, ConsoleCli}
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage._
import org.bitcoins.commons.jsonmodels.dlc.DLCStatus
import org.bitcoins.core.config.MainNet
import org.bitcoins.core.number.{Int32, UInt16, UInt32}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.crypto.{CryptoUtil, ECPrivateKey, Sha256DigestBE}
import org.bitcoins.gui.dlc.dialog._
import org.bitcoins.gui.{GlobalData, TaskRunner}
import scalafx.beans.property.ObjectProperty
import scalafx.scene.control.{TextArea, TextField}
import scalafx.collections.ObservableBuffer
import scalafx.scene.control.TextArea
import scalafx.stage.Window
import scala.util.{Failure, Success}
class DLCPaneModel(
resultArea: TextArea,
oracleInfoArea: TextArea,
numOutcomesTF: TextField) {
class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
var taskRunner: TaskRunner = _
// Sadly, it is a Java "pattern" to pass null into
@ -22,6 +25,39 @@ class DLCPaneModel(
val parentWindow: ObjectProperty[Window] =
ObjectProperty[Window](null.asInstanceOf[Window])
val dlcs: ObservableBuffer[DLCStatus] =
new ObservableBuffer[DLCStatus]()
def getDLCs: Vector[DLCStatus] = {
ConsoleCli.exec(GetDLCs, Config.empty) match {
case Failure(exception) => throw exception
case Success(dlcsStr) =>
ujson.read(dlcsStr).arr.map(DLCStatus.fromJson).toVector
}
}
def setUp(): Unit = {
dlcs.clear()
dlcs ++= getDLCs
}
def updateDLC(paramHash: Sha256DigestBE): Unit = {
ConsoleCli.exec(GetDLC(paramHash), Config.empty) match {
case Failure(exception) => throw exception
case Success(dlcStatus) =>
dlcs += DLCStatus.fromJson(ujson.read(dlcStatus))
dlcs.find(_.paramHash == paramHash).foreach(dlcs -= _)
}
}
def updateDLCs(): Unit = {
val newDLCs = getDLCs
val toAdd = newDLCs.diff(dlcs)
val toRemove = dlcs.diff(newDLCs)
dlcs ++= toAdd
dlcs --= toRemove
}
def printDLCDialogResult[T <: CliCommand](
caption: String,
dialog: DLCDialog[T]): Unit = {
@ -38,46 +74,126 @@ class DLCPaneModel(
err.printStackTrace()
resultArea.text = s"Error executing command:\n${err.getMessage}"
}
updateDLCs()
}
)
case None => ()
}
}
def onInitOracle(): Unit = {
val numOutcomes = BigInt(numOutcomesTF.text()).toInt
require(numOutcomes <= 10, "More than 10 outcomes not supported.")
val result = InitOracleDialog.showAndWait(parentWindow.value, numOutcomes)
def onInitContract(isEnum: Boolean): Unit = {
val result = if (isEnum) {
InitEnumContractDialog.showAndWait(parentWindow.value)
} else {
InitNumericContractDialog.showAndWait(parentWindow.value)
}
result match {
case Some((outcomes, contractInfo)) =>
case Some(contractInfo) =>
val builder = new StringBuilder()
builder.append(s"Serialized Contract Info:\n${contractInfo.hex}\n\n")
val privKey = ECPrivateKey.freshPrivateKey
val pubKey = privKey.schnorrPublicKey
val kValue = ECPrivateKey.freshPrivateKey
val rValue = kValue.schnorrNonce
val oracleInfo = OracleInfo(pubKey, rValue)
val (kValues, rValues, oracleInfo) = contractInfo match {
case SingleNonceContractInfo(_) =>
val kValue = ECPrivateKey.freshPrivateKey
val rValue = kValue.schnorrNonce
val oracleInfo = SingleNonceOracleInfo(pubKey, rValue)
builder.append(
s"Oracle Public Key: ${pubKey.hex}\nEvent R value: ${rValue.hex}\n")
builder.append(s"Serialized Oracle Info: ${oracleInfo.hex}\n\n")
(Vector(kValue), Vector(rValue), oracleInfo)
case MultiNonceContractInfo(_, _, numDigits, _) =>
val kValues =
0.until(numDigits).map(_ => ECPrivateKey.freshPrivateKey).toVector
val rValues = kValues.map(_.schnorrNonce)
val oracleInfo = MultiNonceOracleInfo(pubKey, rValues)
builder.append("Outcome hashes and amounts in order of entry:\n")
contractInfo.foreach {
case (hash, amt) => builder.append(s"${hash.hex} - ${amt.toLong}\n")
}
builder.append(s"\nSerialized Contract Info:\n${contractInfo.hex}\n\n")
builder.append("Outcomes and oracle sigs in order of entry:\n")
outcomes.zip(contractInfo.keys).foreach {
case (outcome, hash) =>
val sig = privKey.schnorrSignWithNonce(hash.bytes, kValue)
builder.append(s"$outcome - ${sig.hex}\n")
(kValues, rValues, oracleInfo)
}
if (GlobalData.network != MainNet) {
val descriptor = contractInfo match {
case SingleNonceContractInfo(outcomeValueMap) =>
EnumEventDescriptorV0TLV(outcomeValueMap.map(_._1.outcome))
case MultiNonceContractInfo(_, base, numDigits, _) =>
UnsignedDigitDecompositionEventDescriptor(UInt16(base),
UInt16(numDigits),
"units",
Int32.zero)
}
val oracleEvent = OracleEventV0TLV(oracleInfo.nonces,
UInt32.zero,
descriptor,
"dummy oracle")
val announcementSig =
privKey.schnorrSign(CryptoUtil.sha256(oracleEvent.bytes).bytes)
val announcement =
OracleAnnouncementV0TLV(announcementSig, pubKey, oracleEvent)
builder.append(
s"Oracle Public Key: ${pubKey.hex}\nEvent R values: ${rValues.map(_.hex).mkString(",")}\n\n")
builder.append(
s"Serialized Oracle Announcement: ${announcement.hex}\n\n")
contractInfo match {
case contractInfo: SingleNonceContractInfo =>
builder.append("Outcomes and oracle sigs in order of entry:\n")
contractInfo.keys.foreach { outcome =>
val bytes = outcome.serialized.head
val hash = CryptoUtil.sha256(bytes).bytes
val sig = privKey.schnorrSignWithNonce(hash, kValues.head)
builder.append(s"$outcome - ${sig.hex}\n")
}
case contractInfo: MultiNonceContractInfo =>
builder.append("Oracle sigs:\n")
val sortedOutcomes = contractInfo.outcomeVec.sortBy(_._2)
val max = UnsignedNumericOutcome(sortedOutcomes.last._1)
val middle = UnsignedNumericOutcome(
sortedOutcomes(sortedOutcomes.size / 2)._1)
val min = UnsignedNumericOutcome(sortedOutcomes.head._1)
val sigsMax =
max.serialized.zip(kValues.take(max.digits.size)).map {
case (bytes, kValue) =>
val hash = CryptoUtil.sha256(bytes).bytes
privKey.schnorrSignWithNonce(hash, kValue)
}
val sigsMiddle =
middle.serialized.zip(kValues.take(middle.digits.size)).map {
case (bytes, kValue) =>
val hash = CryptoUtil.sha256(bytes).bytes
privKey.schnorrSignWithNonce(hash, kValue)
}
val sigsMin =
min.serialized.zip(kValues.take(min.digits.size)).map {
case (bytes, kValue) =>
val hash = CryptoUtil.sha256(bytes).bytes
privKey.schnorrSignWithNonce(hash, kValue)
}
val maxSigsStr = sigsMax.map(_.hex).mkString("\n")
builder.append(s"local win sigs - $maxSigsStr\n\n\n")
val middleSigsStr = sigsMiddle.map(_.hex).mkString("\n")
builder.append(s"tie sigs - $middleSigsStr\n\n\n")
val minSigsStr = sigsMin.map(_.hex).mkString("\n")
builder.append(s"remote win sigs - $minSigsStr")
}
GlobalDLCData.lastOracleAnnouncement = announcement.hex
}
GlobalDLCData.lastOracleInfo = oracleInfo.hex
GlobalDLCData.lastContractInfo = contractInfo.hex
oracleInfoArea.text = builder.result()
@ -86,42 +202,38 @@ class DLCPaneModel(
}
def onOffer(): Unit = {
printDLCDialogResult("CreateDLCOffer", OfferDLCDialog)
printDLCDialogResult("CreateDLCOffer", new OfferDLCDialog)
}
def onAccept(): Unit = {
printDLCDialogResult("AcceptDLCOffer", AcceptDLCDialog)
printDLCDialogResult("AcceptDLCOffer", new AcceptDLCDialog)
}
def onSign(): Unit = {
printDLCDialogResult("SignDLC", SignDLCDialog)
printDLCDialogResult("SignDLC", new SignDLCDialog)
}
def onAddSigs(): Unit = {
printDLCDialogResult("AddDLCSigs", AddSigsDLCDialog)
printDLCDialogResult("AddDLCSigs", new AddSigsDLCDialog)
}
def onGetFunding(): Unit = {
printDLCDialogResult("GetDLCFundingTx", GetFundingDLCDialog)
printDLCDialogResult("GetDLCFundingTx", new GetFundingDLCDialog)
}
def onInitClose(): Unit = {
printDLCDialogResult("InitDLCMutualClose", InitCloseDLCDialog)
}
def onAcceptClose(): Unit = {
printDLCDialogResult("AcceptDLCMutualClose", AcceptCloseDLCDialog)
}
def onForceClose(): Unit = {
printDLCDialogResult("ExecuteUnilateralDLC", ForceCloseDLCDialog)
}
def onPunish(): Unit = {
printDLCDialogResult("ExecuteDLCPunishment", PunishDLCDialog)
def onExecute(): Unit = {
printDLCDialogResult("ExecuteDLC", new ExecuteDLCDialog)
}
def onRefund(): Unit = {
printDLCDialogResult("ExecuteDLCRefund", RefundDLCDialog)
printDLCDialogResult("ExecuteDLCRefund", new RefundDLCDialog)
}
def viewDLC(status: DLCStatus): Unit = {
updateDLCs()
val updatedStatus = dlcs.find(_.tempContractId == status.tempContractId)
ViewDLCDialog.showAndWait(parentWindow.value,
updatedStatus.getOrElse(status),
this)
}
}

View file

@ -0,0 +1,161 @@
package org.bitcoins.gui.dlc
import breeze.plot.{plot, Figure}
import org.bitcoins.commons.jsonmodels.dlc.{
CETCalculator,
OutcomeValueFunction,
RoundingIntervals
}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.util.NumberUtil
object DLCPlotUtil {
def plotCETsWithOriginalCurve(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals): Figure = {
plotCETsWithOriginalCurve(base,
numDigits,
function,
totalCollateral,
rounding,
executedCETOpt = None)
}
def plotCETsWithOriginalCurve(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals,
executedCET: Vector[Int]): Figure = {
plotCETsWithOriginalCurve(base,
numDigits,
function,
totalCollateral,
rounding,
executedCETOpt = Some(executedCET))
}
private def plotCETsWithOriginalCurve(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals,
executedCETOpt: Option[Vector[Int]]): Figure = {
val xs = 0.until(Math.pow(base, numDigits).toInt - 1).toVector
val ys = xs.map(function.apply(_).toLong.toInt)
val figure = plotCETs(base,
numDigits,
function,
totalCollateral,
rounding,
executedCETOpt)
figure.subplot(0) += plot(xs, ys, name = "Original Curve")
figure
}
def plotCETs(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals): Figure = {
plotCETs(base,
numDigits,
function,
totalCollateral,
rounding,
executedCETOpt = None)
}
def plotCETs(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals,
executedDLC: Vector[Int]): Figure = {
plotCETs(base,
numDigits,
function,
totalCollateral,
rounding,
executedCETOpt = Some(executedDLC))
}
private def plotCETs(
base: Int,
numDigits: Int,
function: OutcomeValueFunction,
totalCollateral: Satoshis,
rounding: RoundingIntervals,
executedCETOpt: Option[Vector[Int]]): Figure = {
val cets = CETCalculator.computeCETs(base,
numDigits,
function,
totalCollateral,
rounding)
plotCETs(cets, base, numDigits, executedCETOpt)
}
def plotCETs(
cets: Vector[(Vector[Int], Satoshis)],
base: Int,
numDigits: Int): Figure = {
plotCETs(cets, base, numDigits, executedCETOpt = None)
}
def plotCETs(
cets: Vector[(Vector[Int], Satoshis)],
base: Int,
numDigits: Int,
executedCET: Vector[Int]): Figure = {
plotCETs(cets, base, numDigits, executedCETOpt = Some(executedCET))
}
private def plotCETs(
cets: Vector[(Vector[Int], Satoshis)],
base: Int,
numDigits: Int,
executedCETOpt: Option[Vector[Int]]): Figure = {
def fromDigits(digits: Vector[Int]): Int = {
NumberUtil.fromDigits(digits, base, numDigits).toInt
}
val xs = cets.map(_._1).map(fromDigits)
val ys = cets.map(_._2.toLong.toInt)
val figure = Figure("DLC Payout Curve")
val cetPlot = figure.subplot(0)
val canonicalCETOpt = executedCETOpt
.flatMap { outcome =>
CETCalculator.searchForPrefix(outcome, cets.map(_._1))(identity)
}
.map(fromDigits)
val markedCETNumOpt = canonicalCETOpt.map(xs.indexOf)
val labels = { x: Int =>
if (markedCETNumOpt.contains(x))
s"Executed CET(${canonicalCETOpt.get}, ${ys(x)})"
else ""
}
cetPlot += plot(xs,
ys,
'+',
name = s"CETs (${cets.length})",
labels = labels)
cetPlot.xlabel = "Outcome"
cetPlot.ylabel = "Payout (sats)"
cetPlot.legend = true
figure
}
}

View file

@ -0,0 +1,121 @@
package org.bitcoins.gui.dlc
import org.bitcoins.commons.jsonmodels.dlc.{AcceptedDLCStatus, DLCStatus}
import org.bitcoins.commons.jsonmodels.dlc.DLCStatus._
import scalafx.beans.property.StringProperty
import scalafx.geometry.Insets
import scalafx.scene.control.{ContextMenu, MenuItem, TableColumn, TableView}
class DLCTableView(model: DLCPaneModel) {
val tableView: TableView[DLCStatus] = {
val paramHashCol = new TableColumn[DLCStatus, String] {
text = "Temp Contract Id"
prefWidth = 150
cellValueFactory = { status =>
new StringProperty(status,
"Temp Contract Id",
status.value.tempContractId.hex)
}
}
val contractIdCol = new TableColumn[DLCStatus, String] {
text = "Contract Id"
prefWidth = 150
cellValueFactory = { status =>
val contractIdStr = status.value match {
case _: Offered => ""
case signed: AcceptedDLCStatus =>
signed.contractId.toHex
}
new StringProperty(status, "Contract Id", contractIdStr)
}
}
val statusCol = new TableColumn[DLCStatus, String] {
text = "Status"
prefWidth = 150
cellValueFactory = { status =>
new StringProperty(status, "Status", status.value.statusString)
}
}
val initiatorCol = new TableColumn[DLCStatus, String] {
text = "Initiator"
prefWidth = 80
cellValueFactory = { status =>
val str = if (status.value.isInitiator) {
"Yes"
} else {
"No"
}
new StringProperty(status, "Initiator", str)
}
}
val collateralCol = new TableColumn[DLCStatus, String] {
text = "Collateral"
prefWidth = 110
cellValueFactory = { status =>
new StringProperty(status,
"Collateral",
status.value.localCollateral.toString)
}
}
val oracleCol = new TableColumn[DLCStatus, String] {
text = "Oracle"
prefWidth = 150
cellValueFactory = { status =>
new StringProperty(status, "Oracle", status.value.oracleInfo.pubKey.hex)
}
}
val eventCol = new TableColumn[DLCStatus, String] {
text = "Event"
prefWidth = 150
cellValueFactory = { status =>
new StringProperty(
status,
"Event",
status.value.oracleInfo.nonces.map(_.hex).mkString(""))
}
}
val contractMaturityCol = new TableColumn[DLCStatus, String] {
text = "Contract Mat."
prefWidth = 110
cellValueFactory = { status =>
new StringProperty(
status,
"Contract Maturity",
status.value.timeouts.contractMaturity.toUInt32.toLong.toString)
}
}
new TableView[DLCStatus](model.dlcs) {
columns ++= Seq(paramHashCol,
contractIdCol,
statusCol,
initiatorCol,
collateralCol,
oracleCol,
eventCol,
contractMaturityCol)
margin = Insets(10, 0, 10, 0)
val infoItem: MenuItem = new MenuItem("View DLC") {
onAction = _ => {
val dlc = selectionModel.value.getSelectedItem
model.viewDLC(dlc)
}
}
contextMenu = new ContextMenu() {
items ++= Vector(infoItem)
}
}
}
}

View file

@ -1,8 +1,8 @@
package org.bitcoins.gui.dlc
object GlobalDLCData {
var lastEventId: String = ""
var lastContractId: String = ""
var lastOracleSig: String = ""
var lastOracleInfo: String = ""
var lastOracleAnnouncement: String = ""
var lastContractInfo: String = ""
}

View file

@ -1,19 +0,0 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.AcceptDLCMutualClose
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCMutualCloseSig
object AcceptCloseDLCDialog
extends DLCDialog[AcceptDLCMutualClose](
"Accept DLC Close",
"Enter mutual close offer",
Vector(DLCDialog.dlcMutualCloseOfferStr -> DLCDialog.textArea())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): AcceptDLCMutualClose = {
val mutualCloseSig =
DLCMutualCloseSig.fromJson(ujson.read(inputs(dlcMutualCloseOfferStr)))
AcceptDLCMutualClose(mutualCloseSig, noBroadcast = false)
}
}

View file

@ -1,18 +1,74 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.AcceptDLCOffer
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCOffer
import org.bitcoins.cli.CliCommand._
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.OracleInfo
import org.bitcoins.core.protocol.tlv._
import scalafx.scene.Node
import scalafx.scene.control.Alert.AlertType
import scalafx.scene.control.{Alert, ButtonType, TextField}
object AcceptDLCDialog
extends DLCDialog[AcceptDLCOffer](
class AcceptDLCDialog
extends DLCDialog[AcceptDLCCliCommand](
"Accept DLC Offer",
"Enter DLC offer to accept",
Vector(DLCDialog.dlcOfferStr -> DLCDialog.textArea())) {
"Enter DLC Offer to accept or open from file",
Vector(
DLCDialog.dlcOfferStr -> DLCDialog.textArea(),
DLCDialog.dlcOfferFileStr -> DLCDialog.fileChooserButton(file => {
DLCDialog.offerDLCFile = Some(file)
DLCDialog.offerFileChosenLabel.text = file.toString
}),
DLCDialog.fileChosenStr -> DLCDialog.offerFileChosenLabel,
DLCDialog.oracleAnnouncementStr -> new TextField() {
promptText = "(optional)"
}
),
Vector(DLCDialog.dlcOfferStr,
DLCDialog.dlcOfferFileStr,
DLCDialog.oracleAnnouncementStr)) {
import DLCDialog._
def validateMatchingAnnouncement(
offer: LnMessage[DLCOfferTLV],
announcement: OracleAnnouncementTLV): Boolean = {
val fromOffer = OracleInfo.fromTLV(offer.tlv.oracleInfo)
val fromAnnouncement = OracleInfo.fromOracleAnnouncement(announcement)
fromOffer == fromAnnouncement
}
override def constructFromInput(
inputs: Map[String, String]): AcceptDLCOffer = {
val offer = DLCOffer.fromJson(ujson.read(inputs(dlcOfferStr)))
AcceptDLCOffer(offer, escaped = false)
inputs: Map[String, Node]): AcceptDLCCliCommand = {
offerDLCFile match {
case Some(file) =>
// TODO figure how to validate when using a file
offerDLCFile = None // reset
offerFileChosenLabel.text = "" // reset
AcceptDLCOfferFromFile(file.toPath)
case None =>
val offerHex = readStringFromNode(inputs(dlcOfferStr))
val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerHex)
val announcementHex = readStringFromNode(inputs(oracleAnnouncementStr))
if (announcementHex.nonEmpty) {
val announcement = OracleAnnouncementTLV(announcementHex)
if (!validateMatchingAnnouncement(offer, announcement)) {
throw new RuntimeException(
"Offer given does not have the same oracle info as announcement!")
}
} else {
new Alert(AlertType.Confirmation) {
initOwner(owner)
title = "Confirm no Oracle Announcement"
contentText =
s"Are you sure you would like sign to accept this DLC Offer without verifying it has the correct oracle?"
}.showAndWait() match {
case Some(ButtonType.OK) => ()
case None | Some(_) => throw new RuntimeException("Did not accept")
}
}
AcceptDLCOffer(offer)
}
}
}

View file

@ -1,18 +1,44 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.AddDLCSigs
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCSign
import org.bitcoins.cli.CliCommand._
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.gui.dlc.GlobalDLCData
import scalafx.scene.Node
object AddSigsDLCDialog
extends DLCDialog[AddDLCSigs](
"Sign DLC",
"Enter DLC signatures message",
Vector(DLCDialog.dlcSigStr -> DLCDialog.textArea())) {
class AddSigsDLCDialog
extends DLCDialog[AddDLCSigsCliCommand]("Add DLC Signatures",
"Enter DLC signatures message",
Vector(
DLCDialog.dlcSigStr -> DLCDialog
.textArea(),
DLCDialog.dlcSignFileStr ->
DLCDialog.fileChooserButton { file =>
DLCDialog.signDLCFile =
Some(file)
DLCDialog.signFileChosenLabel.text =
file.toString
}
),
Vector(DLCDialog.dlcSigStr,
DLCDialog.dlcSignFileStr)) {
import DLCDialog._
override def constructFromInput(inputs: Map[String, String]): AddDLCSigs = {
val sign = DLCSign.fromJson(ujson.read(inputs(dlcSigStr)))
AddDLCSigs(sign)
override def constructFromInput(
inputs: Map[String, Node]): AddDLCSigsCliCommand = {
signDLCFile match {
case Some(file) =>
signDLCFile = None // reset
signFileChosenLabel.text = "" // reset
AddDLCSigsFromFile(file.toPath)
case None =>
val signHex = readStringFromNode(inputs(dlcSigStr))
val sign = LnMessageFactory(DLCSignTLV).fromHex(signHex)
GlobalDLCData.lastContractId = sign.tlv.contractId.toHex
AddDLCSigs(sign)
}
}
}

View file

@ -1,31 +1,46 @@
package org.bitcoins.gui.dlc.dialog
import java.io.File
import org.bitcoins.cli.CliCommand
import org.bitcoins.gui.GlobalData
import org.bitcoins.gui.dlc.GlobalDLCData
import scalafx.Includes._
import scalafx.application.Platform
import scalafx.beans.property.BooleanProperty
import scalafx.geometry.Insets
import scalafx.scene.Node
import scalafx.scene.control._
import scalafx.scene.layout.GridPane
import scalafx.stage.Window
import scalafx.stage.{FileChooser, Window}
import scala.util.Properties
abstract class DLCDialog[T <: CliCommand](
dialogTitle: String,
header: String,
fields: Vector[
(String, TextInputControl)
(String, Node)
], // Vector instead of Map to keep order
optionalFields: Vector[String] = Vector.empty) {
private def readCachedValue(key: String, value: String): Unit = {
fields
.find(_._1 == key)
.foreach(_._2.text = value)
.foreach {
_._2 match {
case textInput: TextInputControl =>
textInput.text = value
case node: Node =>
throw new IllegalArgumentException(
s"Control at $key is not a text input control, got $node")
}
}
}
readCachedValue(DLCDialog.dlcEventIdStr, GlobalDLCData.lastEventId)
readCachedValue(DLCDialog.dlcContractIdStr, GlobalDLCData.lastContractId)
readCachedValue(DLCDialog.dlcOracleSigStr, GlobalDLCData.lastOracleSig)
readCachedValue(DLCDialog.oracleInfoStr, GlobalDLCData.lastOracleInfo)
readCachedValue(DLCDialog.oracleAnnouncementStr,
GlobalDLCData.lastOracleAnnouncement)
readCachedValue(DLCDialog.contractInfoStr, GlobalDLCData.lastContractInfo)
private def writeCachedValue(
@ -34,10 +49,19 @@ abstract class DLCDialog[T <: CliCommand](
setter: String => Unit): Unit = {
inputs
.find(_._1 == key)
.foreach(pair => setter(pair._2))
.foreach(pair => if (pair._2.nonEmpty) setter(pair._2))
}
def constructFromInput(inputs: Map[String, String]): T
protected def readStringFromNode(node: Node): String = {
node match {
case textInputControl: TextInputControl =>
textInputControl.text.value
case node: Node =>
throw new RuntimeException(s"Got unexpected Node, got $node")
}
}
def constructFromInput(inputs: Map[String, Node]): T
def showAndWait(parentWindow: Window): Option[T] = {
val dialog = new Dialog[Option[T]]() {
@ -47,6 +71,7 @@ abstract class DLCDialog[T <: CliCommand](
}
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
dialog.dialogPane().stylesheets = GlobalData.currentStyleSheets
dialog.dialogPane().content = new GridPane {
hgap = 10
@ -54,9 +79,9 @@ abstract class DLCDialog[T <: CliCommand](
padding = Insets(20, 100, 10, 10)
var nextRow: Int = 0
def addRow(label: String, textField: TextInputControl): Unit = {
def addRow(label: String, node: Node): Unit = {
add(new Label(label), 0, nextRow)
add(textField, 1, nextRow)
add(node, 1, nextRow)
nextRow += 1
}
@ -67,35 +92,42 @@ abstract class DLCDialog[T <: CliCommand](
// Enable/Disable OK button depending on whether all data was entered.
val okButton = dialog.dialogPane().lookupButton(ButtonType.OK)
val requiredFields =
fields.filterNot(field => optionalFields.contains(field._1))
val requiredTextFields =
fields.filterNot(field => optionalFields.contains(field._1)).collect {
case (_: String, textInputControl: TextInputControl) => textInputControl
}
// Simple validation that sufficient data was entered
okButton.disable <== requiredFields
.map(_._2.text.isEmpty)
.reduce(_ || _)
okButton.disable <== {
val bools = requiredTextFields
.map(_.text.isEmpty)
// Request focus on the first field by default.
Platform.runLater(fields.head._2.requestFocus())
// Need to do this because foldLeft doesn't work nicely
if (bools.isEmpty) {
BooleanProperty(false).delegate
} else bools.reduce(_ || _).delegate
}
// When the OK button is clicked, convert the result to a T.
dialog.resultConverter = dialogButton =>
if (dialogButton == ButtonType.OK) {
val inputs = fields.map { case (key, input) => (key, input.text()) }
val textInputs = fields.collect {
case (key: String, input: TextInputControl) => (key, input.text())
}
writeCachedValue(DLCDialog.dlcEventIdStr,
inputs,
GlobalDLCData.lastEventId = _)
writeCachedValue(DLCDialog.dlcContractIdStr,
textInputs,
GlobalDLCData.lastContractId = _)
writeCachedValue(DLCDialog.dlcOracleSigStr,
inputs,
textInputs,
GlobalDLCData.lastOracleSig = _)
writeCachedValue(DLCDialog.oracleInfoStr,
inputs,
GlobalDLCData.lastOracleInfo = _)
writeCachedValue(DLCDialog.oracleAnnouncementStr,
textInputs,
GlobalDLCData.lastOracleAnnouncement = _)
writeCachedValue(DLCDialog.contractInfoStr,
inputs,
textInputs,
GlobalDLCData.lastContractInfo = _)
Some(constructFromInput(inputs.toMap))
Some(constructFromInput(fields.toMap))
} else None
val result = dialog.showAndWait()
@ -115,15 +147,42 @@ object DLCDialog {
}
}
val oracleInfoStr = "Oracle Info"
def fileChooserButton[T](handleFile: File => T): Node = {
new Button("Browse...") {
onAction = _ => {
val fileChooser = new FileChooser() {
initialDirectory = new File(Properties.userHome)
}
val selectedFile = fileChooser.showOpenDialog(null)
if (selectedFile != null) {
handleFile(selectedFile)
()
}
}
}
}
var offerDLCFile: Option[File] = None
var acceptDLCFile: Option[File] = None
var signDLCFile: Option[File] = None
val offerFileChosenLabel = new Label("")
val acceptFileChosenLabel = new Label("")
val signFileChosenLabel = new Label("")
val oracleAnnouncementStr = "Oracle Announcement"
val contractInfoStr = "Contract Info"
val collateralStr = "Collateral"
val collateralStr = "Your Collateral"
val feeRateStr = "Fee Rate"
val locktimeStr = "Locktime"
val refundLocktimeStr = "Refund Locktime"
val fileChosenStr = ""
val allOfferFields: Map[String, String] = Map[String, String](
oracleInfoStr -> "",
oracleAnnouncementStr -> "",
contractInfoStr -> "",
collateralStr -> "Satoshis",
feeRateStr -> "sats/vbyte (optional)",
@ -141,12 +200,15 @@ object DLCDialog {
}.toVector
val dlcOfferStr = "DLC Offer"
val dlcOfferFileStr = "Open Offer from File"
val dlcAcceptStr = "DLC Accept Message"
val dlcAcceptFileStr = "Open Accept from File"
val dlcSigStr = "DLC Signatures"
val dlcSignFileStr = "Open Sign from File"
val dlcEventIdStr = "Event ID"
val dlcContractIdStr = "Contract ID"
val dlcOracleSigStr = "Oracle Signature"

View file

@ -0,0 +1,29 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.ExecuteDLC
import org.bitcoins.crypto.SchnorrDigitalSignature
import scalafx.scene.Node
import scalafx.scene.control.TextField
import scodec.bits.ByteVector
class ExecuteDLCDialog
extends DLCDialog[ExecuteDLC](
"DLC Close",
"Enter DLC execution info",
Vector(DLCDialog.dlcContractIdStr -> new TextField(),
DLCDialog.dlcOracleSigStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(inputs: Map[String, Node]): ExecuteDLC = {
val contractId = readStringFromNode(inputs(dlcContractIdStr))
val oracleSigsStr = readStringFromNode(inputs(dlcOracleSigStr))
val oracleSigs = oracleSigsStr.split(",").map { str =>
SchnorrDigitalSignature.fromHex(str.trim)
}
ExecuteDLC(ByteVector.fromValidHex(contractId),
oracleSigs.toVector,
noBroadcast = false)
}
}

View file

@ -1,21 +0,0 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.ExecuteDLCUnilateralClose
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
import scalafx.scene.control.TextField
object ForceCloseDLCDialog
extends DLCDialog[ExecuteDLCUnilateralClose](
"DLC Force Close",
"Enter DLC closing info",
Vector(DLCDialog.dlcEventIdStr -> new TextField(),
DLCDialog.dlcOracleSigStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): ExecuteDLCUnilateralClose = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
val oracleSig = SchnorrDigitalSignature(inputs(dlcOracleSigStr))
ExecuteDLCUnilateralClose(eventId, oracleSig, noBroadcast = false)
}
}

View file

@ -1,19 +1,22 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.GetDLCFundingTx
import org.bitcoins.crypto.Sha256DigestBE
import org.bitcoins.cli.CliCommand.BroadcastDLCFundingTx
import scalafx.scene.Node
import scalafx.scene.control.TextField
import scodec.bits.ByteVector
object GetFundingDLCDialog
extends DLCDialog[GetDLCFundingTx](
class GetFundingDLCDialog
extends DLCDialog[BroadcastDLCFundingTx](
"DLC Funding Transaction",
"Enter DLC event ID",
Vector(DLCDialog.dlcEventIdStr -> new TextField())) {
"Enter DLC contract ID",
Vector(DLCDialog.dlcContractIdStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): GetDLCFundingTx = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
GetDLCFundingTx(eventId)
inputs: Map[String, Node]): BroadcastDLCFundingTx = {
val hex = readStringFromNode(inputs(dlcContractIdStr))
val contractId = ByteVector.fromValidHex(hex)
BroadcastDLCFundingTx(contractId)
}
}

View file

@ -1,21 +0,0 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.InitDLCMutualClose
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
import scalafx.scene.control.TextField
object InitCloseDLCDialog
extends DLCDialog[InitDLCMutualClose](
"DLC Funding Transaction",
"Enter DLC closing info",
Vector(DLCDialog.dlcEventIdStr -> new TextField(),
DLCDialog.dlcOracleSigStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): InitDLCMutualClose = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
val oracleSig = SchnorrDigitalSignature(inputs(dlcOracleSigStr))
InitDLCMutualClose(eventId, oracleSig, escaped = false)
}
}

View file

@ -0,0 +1,101 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.SingleNonceContractInfo
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.tlv.EnumOutcome
import org.bitcoins.gui.GlobalData
import org.bitcoins.gui.util.GUIUtil.setNumericInput
import scalafx.Includes._
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.control._
import scalafx.scene.layout._
import scalafx.stage.Window
import scala.collection._
object InitEnumContractDialog {
def showAndWait(parentWindow: Window): Option[SingleNonceContractInfo] = {
val dialog =
new Dialog[Option[SingleNonceContractInfo]]() {
initOwner(parentWindow)
title = "Initialize Demo Oracle"
headerText = "Enter contract outcomes and their outcome values"
}
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
dialog.dialogPane().stylesheets = GlobalData.currentStyleSheets
dialog.resizable = true
val fields: mutable.Map[Int, (TextField, TextField)] = mutable.Map.empty
var nextRow: Int = 1
val gridPane = new GridPane {
alignment = Pos.Center
padding = Insets(top = 10, right = 10, bottom = 10, left = 10)
hgap = 5
vgap = 5
add(new Label("Outcomes"), 0, 0)
add(new Label("Values"), 1, 0)
}
def addOutcomeRow(): Unit = {
val outcomeTF = new TextField()
val amtTF = new TextField() {
promptText = "Satoshis"
}
setNumericInput(amtTF)
val row = nextRow
val _ = fields.put(row, (outcomeTF, amtTF))
gridPane.add(outcomeTF, 0, row)
gridPane.add(amtTF, 1, row)
nextRow += 1
dialog.dialogPane().getScene.getWindow.sizeToScene()
}
addOutcomeRow()
addOutcomeRow()
val addPointButton: Button = new Button("+ Outcome") {
onAction = _ => addOutcomeRow()
}
val bottom = new HBox() {
spacing = 10
alignment = Pos.BottomRight
children = Vector(addPointButton)
}
dialog.dialogPane().content = new VBox(gridPane, bottom)
// When the OK button is clicked, convert the result to a CreateDLCOffer.
dialog.resultConverter = dialogButton =>
if (dialogButton == ButtonType.OK) {
val inputs = fields.values.flatMap {
case (str, value) =>
if (str.text.value.nonEmpty && value.text.value.nonEmpty)
Some((str.text(), value.text()))
else None
}
val contractMap = inputs.map {
case (str, value) =>
EnumOutcome(str) -> Satoshis(BigInt(value))
}.toVector
Some(SingleNonceContractInfo(contractMap))
} else None
val result = dialog.showAndWait()
result match {
case Some(Some(contractInfo: SingleNonceContractInfo)) =>
Some(contractInfo)
case Some(_) | None => None
}
}
}

View file

@ -0,0 +1,301 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.MultiNonceContractInfo
import org.bitcoins.commons.jsonmodels.dlc.{
OutcomeValueFunction,
OutcomeValuePoint,
RoundingIntervals
}
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.gui.GlobalData
import org.bitcoins.gui.dlc.DLCPlotUtil
import org.bitcoins.gui.util.GUIUtil.setNumericInput
import scalafx.Includes._
import scalafx.application.Platform
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.Node
import scalafx.scene.control._
import scalafx.scene.layout.{GridPane, HBox, VBox}
import scalafx.stage.Window
import scala.util.{Failure, Success, Try}
object InitNumericContractDialog {
def showAndWait(parentWindow: Window): Option[MultiNonceContractInfo] = {
val dialog =
new Dialog[Option[MultiNonceContractInfo]]() {
initOwner(parentWindow)
title = "Initialize Demo Oracle"
headerText = "Enter contract interpolation points"
}
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
dialog.dialogPane().stylesheets = GlobalData.currentStyleSheets
dialog.resizable = true
val baseTF = new TextField() {
text = "2"
}
val numDigitsTF = new TextField()
val totalCollateralTF = new TextField() {
promptText = "Satoshis"
}
setNumericInput(baseTF)
setNumericInput(numDigitsTF)
setNumericInput(totalCollateralTF)
val pointMap: scala.collection.mutable.Map[
Int,
(TextField, TextField, CheckBox)] =
scala.collection.mutable.Map.empty
var nextPointRow: Int = 2
val pointGrid: GridPane = new GridPane {
alignment = Pos.Center
padding = Insets(top = 10, right = 10, bottom = 10, left = 10)
hgap = 5
vgap = 5
add(new Label("Outcome"), 0, 0)
add(new Label("Payout"), 1, 0)
add(new Label("Endpoint"), 2, 0)
}
def addPointRow(
xOpt: Option[String] = None,
yOpt: Option[String] = None): Unit = {
val xTF = new TextField() {
promptText = "Outcome (base 10)"
}
xOpt match {
case Some(value) =>
xTF.text = value
case None => ()
}
val yTF = new TextField() {
promptText = "Satoshis"
}
yOpt match {
case Some(value) =>
yTF.text = value
case None => ()
}
val endPointBox = new CheckBox() {
selected = true
}
setNumericInput(xTF)
setNumericInput(yTF)
val row = nextPointRow
val _ = pointMap.put(row, (xTF, yTF, endPointBox))
pointGrid.add(xTF, 0, row)
pointGrid.add(yTF, 1, row)
pointGrid.add(new Label("Endpoint:"), 2, row)
pointGrid.add(endPointBox, 3, row)
nextPointRow += 1
dialog.dialogPane().getScene.getWindow.sizeToScene()
}
addPointRow(Some("0"), Some("0"))
addPointRow()
val addPointButton: Button = new Button("+") {
onAction = _ => addPointRow()
}
val roundingMap: scala.collection.mutable.Map[Int, (TextField, TextField)] =
scala.collection.mutable.Map.empty
var nextRoundingRow: Int = 2
val roundingGrid: GridPane = new GridPane {
alignment = Pos.Center
padding = Insets(top = 10, right = 10, bottom = 10, left = 10)
hgap = 5
vgap = 5
add(new Label("Outcome"), 0, 0)
add(new Label("Rounding Level"), 1, 0)
}
def addRoundingRow(): Unit = {
val outcomeTF = new TextField() {
promptText = "Outcome (base 10)"
}
val roundingLevelTF = new TextField() {
promptText = "Satoshis"
}
setNumericInput(outcomeTF)
setNumericInput(roundingLevelTF)
val row = nextRoundingRow
val _ = roundingMap.put(row, (outcomeTF, roundingLevelTF))
roundingGrid.add(outcomeTF, 0, row)
roundingGrid.add(roundingLevelTF, 1, row)
nextRoundingRow += 1
dialog.dialogPane().getScene.getWindow.sizeToScene()
}
addRoundingRow()
addRoundingRow()
val addRoundingRowButton: Button = new Button("+") {
onAction = _ => addRoundingRow()
}
def getContractInfo: Try[MultiNonceContractInfo] = {
Try {
val base = baseTF.text.value.toInt
val numDigits = numDigitsTF.text.value.toInt
val totalCollateral = Satoshis(totalCollateralTF.text.value.toLong)
val points = pointMap.values.toVector
val outcomesValuePoints = points.flatMap {
case (xTF, yTF, checkBox) =>
if (xTF.text.value.nonEmpty && yTF.text.value.nonEmpty) {
val x = xTF.text.value.toLong
val y = yTF.text.value.toLong
Some(OutcomeValuePoint(x, Satoshis(y), checkBox.selected.value))
} else {
None
}
}
val sorted = outcomesValuePoints.sortBy(_.outcome)
require(sorted == outcomesValuePoints, "Must be sorted by outcome")
val func = OutcomeValueFunction(outcomesValuePoints)
MultiNonceContractInfo(func, base, numDigits, totalCollateral)
}
}
def getRoundingIntervals: RoundingIntervals = {
val roundingIntervalsT = Try {
roundingMap.values.toVector.flatMap {
case (outcomeTF, roundingLevelTF) =>
if (
outcomeTF.text.value.nonEmpty && roundingLevelTF.text.value.nonEmpty
) {
val outcome = BigDecimal(outcomeTF.text.value.toDouble)
val level = roundingLevelTF.text.value.toLong
Some(outcome, level)
} else {
None
}
}
}
roundingIntervalsT match {
case Failure(_) => RoundingIntervals.noRounding
case Success(intervals) => RoundingIntervals(intervals)
}
}
val vBoxContent: VBox = new VBox() {
padding = Insets(20, 10, 10, 10)
spacing = 10
alignment = Pos.Center
val eventDataGrid: GridPane = new GridPane {
padding = Insets(top = 10, right = 10, bottom = 10, left = 10)
hgap = 5
vgap = 5
add(new Label("Base"), 0, 0)
add(baseTF, 1, 0)
add(new Label("Num Digits"), 0, 1)
add(numDigitsTF, 1, 1)
add(new Label("Total Collateral"), 0, 2)
add(totalCollateralTF, 1, 2)
}
val outcomes: Node = new VBox {
alignment = Pos.Center
val label: HBox = new HBox {
alignment = Pos.Center
spacing = 10
children = Vector(new Label("Points"), addPointButton)
}
children = Vector(label, pointGrid)
}
val roundingIntervals: VBox = new VBox {
alignment = Pos.Center
val label: HBox = new HBox {
alignment = Pos.Center
spacing = 10
children =
Vector(new Label("Rounding Intervals"), addRoundingRowButton)
}
children = Vector(label, roundingGrid)
}
val roundingPane: TitledPane = new TitledPane() {
text = "Rounding Info"
content = roundingIntervals
}
val roundingAccordion: Accordion = new Accordion() {
panes = Vector(roundingPane)
}
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
getContractInfo match {
case Failure(_) => ()
case Success(contractInfo) =>
DLCPlotUtil.plotCETsWithOriginalCurve(
contractInfo.base,
contractInfo.numDigits,
contractInfo.outcomeValueFunc,
contractInfo.totalCollateral,
getRoundingIntervals)
()
}
}
}
children = Vector(eventDataGrid,
new Separator(),
outcomes,
roundingAccordion,
previewGraphButton)
}
dialog.dialogPane().content = new ScrollPane() {
content = vBoxContent
}
// Enable/Disable OK button depending on whether all data was entered.
val okButton = dialog.dialogPane().lookupButton(ButtonType.OK)
// Simple validation that sufficient data was entered
okButton.disable <== baseTF.text.isEmpty || totalCollateralTF.text.isEmpty
Platform.runLater(numDigitsTF.requestFocus())
// When the OK button is clicked, convert the result to a CreateDLCOffer.
dialog.resultConverter = dialogButton =>
if (dialogButton == ButtonType.OK) {
getContractInfo match {
case Failure(exception) => throw exception
case Success(contractInfo) =>
Some(contractInfo)
}
} else None
dialog.showAndWait() match {
case Some(Some(contractInfo: MultiNonceContractInfo)) =>
Some(contractInfo)
case Some(_) | None => None
}
}
}

View file

@ -1,87 +0,0 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.ContractInfo
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.crypto.CryptoUtil
import scalafx.Includes._
import scalafx.application.Platform
import scalafx.geometry.Insets
import scalafx.scene.control.{ButtonType, Dialog, Label, TextField}
import scalafx.scene.layout.GridPane
import scalafx.stage.Window
import scodec.bits.ByteVector
object InitOracleDialog {
def showAndWait(
parentWindow: Window,
numOutcomes: Int): Option[(Vector[String], ContractInfo)] = {
val dialog = new Dialog[Option[(Vector[String], ContractInfo)]]() {
initOwner(parentWindow)
title = "Initialize Demo Oracle"
headerText = "Enter contract outcomes and their outcome values"
}
val fields =
(0 until numOutcomes).map(_ =>
(new TextField(),
new TextField() {
promptText = "Satoshis"
}))
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
dialog.dialogPane().content = new GridPane {
hgap = 10
vgap = 10
padding = Insets(20, 100, 10, 10)
add(new Label("Outcomes"), 0, 0)
add(new Label("Values"), 1, 0)
var nextRow: Int = 1
fields.foreach {
case (str, value) =>
add(str, 0, nextRow)
add(value, 1, nextRow)
nextRow += 1
}
}
// Enable/Disable OK button depending on whether all data was entered.
val okButton = dialog.dialogPane().lookupButton(ButtonType.OK)
// Simple validation that sufficient data was entered
okButton.disable <== fields
.map { case (str, value) => str.text.isEmpty || value.text.isEmpty }
.reduce(_ || _)
// Request focus on the first field by default.
Platform.runLater(fields.head._1.requestFocus())
// When the OK button is clicked, convert the result to a CreateDLCOffer.
dialog.resultConverter = dialogButton =>
if (dialogButton == ButtonType.OK) {
val inputs = fields.map {
case (str, value) => (str.text(), value.text())
}
val contractMap = inputs.map {
case (str, value) =>
val hash = CryptoUtil.sha256(ByteVector(str.getBytes)).flip
hash -> Satoshis(BigInt(value))
}.toMap
val outcomes = inputs.map(_._1).toVector
Some((outcomes, ContractInfo(contractMap)))
} else None
val result = dialog.showAndWait()
result match {
case Some(Some((outcomes: Vector[_], contractInfo: ContractInfo))) =>
Some((outcomes.map(_.toString), contractInfo))
case Some(_) | None => None
}
}
}

View file

@ -1,38 +1,39 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.CreateDLCOffer
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import scalafx.scene.Node
object OfferDLCDialog
class OfferDLCDialog
extends DLCDialog[CreateDLCOffer]("Create DLC Offer",
"Enter DLC details",
DLCDialog.constructOfferFields(),
Vector(DLCDialog.feeRateStr)) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): CreateDLCOffer = {
val feeRate = if (inputs(feeRateStr).isEmpty) {
override def constructFromInput(inputs: Map[String, Node]): CreateDLCOffer = {
val feeRate = if (readStringFromNode(inputs(feeRateStr)).isEmpty) {
None
} else {
Some(
SatoshisPerVirtualByte(
Satoshis(BigInt(inputs(feeRateStr)))
Satoshis(BigInt(readStringFromNode(inputs(feeRateStr))))
)
)
}
CreateDLCOffer(
oracleInfo = DLCMessage.OracleInfo.fromHex(inputs(oracleInfoStr)),
contractInfo = DLCMessage.ContractInfo.fromHex(inputs(contractInfoStr)),
collateral = Satoshis(BigInt(inputs(collateralStr))),
oracle = OracleAnnouncementV0TLV.fromHex(
readStringFromNode(inputs(oracleAnnouncementStr))),
contractInfo =
ContractInfoTLV.fromHex(readStringFromNode(inputs(contractInfoStr))),
collateral = Satoshis(BigInt(readStringFromNode(inputs(collateralStr)))),
feeRateOpt = feeRate,
locktime = UInt32(BigInt(inputs(locktimeStr))),
refundLT = UInt32(BigInt(inputs(refundLocktimeStr))),
escaped = false
locktime = UInt32(BigInt(readStringFromNode(inputs(locktimeStr)))),
refundLT = UInt32(BigInt(readStringFromNode(inputs(refundLocktimeStr))))
)
}
}

View file

@ -1,22 +0,0 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.ClaimDLCPenaltyFunds
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto.Sha256DigestBE
import scalafx.scene.control.TextField
object PunishDLCDialog
extends DLCDialog[ClaimDLCPenaltyFunds](
"DLC Force Close",
"Enter DLC punishment info",
Vector(DLCDialog.dlcEventIdStr -> new TextField(),
DLCDialog.dlcForceCloseTxStr -> DLCDialog.textArea())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): ClaimDLCPenaltyFunds = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
val forceCloseTx = Transaction(inputs(dlcForceCloseTxStr))
ClaimDLCPenaltyFunds(eventId, forceCloseTx, noBroadcast = false)
}
}

View file

@ -1,19 +1,22 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.ExecuteDLCRefund
import org.bitcoins.crypto.Sha256DigestBE
import scalafx.scene.Node
import scalafx.scene.control.TextField
import scodec.bits.ByteVector
object RefundDLCDialog
class RefundDLCDialog
extends DLCDialog[ExecuteDLCRefund](
"DLC Refund",
"Enter DLC event ID",
Vector(DLCDialog.dlcEventIdStr -> new TextField())) {
"Enter DLC contract ID",
Vector(DLCDialog.dlcContractIdStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): ExecuteDLCRefund = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
ExecuteDLCRefund(eventId, noBroadcast = false)
inputs: Map[String, Node]): ExecuteDLCRefund = {
val hex = readStringFromNode(inputs(dlcContractIdStr))
val contractId = ByteVector.fromValidHex(hex)
ExecuteDLCRefund(contractId, noBroadcast = false)
}
}

View file

@ -1,19 +1,39 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.SignDLC
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCAccept
import org.bitcoins.gui.dlc.GlobalDLCData
import org.bitcoins.cli.CliCommand._
import org.bitcoins.core.protocol.tlv._
import scalafx.scene.Node
object SignDLCDialog
extends DLCDialog[SignDLC](
"Sign DLC",
"Enter DLC accept message",
Vector(DLCDialog.dlcAcceptStr -> DLCDialog.textArea())) {
class SignDLCDialog
extends DLCDialog[SignDLCCliCommand]("Sign DLC",
"Enter DLC Accept message",
Vector(
DLCDialog.dlcAcceptStr -> DLCDialog
.textArea(),
"Open Accept from File" ->
DLCDialog.fileChooserButton { file =>
DLCDialog.acceptDLCFile =
Some(file)
DLCDialog.acceptFileChosenLabel.text =
file.toString
}
),
Vector(DLCDialog.dlcAcceptStr,
DLCDialog.dlcAcceptFileStr)) {
import DLCDialog._
override def constructFromInput(inputs: Map[String, String]): SignDLC = {
val accept = DLCAccept.fromJson(ujson.read(inputs(dlcAcceptStr)))
GlobalDLCData.lastEventId = accept.eventId.hex
SignDLC(accept, escaped = false)
override def constructFromInput(
inputs: Map[String, Node]): SignDLCCliCommand = {
acceptDLCFile match {
case Some(file) =>
acceptDLCFile = None // reset
acceptFileChosenLabel.text = "" // reset
SignDLCFromFile(file.toPath)
case None =>
val acceptHex = readStringFromNode(inputs(dlcAcceptStr))
val accept = LnMessageFactory(DLCAcceptTLV).fromHex(acceptHex)
SignDLC(accept)
}
}
}

View file

@ -0,0 +1,211 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
MultiNonceContractInfo,
SingleNonceContractInfo
}
import org.bitcoins.commons.jsonmodels.dlc._
import org.bitcoins.gui.GlobalData
import org.bitcoins.gui.dlc.{DLCPaneModel, DLCPlotUtil, GlobalDLCData}
import scalafx.Includes._
import scalafx.geometry.Insets
import scalafx.scene.Node
import scalafx.scene.control._
import scalafx.scene.layout.GridPane
import scalafx.stage.Window
object ViewDLCDialog {
def showAndWait(
parentWindow: Window,
status: DLCStatus,
model: DLCPaneModel): Unit = {
val dialog = new Dialog[Unit]() {
initOwner(parentWindow)
title = "View DLC"
}
dialog.dialogPane().buttonTypes = Seq(ButtonType.Close)
dialog.dialogPane().stylesheets = GlobalData.currentStyleSheets
dialog.resizable = true
dialog.dialogPane().content = new GridPane() {
hgap = 10
vgap = 10
padding = Insets(20, 100, 10, 10)
private var row = 0
add(new Label("Param Hash:"), 0, row)
add(new TextField() {
text = status.paramHash.hex
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Initiator:"), 0, row)
add(new TextField() {
text = if (status.isInitiator) "Yes" else "No"
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("State:"), 0, row)
add(new TextField() {
text = status.statusString
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Temp Contract Id:"), 0, row)
add(new TextField() {
text = status.tempContractId.hex
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Contract Id:"), 0, row)
val contractId: String = DLCStatus
.getContractId(status)
.map(_.toHex)
.getOrElse("")
add(new TextField() {
text = contractId
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Oracle Info:"), 0, row)
add(new TextField() {
text = status.oracleInfo.hex
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Contract Info:"), 0, row)
add(new TextField() {
text = status.contractInfo.hex
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Fee Rate:"), 0, row)
add(new TextField() {
text = s"${status.feeRate.toLong} sats/vbyte"
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Contract Maturity:"), 0, row)
add(new TextField() {
text = status.timeouts.contractMaturity.toUInt32.toLong.toString
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Contract Timeout:"), 0, row)
add(new TextField() {
text = status.timeouts.contractTimeout.toUInt32.toLong.toString
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Collateral:"), 0, row)
add(
new TextField() {
text = status.totalCollateral.satoshis.toLong.toString
editable = false
},
columnIndex = 1,
rowIndex = row
)
row += 1
add(new Label("Funding TxId:"), 0, row)
add(new TextField() {
text = DLCStatus.getFundingTxId(status).map(_.hex).getOrElse("")
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Closing TxId:"), 0, row)
add(new TextField() {
text = DLCStatus.getClosingTxId(status).map(_.hex).getOrElse("")
editable = false
},
columnIndex = 1,
rowIndex = row)
row += 1
add(new Label("Oracle Signatures:"), 0, row)
val sigsOpt: Option[String] = DLCStatus
.getOracleSignatures(status)
.map(_.map(_.hex).mkString(","))
val node: Node = sigsOpt match {
case Some(sigs) =>
new TextField() {
text = sigs
editable = false
}
case None =>
new Button("Execute") {
onAction = _ => {
// Set data for this DLC
GlobalDLCData.lastContractId = contractId
GlobalDLCData.lastOracleSig = ""
model.onExecute()
}
}
}
add(node, columnIndex = 1, rowIndex = row)
row += 1
status.contractInfo match {
case _: SingleNonceContractInfo => ()
case MultiNonceContractInfo(outcomeValueFunc,
base,
numDigits,
totalCollateral) =>
val previewGraphButton: Button = new Button("Preview Graph") {
onAction = _ => {
DLCPlotUtil.plotCETsWithOriginalCurve(
base,
numDigits,
outcomeValueFunc,
totalCollateral,
RoundingIntervals.noRounding)
()
}
}
add(previewGraphButton, 1, row)
}
}
val _ = dialog.showAndWait()
}
}

View file

@ -0,0 +1,18 @@
package org.bitcoins.gui.util
import scalafx.Includes._
import scalafx.scene.control.TextField
object GUIUtil {
def setNumericInput(textField: TextField): Unit = {
textField.text.addListener {
(
_: javafx.beans.value.ObservableValue[_ <: String],
_: String,
newVal: String) =>
if (!newVal.matches("-?\\d*"))
textField.setText(newVal.replaceAll("[^-?\\d]", ""))
}
}
}

View file

@ -0,0 +1,8 @@
akka {
# Set these to the defaults instead of the
# appServer's modified ones
http.server.request-timeout = 10s
http.server.parsing.max-content-length = 8m
http.client.parsing.max-content-length = 8m
}

View file

@ -61,7 +61,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
val mockNode = mock[Node]
val chainRoutes = ChainRoutes(mockChainApi)
val chainRoutes = ChainRoutes(mockChainApi, RegTest)
val nodeRoutes = NodeRoutes(mockNode)

View file

@ -274,7 +274,7 @@ class BitcoinSServerMain(override val args: Array[String])
val walletRoutes = WalletRoutes(wallet)
val nodeRoutes = NodeRoutes(nodeApi)
val chainRoutes = ChainRoutes(chainApi)
val chainRoutes = ChainRoutes(chainApi, nodeConf.network)
val coreRoutes = CoreRoutes(Core)
val server = {
rpcPortOpt match {

View file

@ -3,10 +3,13 @@ package org.bitcoins.server
import akka.actor.ActorSystem
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import org.bitcoins.commons.jsonmodels.BitcoinSServerInfo
import org.bitcoins.commons.serializers.Picklers._
import org.bitcoins.core.api.chain.ChainApi
import org.bitcoins.core.config.BitcoinNetwork
case class ChainRoutes(chain: ChainApi)(implicit system: ActorSystem)
case class ChainRoutes(chain: ChainApi, network: BitcoinNetwork)(implicit
system: ActorSystem)
extends ServerRoute {
import system.dispatcher
@ -35,6 +38,15 @@ case class ChainRoutes(chain: ChainApi)(implicit system: ActorSystem)
Server.httpSuccess(hash)
}
}
case ServerCommand("getinfo", _) =>
complete {
chain.getBestBlockHeader().map { header =>
val info = BitcoinSServerInfo(network, header.height, header.hashBE)
Server.httpSuccess(info.toJson)
}
}
}
}

View file

@ -436,11 +436,16 @@ object Rescan extends ServerJsonModels {
}
trait Broadcastable {
def noBroadcast: Boolean
}
case class SendToAddress(
address: BitcoinAddress,
amount: Bitcoins,
satoshisPerVirtualByte: Option[SatoshisPerVirtualByte],
noBroadcast: Boolean)
extends Broadcastable
object SendToAddress extends ServerJsonModels {

View file

@ -0,0 +1,73 @@
package org.bitcoins.core.protocol.tlv
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.testkit.core.gen.LnMessageGen
import org.bitcoins.testkit.util.BitcoinSUnitTest
import scodec.bits.ByteVector
class LnMessageTest extends BitcoinSUnitTest {
"LnMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.lnMessage) { msg =>
assert(LnMessage(msg.bytes) == msg)
}
}
"UnknownMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.unknownMessage) { unknown =>
assert(LnMessage(unknown.bytes) == unknown)
}
}
"InitMessage" must "parse correctly as an unknown message" in {
assert(
LnMessage(
"001000022200000302aaa2012006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f") == LnMessage(
UnknownTLV(
BigSizeUInt(16),
ByteVector.fromValidHex(
"00022200000302aaa2012006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"))))
}
"ErrorMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.errorMessage) { error =>
assert(LnMessage(error.bytes) == error)
}
}
"PingMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.pingMessage) { ping =>
assert(LnMessage(ping.bytes) == ping)
}
}
"PongMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.pongMessage) { pong =>
assert(LnMessage(pong.bytes) == pong)
}
}
"PongMessage" must "parse correctly" in {
assert(
LnMessage("001300020000") == LnMessage(
PongTLV.forIgnored(ByteVector.fromValidHex("0000"))))
}
"DLCOfferMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.dlcOfferMessage) { dlcOffer =>
assert(LnMessage(dlcOffer.bytes) == dlcOffer)
}
}
"DLCAcceptMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.dlcAcceptMessage) { dlcAccept =>
assert(LnMessage(dlcAccept.bytes) == dlcAccept)
}
}
"DLCSignMessage" must "have serialization symmetry" in {
forAll(LnMessageGen.dlcSignMessage) { dlcSign =>
assert(LnMessage(dlcSign.bytes) == dlcSign)
}
}
}

View file

@ -5,9 +5,6 @@ import org.bitcoins.testkit.util.BitcoinSUnitTest
class TLVTest extends BitcoinSUnitTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
"TLV" must "have serialization symmetry" in {
forAll(TLVGen.tlv) { tlv =>
assert(TLV(tlv.bytes) == tlv)
@ -85,4 +82,60 @@ class TLVTest extends BitcoinSUnitTest {
assert(TLV(tlv.bytes) == tlv)
}
}
"ContractInfoV0TLV" must "have serialization symmetry" in {
forAll(TLVGen.contractInfoV0TLV) { contractInfo =>
assert(ContractInfoV0TLV(contractInfo.bytes) == contractInfo)
assert(TLV(contractInfo.bytes) == contractInfo)
}
}
"OracleInfoV0TLV" must "have serialization symmetry" in {
forAll(TLVGen.oracleInfoV0TLV) { oracleInfo =>
assert(OracleInfoV0TLV(oracleInfo.bytes) == oracleInfo)
assert(TLV(oracleInfo.bytes) == oracleInfo)
}
}
"FundingInputV0TLV" must "have serialization symmetry" in {
forAll(TLVGen.fundingInputV0TLV) { fundingInput =>
assert(FundingInputV0TLV(fundingInput.bytes) == fundingInput)
assert(TLV(fundingInput.bytes) == fundingInput)
}
}
"CETSignaturesV0TLV" must "have serialization symmetry" in {
forAll(TLVGen.cetSignaturesV0TLV) { cetSigs =>
assert(CETSignaturesV0TLV(cetSigs.bytes) == cetSigs)
assert(TLV(cetSigs.bytes) == cetSigs)
}
}
"FundingSignaturesV0TLV" must "have serialization symmetry" in {
forAll(TLVGen.fundingSignaturesV0TLV) { fundingSigs =>
assert(FundingSignaturesV0TLV(fundingSigs.bytes) == fundingSigs)
assert(TLV(fundingSigs.bytes) == fundingSigs)
}
}
"DLCOfferTLV" must "have serialization symmetry" in {
forAll(TLVGen.dlcOfferTLV) { offer =>
assert(DLCOfferTLV(offer.bytes) == offer)
assert(TLV(offer.bytes) == offer)
}
}
"DLCAcceptTLV" must "have serialization symmetry" in {
forAll(TLVGen.dlcAcceptTLV) { accept =>
assert(DLCAcceptTLV(accept.bytes) == accept)
assert(TLV(accept.bytes) == accept)
}
}
"DLCSignTLV" must "have serialization symmetry" in {
forAll(TLVGen.dlcSignTLV) { sign =>
assert(DLCSignTLV(sign.bytes) == sign)
assert(TLV(sign.bytes) == sign)
}
}
}

View file

@ -5,8 +5,11 @@ import java.math.BigInteger
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
import org.scalatest.Assertion
import scala.annotation.tailrec
class NumberUtilTest extends BitcoinSUnitTest {
behavior of "NumberUtil"
@ -230,4 +233,82 @@ class NumberUtilTest extends BitcoinSUnitTest {
val expected1 = Vector(0, 7, 11)
assert(NumberUtil.decompose(num1, 16, 3) == expected1)
}
it must "correctly decompose in any base" in {
assert(NumberUtil.decompose(255, 16, 2) == Vector(15, 15))
forAll(Gen.choose(2, 256), Gen.choose(0L, Long.MaxValue)) {
case (base, num) =>
val numStr = num.toString
val expectedBase10 = numStr
.foldLeft(Vector.empty[Int]) {
case (vec, char) =>
vec :+ (char.toInt - '0'.toInt)
}
val base10 = NumberUtil.decompose(num, 10, numStr.length)
assert(base10 == expectedBase10)
// Add some extra digits for leading zeroes
val numDigits = (Math.log(num) / Math.log(base)).toInt + 5
val decomposed = NumberUtil.decompose(num, base, numDigits)
assert(decomposed.head == 0)
@tailrec
def pow(base: BigInt, exp: Int, prodSoFar: BigInt = 1): BigInt = {
if (exp == 0) {
prodSoFar
} else {
pow(base, exp - 1, base * prodSoFar)
}
}
val computedNum = decomposed.reverse.zipWithIndex.foldLeft(BigInt(0)) {
case (sumSoFar, (digit, position)) =>
sumSoFar + digit * pow(BigInt(base), position)
}
assert(computedNum.toLong == num)
}
}
behavior of "NumberUtil.fromDigits"
it must "correctly handle digit decomposition in base 10" in {
val expected0 = 987
val num0 = Vector(9, 8, 7)
assert(NumberUtil.fromDigits(num0, 10, 3) == expected0)
val expected1 = 123
val num1 = Vector(0, 1, 2, 3)
assert(NumberUtil.fromDigits(num1, 10, 4) == expected1)
}
it must "correctly do digit decomposition in base 2" in {
val expected0 = 987
val num0 = Vector(1, 1, 1, 1, 0, 1, 1, 0, 1, 1)
assert(NumberUtil.fromDigits(num0, 2, 10) == expected0)
val expected1 = 123
val num1 = Vector(0, 1, 1, 1, 1, 0, 1, 1)
assert(NumberUtil.fromDigits(num1, 2, 8) == expected1)
}
it must "correctly do digit decomposition n base 16" in {
val expected0 = 987
val num0 = Vector(3, 13, 11)
assert(NumberUtil.fromDigits(num0, 16, 3) == expected0)
val expected1 = 123
val num1 = Vector(0, 7, 11)
assert(NumberUtil.fromDigits(num1, 16, 3) == expected1)
}
it must "correctly invert decompose" in {
forAll(Gen.choose(2, 256), Gen.choose(0L, Long.MaxValue)) {
case (base, num) =>
// Add some extra digits for leading zeroes
val numDigits = (Math.log(num) / Math.log(base)).toInt + 5
val digits = NumberUtil.decompose(num, base, numDigits)
assert(NumberUtil.fromDigits(digits, base, numDigits) == num)
}
}
}

View file

@ -1,11 +1,13 @@
package org.bitcoins.core.wallet.utxo
import org.bitcoins.core.currency.CurrencyUnits
import org.bitcoins.core.currency.{CurrencyUnits, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.signer.BitcoinSigner
import org.bitcoins.crypto.{ECPrivateKey, ECPublicKey}
import org.bitcoins.testkit.core.gen.{
CreditingTxGen,
GenUtil,
ScriptGenerators,
TransactionGenerators
@ -14,6 +16,9 @@ import org.bitcoins.testkit.util.BitcoinSAsyncTest
class InputInfoTest extends BitcoinSAsyncTest {
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
def randomSPK: ScriptPubKey = {
GenUtil.sample(ScriptGenerators.scriptPubKey.map(_._1))
}
@ -207,4 +212,57 @@ class InputInfoTest extends BitcoinSAsyncTest {
)
}
}
it must "successfully compute maxWitnessLengths" in {
forAllAsync(CreditingTxGen.output) { scriptSigParams =>
val dummyTx = BaseTransaction(
TransactionConstants.validLockVersion,
Vector(
TransactionInput(scriptSigParams.inputInfo.outPoint,
EmptyScriptSignature,
UInt32.zero)),
Vector(TransactionOutput(Satoshis.zero, EmptyScriptPubKey)),
UInt32.zero
)
val maxWitnessLenF = BitcoinSigner
.sign(scriptSigParams, unsignedTx = dummyTx, isDummySignature = true)
.map(_.transaction)
.map {
case wtx: WitnessTransaction => wtx.witness.head.byteSize.toInt
case _: NonWitnessTransaction => 0
}
maxWitnessLenF.map { expectedLen =>
assert(scriptSigParams.maxWitnessLen == expectedLen)
}
}
}
it must "successfully compute maxScriptSigLengths" in {
forAllAsync(CreditingTxGen.output) { scriptSigParams =>
val dummyTx = BaseTransaction(
TransactionConstants.validLockVersion,
Vector(
TransactionInput(scriptSigParams.inputInfo.outPoint,
EmptyScriptSignature,
UInt32.zero)),
Vector(TransactionOutput(Satoshis.zero, EmptyScriptPubKey)),
UInt32.zero
)
val maxScriptSigF = BitcoinSigner
.sign(scriptSigParams, unsignedTx = dummyTx, isDummySignature = true)
.map(_.transaction)
.map { tx =>
tx.inputs.head.scriptSignature
}
maxScriptSigF.map { scriptSig =>
assert(InputInfo.maxScriptSigLen(
scriptSigParams.inputInfo) == scriptSig.byteSize,
scriptSig.hex)
}
}
}
}

View file

@ -1,7 +1,7 @@
package org.bitcoins.core.config
import org.bitcoins.core.protocol.blockchain._
import org.bitcoins.crypto.{CryptoUtil, StringFactory}
import org.bitcoins.crypto.{CryptoUtil, DoubleSha256DigestBE, StringFactory}
import scodec.bits.ByteVector
sealed abstract class NetworkParameters {
@ -68,8 +68,6 @@ sealed abstract class MainNet extends BitcoinNetwork {
* @inheritdoc
*/
override def rpcPort = 8332
//mainnet doesn't need to be specified like testnet or regtest
override def name = ""
/**
* @inheritdoc
@ -202,6 +200,13 @@ object Networks extends StringFactory[NetworkParameters] {
def bytesToNetwork: Map[ByteVector, NetworkParameters] =
BitcoinNetworks.bytesToNetwork
def fromChainHash(chainHash: DoubleSha256DigestBE): NetworkParameters = {
knownNetworks
.find(_.chainParams.genesisBlock.blockHeader.hashBE == chainHash)
.getOrElse(throw new IllegalArgumentException(
s"$chainHash is not a recognized Chain Hash"))
}
}
object BitcoinNetworks extends StringFactory[BitcoinNetwork] {
@ -228,7 +233,7 @@ object BitcoinNetworks extends StringFactory[BitcoinNetwork] {
}
/** Map of magic network bytes to the corresponding network */
val magicToNetwork: Map[ByteVector, NetworkParameters] =
lazy val magicToNetwork: Map[ByteVector, NetworkParameters] =
Map(
MainNet.magicBytes -> MainNet,
TestNet3.magicBytes -> TestNet3,
@ -236,7 +241,7 @@ object BitcoinNetworks extends StringFactory[BitcoinNetwork] {
SigNet.magicBytes -> SigNet
)
def bytesToNetwork: Map[ByteVector, NetworkParameters] =
lazy val bytesToNetwork: Map[ByteVector, NetworkParameters] =
Map(
MainNet.p2shNetworkByte -> MainNet,
MainNet.p2pkhNetworkByte -> MainNet,

View file

@ -1,7 +1,13 @@
package org.bitcoins.core.currency
import org.bitcoins.core.consensus.Consensus
import org.bitcoins.core.number.{BaseNumbers, BasicArithmetic, Bounded, Int64}
import org.bitcoins.core.number.{
BaseNumbers,
BasicArithmetic,
Bounded,
Int64,
UInt64
}
import org.bitcoins.core.serializers.RawSatoshisSerializer
import org.bitcoins.crypto.{Factory, NetworkElement}
import scodec.bits.ByteVector
@ -108,6 +114,12 @@ sealed abstract class Satoshis extends CurrencyUnit {
def toLong: Long = underlying.toLong
lazy val toUInt64: UInt64 = {
require(toLong >= 0, "Cannot cast negative value to UInt64")
UInt64(toLong)
}
def ==(satoshis: Satoshis): Boolean = underlying == satoshis.underlying
}
@ -124,6 +136,7 @@ object Satoshis
override def fromBytes(bytes: ByteVector): Satoshis =
RawSatoshisSerializer.read(bytes)
def apply(int64: Int64): Satoshis = SatoshisImpl(int64)
def apply(uint64: UInt64): Satoshis = SatoshisImpl(Int64(uint64.toLong))
def apply(satoshis: Long): Satoshis = SatoshisImpl(Int64(satoshis))
def apply(satoshis: BigInt): Satoshis = SatoshisImpl(Int64(satoshis))

View file

@ -57,7 +57,6 @@ object BlockStamp extends StringFactory[BlockStamp] {
val time = UInt32(seconds)
new BlockTime(time)
}
}
override def fromStringT(s: String): Try[BlockStamp] = {
@ -73,6 +72,21 @@ object BlockStamp extends StringFactory[BlockStamp] {
blockHash orElse blockHeight orElse blockTime orElse error
}
def apply(timeLockNumber: UInt32): BlockTimeStamp =
fromUInt32(timeLockNumber)
def apply(timeLockNumber: Int): BlockTimeStamp =
fromUInt32(UInt32(timeLockNumber))
/** @see [[https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki#detailed-specification]] */
def fromUInt32(uInt32: UInt32): BlockTimeStamp = {
if (uInt32 < TransactionConstants.locktimeThreshold) {
BlockHeight(uInt32.toInt)
} else {
BlockStamp.BlockTime(uInt32)
}
}
override def fromString(string: String): BlockStamp = {
fromStringT(string) match {
case Failure(exception) => throw exception

View file

@ -55,13 +55,13 @@ object NonStandardScriptSignature
}
/** A script signature to be used in tests for signing EmptyScriptPubKey.
* This script pushes an OP_TRUE onto the stack, causing a successful spend.
* This script pushes a true onto the stack, causing a successful spend.
*/
case object TrivialTrueScriptSignature extends ScriptSignature {
override lazy val signatures: Seq[ECDigitalSignature] = Nil
override lazy val asm: Vector[ScriptToken] =
Vector(BytesToPushOntoStack(1), ScriptConstant("51"))
Vector(OP_TRUE)
def isTrivialTrueScriptSignature(asm: Seq[ScriptToken]): Boolean = {
asm == this.asm

View file

@ -11,6 +11,7 @@ import org.bitcoins.crypto.{
ECDigitalSignature,
ECPublicKey,
EmptyDigitalSignature,
Factory,
NetworkElement
}
import scodec.bits.ByteVector
@ -160,7 +161,11 @@ object P2WSHWitnessV0 {
}
}
object ScriptWitness {
object ScriptWitness extends Factory[ScriptWitness] {
override def fromBytes(bytes: ByteVector): ScriptWitness = {
RawScriptWitnessParser.read(bytes)
}
def apply(stack: Seq[ByteVector]): ScriptWitness = {
//TODO: eventually only compressed public keys will be allowed in v0 scripts

View file

@ -0,0 +1,30 @@
package org.bitcoins.core.protocol.tlv
import org.bitcoins.crypto.CryptoUtil
import scodec.bits.ByteVector
/** Represents a DLC event that could be signed by an oracle */
sealed trait DLCOutcomeType {
/** The ByteVectors to be signed by the oracle using pre-committed nonces */
def serialized: Vector[ByteVector]
}
/** An outcome from an enumerated event type */
case class EnumOutcome(outcome: String) extends DLCOutcomeType {
override lazy val serialized: Vector[ByteVector] =
Vector(CryptoUtil.serializeForHash(outcome))
}
/** An outcome from a multi-nonce unsigned numeric event type.
*
* If digits.length is less than the the total number of digits to be
* signed by the oracle then this outcome represents all outcomes prefixed
* by the given digits.
*/
case class UnsignedNumericOutcome(digits: Vector[Int]) extends DLCOutcomeType {
override lazy val serialized: Vector[ByteVector] =
digits.map(digit => CryptoUtil.serializeForHash(digit.toString))
}

View file

@ -0,0 +1,49 @@
package org.bitcoins.core.protocol.tlv
import org.bitcoins.core.number.UInt16
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.crypto.{Factory, NetworkElement}
import scodec.bits.ByteVector
/** Lightning Network message serialization is the same as TLV
* serialization except that the type is represented with a
* UInt16 instead of BigSizeUInt and length is omitted.
*
* The reason for the omission is that the message is expected to
* be encrypted by the LN transport layer and the length is included
* there in the unencrypted part of the packet.
*
* The reason that LnMessage doesn't just do what TLV does (which is better)
* is because TLVs are newer and so we're stuck with the legacy format.
*/
case class LnMessage[+T <: TLV](tlv: T) extends NetworkElement {
require(tlv.tpe.toLong <= 65535L, s"LN Message format requires UInt16 types")
val tpe: UInt16 = UInt16(tlv.tpe.toInt)
val payload: ByteVector = tlv.value
override lazy val bytes: ByteVector = tpe.bytes ++ payload
}
object LnMessage extends Factory[LnMessage[TLV]] {
override def fromBytes(bytes: ByteVector): LnMessage[TLV] = {
val tpe = BigSizeUInt(UInt16(bytes.take(2)).toInt)
val value = bytes.drop(2)
val length = BigSizeUInt(value.length)
val tlv = TLV.fromBytes(tpe.bytes ++ length.bytes ++ value)
LnMessage(tlv)
}
}
case class LnMessageFactory[+T <: TLV](tlvFactory: TLVFactory[T])
extends Factory[LnMessage[T]] {
override def fromBytes(bytes: ByteVector): LnMessage[T] = {
val tpe = BigSizeUInt(UInt16(bytes.take(2)).toInt)
val value = bytes.drop(2)
val length = BigSizeUInt(value.length)
val tlv = tlvFactory.fromBytes(tpe.bytes ++ length.bytes ++ value)
LnMessage(tlv)
}
}

View file

@ -3,14 +3,17 @@ package org.bitcoins.core.protocol.tlv
import java.nio.charset.StandardCharsets
import java.time.Instant
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number._
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.tlv.TLV.{
DecodeTLVResult,
FALSE_BYTE,
TRUE_BYTE
}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.{BigSizeUInt, BlockTimeStamp}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto._
import scodec.bits.ByteVector
@ -29,6 +32,21 @@ sealed trait TLV extends NetworkElement {
def sha256: Sha256Digest = CryptoUtil.sha256(bytes)
}
trait TLVSerializable[+T <: TLV] extends NetworkElement {
def toTLV: T
override def bytes: ByteVector = toTLV.bytes
}
abstract class TLVDeserializable[T <: TLV, +U <: TLVSerializable[T]](
tlvFactory: Factory[T])
extends Factory[U] {
def fromTLV(tlv: T): U
override def fromBytes(bytes: ByteVector): U =
fromTLV(tlvFactory.fromBytes(bytes))
}
sealed trait TLVParentFactory[T <: TLV] extends Factory[T] {
def typeName: String
@ -74,12 +92,21 @@ object TLV extends TLVParentFactory[TLV] {
val typeName = "TLV"
val allFactories: Vector[TLVFactory[TLV]] =
val allFactories: Vector[TLVFactory[TLV]] = {
Vector(ErrorTLV,
PingTLV,
PongTLV,
OracleEventV0TLV,
OracleAnnouncementV0TLV) ++ EventDescriptorTLV.allFactories
FundingInputV0TLV,
CETSignaturesV0TLV,
FundingSignaturesV0TLV,
DLCOfferTLV,
DLCAcceptTLV,
DLCSignTLV) ++ EventDescriptorTLV.allFactories ++
ContractInfoTLV.allFactories ++
OracleInfoTLV.allFactories ++
OracleAnnouncementTLV.allFactories
}
// Need to override to be able to default to Unknown
override def fromBytes(bytes: ByteVector): TLV = {
@ -97,6 +124,10 @@ object TLV extends TLVParentFactory[TLV] {
size.bytes ++ strBytes
}
def encodeScript(script: Script): ByteVector = {
UInt16(script.asmBytes.length).bytes ++ script.asmBytes
}
}
sealed trait TLVFactory[+T <: TLV] extends Factory[T] {
@ -159,6 +190,12 @@ sealed trait TLVFactory[+T <: TLV] extends Factory[T] {
val len = UInt16(takeBits(16)).toInt
ScriptPubKey.fromAsmBytes(take(len))
}
def takePoint(): TLVPoint = {
val point = TLVPoint(current)
skip(point)
point
}
}
}
@ -345,6 +382,9 @@ object EnumEventDescriptorV0TLV extends TLVFactory[EnumEventDescriptorV0TLV] {
EnumEventDescriptorV0TLV(result)
}
val dummy: EnumEventDescriptorV0TLV = EnumEventDescriptorV0TLV(
Vector("dummy"))
}
sealed trait NumericEventDescriptorTLV extends EventDescriptorTLV {
@ -456,7 +496,8 @@ object RangeEventDescriptorV0TLV extends TLVFactory[RangeEventDescriptorV0TLV] {
}
/** Describes a large range event using numerical decomposition */
trait DigitDecompositionEventDescriptorV0TLV extends NumericEventDescriptorTLV {
sealed trait DigitDecompositionEventDescriptorV0TLV
extends NumericEventDescriptorTLV {
require(numDigits > UInt16.zero,
s"Number of digits must be positive, got $numDigits")
@ -641,6 +682,14 @@ sealed trait OracleAnnouncementTLV extends TLV {
def validateSignature: Boolean
}
object OracleAnnouncementTLV extends TLVParentFactory[OracleAnnouncementTLV] {
val allFactories: Vector[TLVFactory[OracleAnnouncementTLV]] =
Vector(OracleAnnouncementV0TLV)
override def typeName: String = "OracleAnnouncementTLV"
}
case class OracleAnnouncementV0TLV(
announcementSignature: SchnorrDigitalSignature,
publicKey: SchnorrPublicKey,
@ -669,4 +718,498 @@ object OracleAnnouncementV0TLV extends TLVFactory[OracleAnnouncementV0TLV] {
OracleAnnouncementV0TLV(sig, publicKey, eventTLV)
}
lazy val dummy: OracleAnnouncementV0TLV = {
val priv = ECPrivateKey.freshPrivateKey
val event = OracleEventV0TLV(Vector(priv.schnorrNonce),
UInt32.zero,
EnumEventDescriptorV0TLV.dummy,
"dummy")
val sig = priv.schnorrSign(CryptoUtil.sha256(event.bytes).bytes)
OracleAnnouncementV0TLV(sig, priv.schnorrPublicKey, event)
}
}
sealed trait ContractInfoTLV extends TLV
object ContractInfoTLV extends TLVParentFactory[ContractInfoTLV] {
val allFactories: Vector[TLVFactory[ContractInfoTLV]] =
Vector(ContractInfoV0TLV, ContractInfoV1TLV)
override def typeName: String = "ContractInfoTLV"
}
/** @see https://github.com/discreetlogcontracts/dlcspecs/blob/master/Messaging.md#version-0-contract_info */
case class ContractInfoV0TLV(outcomes: Vector[(String, Satoshis)])
extends ContractInfoTLV {
override val tpe: BigSizeUInt = ContractInfoV0TLV.tpe
override val value: ByteVector = {
outcomes.foldLeft(ByteVector.empty) {
case (bytes, (outcome, amt)) =>
val outcomeBytes = CryptoUtil.serializeForHash(outcome)
bytes ++ BigSizeUInt
.calcFor(outcomeBytes)
.bytes ++ outcomeBytes ++ amt.toUInt64.bytes
}
}
}
object ContractInfoV0TLV extends TLVFactory[ContractInfoV0TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42768)
override def fromTLVValue(value: ByteVector): ContractInfoV0TLV = {
val iter = ValueIterator(value)
val builder = Vector.newBuilder[(String, Satoshis)]
while (iter.index < value.length) {
val outcomeLen = BigSizeUInt(iter.current)
iter.skip(outcomeLen)
val outcome =
new String(iter.take(outcomeLen.toInt).toArray, StandardCharsets.UTF_8)
val amt = Satoshis(UInt64(iter.takeBits(64)))
builder.+=(outcome -> amt)
}
ContractInfoV0TLV(builder.result())
}
}
case class TLVPoint(outcome: Long, value: Satoshis, isEndpoint: Boolean)
extends NetworkElement {
lazy val leadingByte: Byte = if (isEndpoint) {
1.toByte
} else {
0.toByte
}
override def bytes: ByteVector = {
ByteVector(leadingByte) ++ BigSizeUInt(outcome).bytes ++ UInt64(
value.toLong).bytes
}
}
object TLVPoint extends Factory[TLVPoint] {
override def fromBytes(bytes: ByteVector): TLVPoint = {
val isEndpoint = bytes.head match {
case 0 => false
case 1 => true
case b: Byte =>
throw new IllegalArgumentException(
s"Did not recognize leading byte: $b")
}
val outcome = BigSizeUInt(bytes.tail)
val value = UInt64(bytes.drop(1 + outcome.byteSize).take(8))
TLVPoint(outcome.toLong, Satoshis(value.toLong), isEndpoint)
}
}
/** @see https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/NumericOutcome.md#curve-serialization */
case class ContractInfoV1TLV(
base: Int,
numDigits: Int,
totalCollateral: Satoshis,
points: Vector[TLVPoint])
extends ContractInfoTLV {
override val tpe: BigSizeUInt = ContractInfoV1TLV.tpe
override val value: ByteVector = {
BigSizeUInt(base).bytes ++ UInt16(numDigits).bytes ++ UInt64(
totalCollateral.toLong).bytes ++ BigSizeUInt(
points.length).bytes ++ points.foldLeft(ByteVector.empty)(_ ++ _.bytes)
}
}
object ContractInfoV1TLV extends TLVFactory[ContractInfoV1TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42784)
override def fromTLVValue(value: ByteVector): ContractInfoV1TLV = {
val iter = ValueIterator(value)
val base = BigSizeUInt(iter.current)
iter.skip(base)
val numDigits = UInt16(iter.takeBits(16))
val totalCollateral = UInt64(iter.takeBits(64))
val numPoints = BigSizeUInt(iter.current)
iter.skip(numPoints)
val points = (0L until numPoints.toLong).toVector.map { _ =>
iter.takePoint()
}
ContractInfoV1TLV(base.toInt,
numDigits.toInt,
Satoshis(totalCollateral.toLong),
points)
}
}
sealed trait OracleInfoTLV extends TLV
object OracleInfoTLV extends TLVParentFactory[OracleInfoTLV] {
override val allFactories: Vector[TLVFactory[OracleInfoTLV]] =
Vector(OracleInfoV0TLV, OracleInfoV1TLV)
override def typeName: String = "OracleInfoTLV"
}
case class OracleInfoV0TLV(pubKey: SchnorrPublicKey, rValue: SchnorrNonce)
extends OracleInfoTLV {
override val tpe: BigSizeUInt = OracleInfoV0TLV.tpe
override val value: ByteVector = {
pubKey.bytes ++ rValue.bytes
}
}
object OracleInfoV0TLV extends TLVFactory[OracleInfoV0TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42770)
override def fromTLVValue(value: ByteVector): OracleInfoV0TLV = {
val (pubKeyBytes, rBytes) = value.splitAt(32)
val pubKey = SchnorrPublicKey(pubKeyBytes)
val rValue = SchnorrNonce(rBytes)
OracleInfoV0TLV(pubKey, rValue)
}
}
case class OracleInfoV1TLV(
pubKey: SchnorrPublicKey,
nonces: Vector[SchnorrNonce])
extends OracleInfoTLV {
override val tpe: BigSizeUInt = OracleInfoV1TLV.tpe
override val value: ByteVector = {
nonces.foldLeft(pubKey.bytes)(_ ++ _.bytes)
}
}
object OracleInfoV1TLV extends TLVFactory[OracleInfoV1TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42786)
override def fromTLVValue(value: ByteVector): OracleInfoV1TLV = {
require(
value.length >= 64 && value.length % 32 == 0,
s"Expected multiple of 32 bytes with at least one nonce, got $value")
val iter = ValueIterator(value)
val pubKey = SchnorrPublicKey(iter.take(32))
val nonces = (0L until iter.current.length / 32).toVector.map { _ =>
SchnorrNonce(iter.take(32))
}
OracleInfoV1TLV(pubKey, nonces)
}
}
sealed trait FundingInputTLV extends TLV
case class FundingInputV0TLV(
prevTx: Transaction,
prevTxVout: UInt32,
sequence: UInt32,
maxWitnessLen: UInt16,
redeemScriptOpt: Option[WitnessScriptPubKey])
extends FundingInputTLV {
override val tpe: BigSizeUInt = FundingInputV0TLV.tpe
lazy val output: TransactionOutput = prevTx.outputs(prevTxVout.toInt)
lazy val outPoint: TransactionOutPoint =
TransactionOutPoint(prevTx.txId, prevTxVout)
lazy val input: TransactionInput = {
val scriptSig = redeemScriptOpt match {
case Some(redeemScript) => P2SHScriptSignature(redeemScript)
case None => EmptyScriptSignature
}
TransactionInput(outPoint, scriptSig, sequence)
}
lazy val outputReference: OutputReference = OutputReference(outPoint, output)
override val value: ByteVector = {
val redeemScript =
redeemScriptOpt.getOrElse(EmptyScriptPubKey)
UInt16(prevTx.byteSize).bytes ++
prevTx.bytes ++
prevTxVout.bytes ++
sequence.bytes ++
maxWitnessLen.bytes ++
TLV.encodeScript(redeemScript)
}
}
object FundingInputV0TLV extends TLVFactory[FundingInputV0TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42772)
override def fromTLVValue(value: ByteVector): FundingInputV0TLV = {
val iter = ValueIterator(value)
val prevTxLen = UInt16(iter.takeBits(16))
val prevTx = Transaction(iter.take(prevTxLen.toInt))
val prevTxVout = UInt32(iter.takeBits(32))
val sequence = UInt32(iter.takeBits(32))
val maxWitnessLen = UInt16(iter.takeBits(16))
val redeemScript = iter.takeSPK()
val redeemScriptOpt = redeemScript match {
case EmptyScriptPubKey => None
case wspk: WitnessScriptPubKey => Some(wspk)
case _: NonWitnessScriptPubKey =>
throw new IllegalArgumentException(
s"Redeem Script must be Segwith SPK: $redeemScript")
}
FundingInputV0TLV(prevTx,
prevTxVout,
sequence,
maxWitnessLen,
redeemScriptOpt)
}
}
sealed trait CETSignaturesTLV extends TLV
case class CETSignaturesV0TLV(sigs: Vector[ECAdaptorSignature])
extends CETSignaturesTLV {
override val tpe: BigSizeUInt = CETSignaturesV0TLV.tpe
override val value: ByteVector = {
sigs.foldLeft(ByteVector.empty)(_ ++ _.bytes)
}
}
object CETSignaturesV0TLV extends TLVFactory[CETSignaturesV0TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42774)
override def fromTLVValue(value: ByteVector): CETSignaturesV0TLV = {
val iter = ValueIterator(value)
val builder = Vector.newBuilder[ECAdaptorSignature]
while (iter.index < value.length) {
val sig = ECAdaptorSignature(iter.take(162))
builder.+=(sig)
}
CETSignaturesV0TLV(builder.result())
}
}
sealed trait FundingSignaturesTLV extends TLV
case class FundingSignaturesV0TLV(witnesses: Vector[ScriptWitnessV0])
extends FundingSignaturesTLV {
override val tpe: BigSizeUInt = FundingSignaturesV0TLV.tpe
override val value: ByteVector = {
witnesses.foldLeft(UInt16(witnesses.length).bytes) {
case (bytes, witness) =>
witness.stack.reverse.foldLeft(
bytes ++ UInt16(witness.stack.length).bytes) {
case (bytes, stackElem) =>
bytes ++ UInt16(stackElem.length).bytes ++ stackElem
}
}
}
}
object FundingSignaturesV0TLV extends TLVFactory[FundingSignaturesV0TLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42776)
override def fromTLVValue(value: ByteVector): FundingSignaturesV0TLV = {
val iter = ValueIterator(value)
val numWitnesses = UInt16(iter.takeBits(16))
val witnesses = (0 until numWitnesses.toInt).toVector.map { _ =>
val numStackElements = UInt16(iter.takeBits(16))
val stack = (0 until numStackElements.toInt).toVector.map { _ =>
val stackElemLength = UInt16(iter.takeBits(16))
iter.take(stackElemLength.toInt)
}
ScriptWitness(stack.reverse) match {
case EmptyScriptWitness =>
throw new IllegalArgumentException(s"Invalid witness: $stack")
case witness: ScriptWitnessV0 => witness
}
}
FundingSignaturesV0TLV(witnesses)
}
}
case class DLCOfferTLV(
contractFlags: Byte,
chainHash: DoubleSha256Digest,
contractInfo: ContractInfoTLV,
oracleInfo: OracleInfoTLV,
fundingPubKey: ECPublicKey,
payoutSPK: ScriptPubKey,
totalCollateralSatoshis: Satoshis,
fundingInputs: Vector[FundingInputTLV],
changeSPK: ScriptPubKey,
feeRate: SatoshisPerVirtualByte,
contractMaturityBound: BlockTimeStamp,
contractTimeout: BlockTimeStamp)
extends TLV {
override val tpe: BigSizeUInt = DLCOfferTLV.tpe
override val value: ByteVector = {
ByteVector(contractFlags) ++
chainHash.bytes ++
contractInfo.bytes ++
oracleInfo.bytes ++
fundingPubKey.bytes ++
TLV.encodeScript(payoutSPK) ++
totalCollateralSatoshis.toUInt64.bytes ++
UInt16(fundingInputs.length).bytes ++
fundingInputs.foldLeft(ByteVector.empty)(_ ++ _.bytes) ++
TLV.encodeScript(changeSPK) ++
feeRate.currencyUnit.satoshis.toUInt64.bytes ++
contractMaturityBound.toUInt32.bytes ++
contractTimeout.toUInt32.bytes
}
}
object DLCOfferTLV extends TLVFactory[DLCOfferTLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42778)
override def fromTLVValue(value: ByteVector): DLCOfferTLV = {
val iter = ValueIterator(value)
val contractFlags = iter.take(1).head
val chainHash = DoubleSha256Digest(iter.take(32))
val contractInfo = ContractInfoTLV.fromBytes(iter.current)
iter.skip(contractInfo)
val oracleInfo = OracleInfoTLV.fromBytes(iter.current)
iter.skip(oracleInfo)
val fundingPubKey = ECPublicKey(iter.take(33))
val payoutSPK = iter.takeSPK()
val totalCollateralSatoshis = Satoshis(UInt64(iter.takeBits(64)))
val numFundingInputs = UInt16(iter.takeBits(16))
val fundingInputs = (0 until numFundingInputs.toInt).toVector.map { _ =>
val fundingInput = FundingInputV0TLV.fromBytes(iter.current)
iter.skip(fundingInput)
fundingInput
}
val changeSPK = iter.takeSPK()
val feeRate = SatoshisPerVirtualByte(Satoshis(UInt64(iter.takeBits(64))))
val contractMaturityBound = BlockTimeStamp(UInt32(iter.takeBits(32)))
val contractTimeout = BlockTimeStamp(UInt32(iter.takeBits(32)))
DLCOfferTLV(
contractFlags,
chainHash,
contractInfo,
oracleInfo,
fundingPubKey,
payoutSPK,
totalCollateralSatoshis,
fundingInputs,
changeSPK,
feeRate,
contractMaturityBound,
contractTimeout
)
}
}
case class DLCAcceptTLV(
tempContractId: Sha256Digest,
totalCollateralSatoshis: Satoshis,
fundingPubKey: ECPublicKey,
payoutSPK: ScriptPubKey,
fundingInputs: Vector[FundingInputTLV],
changeSPK: ScriptPubKey,
cetSignatures: CETSignaturesTLV,
refundSignature: ECDigitalSignature)
extends TLV {
override val tpe: BigSizeUInt = DLCAcceptTLV.tpe
override val value: ByteVector = {
tempContractId.bytes ++
totalCollateralSatoshis.toUInt64.bytes ++
fundingPubKey.bytes ++
TLV.encodeScript(payoutSPK) ++
UInt16(fundingInputs.length).bytes ++
fundingInputs.foldLeft(ByteVector.empty)(_ ++ _.bytes) ++
TLV.encodeScript(changeSPK) ++
cetSignatures.bytes ++
refundSignature.toRawRS
}
}
object DLCAcceptTLV extends TLVFactory[DLCAcceptTLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42780)
override def fromTLVValue(value: ByteVector): DLCAcceptTLV = {
val iter = ValueIterator(value)
val tempContractId = Sha256Digest(iter.take(32))
val totalCollateralSatoshis = Satoshis(UInt64(iter.takeBits(64)))
val fundingPubKey = ECPublicKey(iter.take(33))
val payoutSPK = iter.takeSPK()
val numFundingInputs = UInt16(iter.takeBits(16))
val fundingInputs = (0 until numFundingInputs.toInt).toVector.map { _ =>
val fundingInput = FundingInputV0TLV.fromBytes(iter.current)
iter.skip(fundingInput)
fundingInput
}
val changeSPK = iter.takeSPK()
val cetSignatures = CETSignaturesV0TLV.fromBytes(iter.current)
iter.skip(cetSignatures)
val refundSignature = ECDigitalSignature.fromRS(iter.take(64))
DLCAcceptTLV(tempContractId,
totalCollateralSatoshis,
fundingPubKey,
payoutSPK,
fundingInputs,
changeSPK,
cetSignatures,
refundSignature)
}
}
case class DLCSignTLV(
contractId: ByteVector,
cetSignatures: CETSignaturesTLV,
refundSignature: ECDigitalSignature,
fundingSignatures: FundingSignaturesTLV)
extends TLV {
override val tpe: BigSizeUInt = DLCSignTLV.tpe
override val value: ByteVector = {
contractId ++
cetSignatures.bytes ++
refundSignature.toRawRS ++
fundingSignatures.bytes
}
}
object DLCSignTLV extends TLVFactory[DLCSignTLV] {
override val tpe: BigSizeUInt = BigSizeUInt(42782)
override def fromTLVValue(value: ByteVector): DLCSignTLV = {
val iter = ValueIterator(value)
val contractId = iter.take(32)
val cetSignatures = CETSignaturesV0TLV.fromBytes(iter.current)
iter.skip(cetSignatures)
val refundSignature = ECDigitalSignature.fromRS(iter.take(64))
val fundingSignatures = FundingSignaturesV0TLV.fromBytes(iter.current)
iter.skip(fundingSignatures)
DLCSignTLV(contractId, cetSignatures, refundSignature, fundingSignatures)
}
}

View file

@ -109,16 +109,15 @@ case class PSBT(
lazy val estimateWeight: Option[Long] = {
if (nextRole.order >= PSBTRole.SignerPSBTRole.order) {
// Need a exe context for maxScriptSigAndWitnessWeight
import scala.concurrent.ExecutionContext.Implicits.global
val dummySigner = Sign.dummySign(ECPublicKey.freshPublicKey)
val inputWeight =
inputMaps.zip(transaction.inputs).foldLeft(0L) {
case (weight, (inputMap, txIn)) =>
val (scriptSigLen, maxWitnessLen) = inputMap
val signingInfo = inputMap
.toUTXOSatisfyingInfoUsingSigners(txIn, Vector(dummySigner))
.maxScriptSigAndWitnessWeight
val scriptSigLen = signingInfo.maxScriptSigLen
val maxWitnessLen = signingInfo.maxWitnessLen
weight + 164 + maxWitnessLen + scriptSigLen
}

View file

@ -1,12 +1,5 @@
package org.bitcoins.core.util
trait SeqWrapper[+T] extends IndexedSeq[T] {
protected def wrapped: IndexedSeq[T]
override def iterator: Iterator[T] = wrapped.iterator
override def length: Int = wrapped.length
override def apply(idx: Int): T = wrapped(idx)
}
class Mutable[A](initialValue: A) {
private val lock = new java.util.concurrent.locks.ReentrantReadWriteLock()

View file

@ -8,6 +8,7 @@ import org.bitcoins.core.protocol.blockchain.BlockHeader.TargetDifficultyHelper
import org.bitcoins.crypto.FieldElement
import scodec.bits.{BitVector, ByteVector}
import scala.annotation.tailrec
import scala.math.BigInt
import scala.util.{Failure, Success, Try}
@ -352,6 +353,125 @@ sealed abstract class NumberUtil extends BitcoinSLogger {
backwardsDigits.reverse
}
/** Recomposes the input digits into the number they represent.
* The input Vector has the most significant digit first and the 1's place last.
*/
def fromDigits(digits: Vector[Int], base: Int, numDigits: Int): Long = {
def pow(base: Long, exp: Long): Long = {
if (math.pow(base.toDouble, numDigits.toDouble) <= Int.MaxValue) {
math.pow(base.toDouble, exp.toDouble).toLong
} else { // For large numbers, Double loss of precision becomes an issue
def powRec(base: Long, currentExp: Long): Long = {
if (currentExp == 0) 1
else base * powRec(base, currentExp - 1)
}
powRec(base, exp)
}
}
digits.indices.foldLeft(0L) { (numSoFar, index) =>
numSoFar + digits(index) * pow(base, numDigits - 1 - index).toLong
}
}
/** Returns a pseudorandom, uniformly distributed long value between 0
* (inclusive) and the specified value (exclusive), drawn from this
* random number generator's sequence.
*
* Stolen from scala.util.Random.nextLong (in scala version 2.13)
* @see https://github.com/scala/scala/blob/4aae0b91cd266f02b9f3d911db49381a300b5103/src/library/scala/util/Random.scala#L131
*/
def randomLong(bound: Long): Long = {
require(bound > 0, "bound must be positive")
/*
* Divide bound by two until small enough for nextInt. On each
* iteration (at most 31 of them but usually much less),
* randomly choose both whether to include high bit in result
* (offset) and whether to continue with the lower vs upper
* half (which makes a difference only if odd).
*/
var offset = 0L
var _bound = bound
while (_bound >= Integer.MAX_VALUE) {
val bits = scala.util.Random.nextInt(2)
val halfn = _bound >>> 1
val nextn =
if ((bits & 2) == 0) halfn
else _bound - halfn
if ((bits & 1) == 0)
offset += _bound - nextn
_bound = nextn
}
offset + scala.util.Random.nextInt(_bound.toInt)
}
def randomBytes(num: Int): ByteVector = {
val bytes = new Array[Byte](0 max num)
scala.util.Random.self.nextBytes(bytes)
ByteVector(bytes)
}
def lexicographicalOrdering[T](implicit
ord: Ordering[T]): Ordering[Vector[T]] = {
new Ordering[Vector[T]] {
override def compare(x: Vector[T], y: Vector[T]): Int = {
val xe = x.iterator
val ye = y.iterator
while (xe.hasNext && ye.hasNext) {
val res = ord.compare(xe.next(), ye.next())
if (res != 0) return res
}
Ordering.Boolean.compare(xe.hasNext, ye.hasNext)
}
}
}
/** Stolen from Scala 2.13 IndexedSeq::binarySearch
* @see https://github.com/scala/scala/blob/4aae0b91cd266f02b9f3d911db49381a300b5103/src/library/scala/collection/IndexedSeq.scala#L117
*/
@tailrec
final def search[A, B >: A, Wrapper](
seq: IndexedSeq[Wrapper],
elem: B,
from: Int,
to: Int,
unwrap: Wrapper => A)(implicit ord: Ordering[B]): Int = {
if (from < 0) search(seq, elem, from = 0, to, unwrap)
else if (to > seq.length) search(seq, elem, from, seq.length, unwrap)
else if (to <= from) from
else {
val idx = from + (to - from - 1) / 2
math.signum(ord.compare(elem, unwrap(seq(idx)))) match {
case -1 => search(seq, elem, from, idx, unwrap)(ord)
case 1 => search(seq, elem, idx + 1, to, unwrap)(ord)
case _ => idx
}
}
}
def search[A, B >: A, Wrapper](
seq: IndexedSeq[Wrapper],
elem: B,
unwrap: Wrapper => A)(implicit ord: Ordering[B]): Int = {
search(seq, elem, from = 0, to = seq.length, unwrap)
}
def search[A, B >: A](seq: IndexedSeq[A], elem: B, from: Int, to: Int)(
implicit ord: Ordering[B]): Int = {
search(seq, elem, from, to, identity[A])
}
def search[A, B >: A](seq: IndexedSeq[A], elem: B)(implicit
ord: Ordering[B]): Int = {
search(seq, elem, from = 0, to = seq.length)
}
}
object NumberUtil extends NumberUtil

View file

@ -0,0 +1,8 @@
package org.bitcoins.core.util
trait SeqWrapper[+T] extends IndexedSeq[T] {
protected def wrapped: IndexedSeq[T]
override def iterator: Iterator[T] = wrapped.iterator
override def length: Int = wrapped.length
override def apply(idx: Int): T = wrapped(idx)
}

View file

@ -183,3 +183,10 @@ case class RawTxBuilderWithFinalizer[F <: RawTxFinalizer](
this
}
}
object RawTxBuilderWithFinalizer {
def apply[F <: RawTxFinalizer](finalizer: F): RawTxBuilderWithFinalizer[F] = {
RawTxBuilderWithFinalizer(RawTxBuilder(), finalizer)
}
}

View file

@ -5,7 +5,9 @@ import org.bitcoins.core.protocol.transaction.{
BaseTransaction,
Transaction,
TransactionInput,
TransactionOutput
TransactionOutput,
TransactionWitness,
WitnessTransaction
}
/** Raw Transaction to be finalized by a RawTxFinalizer */
@ -18,6 +20,10 @@ case class RawTxBuilderResult(
def toBaseTransaction: BaseTransaction = {
BaseTransaction(version, inputs, outputs, lockTime)
}
def toWitnessTransaction(witness: TransactionWitness): WitnessTransaction = {
WitnessTransaction(version, inputs, outputs, lockTime, witness)
}
}
object RawTxBuilderResult {

View file

@ -3,13 +3,13 @@ package org.bitcoins.core.wallet.builder
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.number.Int64
import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptSignature}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerVirtualByte}
import org.bitcoins.core.wallet.utxo.{InputInfo, InputSigningInfo}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Random, Success, Try}
import scala.util.{Failure, Random, Success, Try}
/** This trait is responsible for converting RawTxBuilderResults into
* finalized (unsigned) transactions. This process usually includes
@ -361,33 +361,105 @@ object ShufflingNonInteractiveFinalizer
}
}
case class SubtractFeeFromOutputsFinalizer(
inputInfos: Vector[InputInfo],
feeRate: FeeUnit)
/** Adds a an amount to the output with the given ScriptPubKey
* and subtracts that amount in equal proportions from the specified
* change ScriptPubKeys.
*
* This can be useful when you want to account for a future spending
* fee in this transaction to get nice output values on the spending tx.
*/
case class AddFutureFeeFinalizer(
spk: ScriptPubKey,
futureFee: CurrencyUnit,
changeSPKs: Vector[ScriptPubKey])
extends RawTxFinalizer {
override def buildTx(txBuilderResult: RawTxBuilderResult)(implicit
ec: ExecutionContext): Future[Transaction] = {
val RawTxBuilderResult(version, inputs, outputs, lockTime) = txBuilderResult
val changeOutputs = txBuilderResult.outputs.filter(output =>
changeSPKs.contains(output.scriptPubKey))
val witnesses = inputInfos.map(InputInfo.getScriptWitness)
val txWithPossibleWitness = TransactionWitness.fromWitOpt(witnesses) match {
case _: EmptyWitness =>
BaseTransaction(version, inputs, outputs, lockTime)
case wit: TransactionWitness =>
WitnessTransaction(version, inputs, outputs, lockTime, wit)
val outputT = txBuilderResult.outputs.zipWithIndex
.find(_._1.scriptPubKey == spk) match {
case Some((output, index)) =>
val newOutput = output.copy(value = output.value + futureFee)
Success((newOutput, index))
case None =>
Failure(new RuntimeException(
s"Did not find expected SPK $spk in ${txBuilderResult.outputs.map(_.scriptPubKey)}"))
}
val dummyTxF = TxUtil.addDummySigs(txWithPossibleWitness, inputInfos)
val outputsT = outputT.map {
case (newOutput, index) =>
val subFromChange =
Satoshis(futureFee.satoshis.toLong / changeOutputs.length)
val outputsMinusChange = txBuilderResult.outputs.map { output =>
if (changeSPKs.contains(output.scriptPubKey)) {
output.copy(value = output.value - subFromChange)
} else {
output
}
}
outputsMinusChange.updated(index, newOutput)
}
val txT = outputsT.map { outputs =>
txBuilderResult.toBaseTransaction.copy(outputs = outputs)
}
Future.fromTry(txT)
}
}
/** Subtracts the given fee from the output with the given ScriptPubKey.
* This can be useful if you are constructing a transaction without
* considering fees and have some after-the-fact external formula for
* computing fees and they need the removed.
*/
case class SubtractFromOutputFinalizer(spk: ScriptPubKey, subAmt: CurrencyUnit)
extends RawTxFinalizer {
override def buildTx(txBuilderResult: RawTxBuilderResult)(implicit
ec: ExecutionContext): Future[Transaction] = {
txBuilderResult.outputs.zipWithIndex.find(_._1.scriptPubKey == spk) match {
case Some((output, index)) =>
val newOutput = output.copy(value = output.value - subAmt)
val newOutputs = txBuilderResult.outputs.updated(index, newOutput)
Future.successful(
txBuilderResult.toBaseTransaction.copy(outputs = newOutputs))
case None =>
Future.failed(new RuntimeException(
s"Did not find expected SPK $spk in ${txBuilderResult.outputs.map(_.scriptPubKey)}"))
}
}
}
/** Assumes the input transaction has had no fee considerations
* and subtracts the estimated fee in equal portions from the
* outputs with the specified ScriptPubKeys
*/
case class SubtractFeeFromOutputsFinalizer(
inputInfos: Vector[InputInfo],
feeRate: FeeUnit,
spks: Vector[ScriptPubKey])
extends RawTxFinalizer {
override def buildTx(txBuilderResult: RawTxBuilderResult)(implicit
ec: ExecutionContext): Future[Transaction] = {
val dummyTxF =
TxUtil.addDummySigs(txBuilderResult.toBaseTransaction, inputInfos)
val outputsAfterFeeF = dummyTxF.map { dummyTx =>
SubtractFeeFromOutputsFinalizer.subtractFees(dummyTx,
feeRate,
outputs.map(_.scriptPubKey))
SubtractFeeFromOutputsFinalizer.subtractFees(
dummyTx,
feeRate,
spks
)
}
outputsAfterFeeF.map { outputsAfterFee =>
BaseTransaction(version, inputs, outputsAfterFee, lockTime)
txBuilderResult.toBaseTransaction.copy(outputs = outputsAfterFee)
}
}
}
@ -400,12 +472,10 @@ object SubtractFeeFromOutputsFinalizer {
spks: Vector[ScriptPubKey]): Vector[TransactionOutput] = {
val fee = feeRate.calc(tx)
val outputs = tx.outputs.zipWithIndex.filter {
case (output, _) => spks.contains(output.scriptPubKey)
}
val unchangedOutputs = tx.outputs.zipWithIndex.filterNot {
case (output, _) => spks.contains(output.scriptPubKey)
}
val (outputs, unchangedOutputs) =
tx.outputs.zipWithIndex.toVector.partition {
case (output, _) => spks.contains(output.scriptPubKey)
}
val feePerOutput = Satoshis(Int64(fee.satoshis.toLong / outputs.length))
val feeRemainder = Satoshis(Int64(fee.satoshis.toLong % outputs.length))
@ -422,7 +492,81 @@ object SubtractFeeFromOutputsFinalizer {
.dropRight(1)
.:+((newLastOutput, lastOutputIndex))
(newOutputs ++ unchangedOutputs).sortBy(_._2).map(_._1).toVector
(newOutputs ++ unchangedOutputs).sortBy(_._2).map(_._1)
}
}
case class DualFundingInput(
scriptSignature: ScriptSignature,
maxWitnessLen: Int)
/** Finalizes a dual-funded transaction given the DualFundingInputs
* from both parties, their change spks and the funding scriptpubkey
* for the dual funded protocol.
*
* This includes adding the future fee of spending transactions
* to the funding output as well as subtracting relevant fees
* from the change outputs. This finalizer filters dust outputs.
*/
case class DualFundingTxFinalizer(
offerInputs: Vector[DualFundingInput],
offerPayoutSPK: ScriptPubKey,
offerChangeSPK: ScriptPubKey,
acceptInputs: Vector[DualFundingInput],
acceptPayoutSPK: ScriptPubKey,
acceptChangeSPK: ScriptPubKey,
feeRate: SatoshisPerVirtualByte,
fundingSPK: ScriptPubKey)
extends RawTxFinalizer {
/** @see https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/Transactions.md#fees */
private def computeFees(
inputs: Vector[DualFundingInput],
payoutSPK: ScriptPubKey,
changeSPK: ScriptPubKey): (CurrencyUnit, CurrencyUnit) = {
// https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/Transactions.md#expected-weight-of-the-contract-execution-or-refund-transaction
val futureFeeWeight = 249 + 4 * payoutSPK.asmBytes.length
val futureFeeVBytes = Math.ceil(futureFeeWeight / 4.0).toLong
val futureFee = feeRate * futureFeeVBytes
// https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/Transactions.md#expected-weight-of-the-funding-transaction
val inputWeight =
inputs.foldLeft(0L) {
case (weight, DualFundingInput(scriptSignature, maxWitnessLen)) =>
weight + 164 + 4 * scriptSignature.asmBytes.length + maxWitnessLen.toInt
}
val outputWeight = 36 + 4 * changeSPK.asmBytes.length
val weight = 107 + outputWeight + inputWeight
val vbytes = Math.ceil(weight / 4.0).toLong
val fundingFee = feeRate * vbytes
(futureFee, fundingFee)
}
lazy val (offerFutureFee, offerFundingFee) =
computeFees(offerInputs, offerPayoutSPK, offerChangeSPK)
lazy val (acceptFutureFee, acceptFundingFee) =
computeFees(acceptInputs, acceptPayoutSPK, acceptChangeSPK)
override def buildTx(txBuilderResult: RawTxBuilderResult)(implicit
ec: ExecutionContext): Future[Transaction] = {
val addOfferFutureFee =
AddFutureFeeFinalizer(fundingSPK, offerFutureFee, Vector(offerChangeSPK))
val addAcceptFutureFee = AddFutureFeeFinalizer(fundingSPK,
acceptFutureFee,
Vector(acceptChangeSPK))
val subtractOfferFundingFee =
SubtractFromOutputFinalizer(offerChangeSPK, offerFundingFee)
val subtractAcceptFundingFee =
SubtractFromOutputFinalizer(acceptChangeSPK, acceptFundingFee)
addOfferFutureFee
.andThen(addAcceptFutureFee)
.andThen(subtractOfferFundingFee)
.andThen(subtractAcceptFundingFee)
.andThen(FilterDustFinalizer)
.buildTx(txBuilderResult)
}
}

View file

@ -168,6 +168,8 @@ case class SatoshisPerVirtualByte(currencyUnit: CurrencyUnit)
SatoshisPerVirtualByte
override def toString: String = s"$toLong sats/vbyte"
lazy val toSatoshisPerKW: SatoshisPerKW = SatoshisPerKW(currencyUnit * 4000)
}
object SatoshisPerVirtualByte extends FeeUnitFactory[SatoshisPerVirtualByte] {

View file

@ -1,10 +1,19 @@
package org.bitcoins.core.wallet.utxo
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt64
import org.bitcoins.core.protocol.CompactSizeUInt
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.constant.ScriptConstant
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.crypto.{ECPublicKey, NetworkElement, Sign}
import org.bitcoins.core.util.{BitcoinScriptUtil, BytesUtil}
import org.bitcoins.crypto.{
ECPublicKey,
LowRDummyECDigitalSignature,
NetworkElement,
Sign
}
import scala.annotation.tailrec
@ -163,6 +172,95 @@ object InputInfo {
(preImages, conditionalPath)
}
/** Returns the maximum byteSize of any resulting ScriptSignature */
def maxScriptSigLen(info: InputInfo): Int = {
val asmByteSize =
maxScriptSigLenAndStackHeight(info, forP2WSH = false).scriptSigLen
val varIntSize = CompactSizeUInt(UInt64(asmByteSize)).byteSize
asmByteSize + varIntSize.toInt
}
case class ScriptSigLenAndStackHeight(scriptSigLen: Int, stackHeight: Int)
/** Computes the byteSize of witness/scriptSignature for the given info,
* and also returns the number of stack elements for the P2WSH case.
*/
private def maxScriptSigLenAndStackHeight(
info: InputInfo,
forP2WSH: Boolean): ScriptSigLenAndStackHeight = {
val boolSize = if (forP2WSH) 2 else 1
info match {
case _: SegwitV0NativeInputInfo | _: UnassignedSegwitNativeInputInfo =>
ScriptSigLenAndStackHeight(0, 0)
case info: P2SHInputInfo =>
val serializedRedeemScript = ScriptConstant(info.redeemScript.asmBytes)
val pushOps = BitcoinScriptUtil.calculatePushOp(serializedRedeemScript)
val redeemScriptLen =
BytesUtil
.toByteVector(pushOps.:+(serializedRedeemScript))
.length
.toInt
val ScriptSigLenAndStackHeight(scriptSigLen, stackHeight) =
maxScriptSigLenAndStackHeight(info.nestedInputInfo, forP2WSH)
ScriptSigLenAndStackHeight(redeemScriptLen + scriptSigLen,
stackHeight + 1)
case _: EmptyInputInfo =>
ScriptSigLenAndStackHeight(boolSize, 1)
case _: P2PKInputInfo =>
ScriptSigLenAndStackHeight(
P2PKScriptSignature(
LowRDummyECDigitalSignature).asmBytes.length.toInt,
1)
case _: P2PKHInputInfo =>
ScriptSigLenAndStackHeight(
P2PKHScriptSignature(LowRDummyECDigitalSignature,
ECPublicKey.dummy).asmBytes.length.toInt,
2)
case info: P2PKWithTimeoutInputInfo =>
ScriptSigLenAndStackHeight(
P2PKWithTimeoutScriptSignature(
info.isBeforeTimeout,
LowRDummyECDigitalSignature).asmBytes.length.toInt,
2)
case info: MultiSignatureInputInfo =>
ScriptSigLenAndStackHeight(
MultiSignatureScriptSignature(
Vector.fill(info.requiredSigs)(
LowRDummyECDigitalSignature)).asmBytes.length.toInt,
1 + info.requiredSigs)
case info: ConditionalInputInfo =>
val ScriptSigLenAndStackHeight(maxLen, stackHeight) =
maxScriptSigLenAndStackHeight(info.nestedInputInfo, forP2WSH)
ScriptSigLenAndStackHeight(maxLen + boolSize, stackHeight + 1)
case info: LockTimeInputInfo =>
maxScriptSigLenAndStackHeight(info.nestedInputInfo, forP2WSH)
}
}
/** Returns the maximum byteSize of any resulting ScriptWitness */
@tailrec
def maxWitnessLen(info: InputInfo): Int = {
info match {
case _: RawInputInfo | _: P2SHNonSegwitInputInfo => 0
case _: P2WPKHV0InputInfo => 107
case info: P2WSHV0InputInfo =>
val ScriptSigLenAndStackHeight(scriptSigLen, stackHeight) =
maxScriptSigLenAndStackHeight(info.nestedInputInfo, forP2WSH = true)
val stackHeightByteSize = CompactSizeUInt(UInt64(stackHeight)).byteSize
val redeemScriptSize = info.scriptWitness.redeemScript.byteSize
(stackHeightByteSize + redeemScriptSize + scriptSigLen).toInt
case info: P2SHNestedSegwitV0InputInfo =>
maxWitnessLen(info.nestedInputInfo)
case _: UnassignedSegwitNativeInputInfo =>
throw new IllegalArgumentException(
s"Cannot compute witness for unknown segwit InputInfo, got $info")
}
}
def apply(
outPoint: TransactionOutPoint,
output: TransactionOutput,

View file

@ -1,16 +1,15 @@
package org.bitcoins.core.wallet.utxo
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.script.{
SigVersionBase,
SigVersionWitnessV0,
SignatureVersion
}
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.signer.BitcoinSigner
import org.bitcoins.crypto.Sign
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext}
/** Stores the information required to generate a signature (ECSignatureParams)
* or to generate a script signature (ScriptSignatureParams) for a given satisfaction
* condition on a UTXO.
@ -88,33 +87,9 @@ case class ScriptSignatureParams[+InputType <: InputInfo](
this.copy(inputInfo = func(this.inputInfo))
}
def maxScriptSigAndWitnessWeight(implicit
ec: ExecutionContext): (Long, Long) = {
val dummyTx = BaseTransaction(
TransactionConstants.validLockVersion,
Vector(
TransactionInput(inputInfo.outPoint,
EmptyScriptSignature,
UInt32.zero)),
Vector(TransactionOutput(Satoshis.zero, EmptyScriptPubKey)),
UInt32.zero
)
lazy val maxWitnessLen: Int = InputInfo.maxWitnessLen(inputInfo)
val maxWitnessLenF = BitcoinSigner
.sign(this, unsignedTx = dummyTx, isDummySignature = true)
.map(_.transaction)
.map {
case wtx: WitnessTransaction =>
val scriptSigSize = wtx.inputs.head.scriptSignature.asmBytes.size
val witnessSize = wtx.witness.head.byteSize
(scriptSigSize * 4, witnessSize)
case tx: NonWitnessTransaction =>
val scriptSigSize = tx.inputs.head.scriptSignature.asmBytes.size
(scriptSigSize * 4, 0L)
}
Await.result(maxWitnessLenF, 30.seconds)
}
lazy val maxScriptSigLen: Int = InputInfo.maxScriptSigLen(inputInfo)
}
object ScriptSignatureParams {

View file

@ -44,8 +44,15 @@ object ECAdaptorSignature extends Factory[ECAdaptorSignature] {
)
}
def empty(): ECAdaptorSignature =
fromBytes(ByteVector.fill(162)(0.toByte))
lazy val dummy: ECAdaptorSignature = {
ECAdaptorSignature(
ECPublicKey.freshPublicKey,
ECPrivateKey.freshPrivateKey.fieldElement,
ECPublicKey.freshPublicKey,
ECPrivateKey.freshPrivateKey.fieldElement,
ECPrivateKey.freshPrivateKey.fieldElement
)
}
def serializePoint(point: ECPublicKey): ByteVector = {
val (sign, xCoor) = point.bytes.splitAt(1)

View file

@ -137,6 +137,24 @@ object ECDigitalSignature extends Factory[ECDigitalSignature] {
}
}
/** Reads a (DER encoded) ECDigitalSignature from the front of a ByteVector */
def fromFrontOfBytes(bytes: ByteVector): ECDigitalSignature = {
val sigWithExtra = fromBytes(bytes)
val sig = fromRS(sigWithExtra.r, sigWithExtra.s)
require(bytes.startsWith(sig.bytes),
s"Received weirdly encoded signature at beginning of $bytes")
sig
}
/** Reads a (DER encoded with sighash) ECDigitalSignature from the front of a ByteVector */
def fromFrontOfBytesWithSigHash(bytes: ByteVector): ECDigitalSignature = {
val sigWithoutSigHash = fromFrontOfBytes(bytes)
ECDigitalSignature(
sigWithoutSigHash.bytes :+ bytes.drop(sigWithoutSigHash.byteSize).head)
}
def apply(r: BigInt, s: BigInt): ECDigitalSignature = fromRS(r, s)
/**

View file

@ -599,6 +599,8 @@ object ECPublicKey extends Factory[ECPublicKey] {
def apply(): ECPublicKey = freshPublicKey
val dummy: ECPublicKey = FieldElement.one.getPublicKey
/** Generates a fresh [[org.bitcoins.crypto.ECPublicKey ECPublicKey]] that has not been used before. */
def freshPublicKey: ECPublicKey = ECPrivateKey.freshPrivateKey.publicKey

View file

@ -105,6 +105,8 @@ object Sha256DigestBE extends Factory[Sha256DigestBE] {
"Sha256Digest must be 32 bytes in size, got: " + bytes.length)
Sha256DigestBEImpl(bytes)
}
lazy val empty: Sha256DigestBE = Sha256DigestBE(ByteVector.low(32))
}
/**

View file

@ -111,7 +111,6 @@ bitcoin-s {
rpcport = 9999
}
oracle = ${bitcoin-s.sqlite}
oracle {
# this config key is read by Slick
@ -140,6 +139,13 @@ bitcoin-s {
akka {
# Let Cli commands take 30 seconds
http.server.request-timeout = 30s
# Can parse 20MBs
http.server.parsing.max-content-length = 20m
http.client.parsing.max-content-length = 20m
# Loggers to register at boot time (akka.event.Logging$DefaultLogger logs
# to STDOUT)
loggers = ["akka.event.slf4j.Slf4jLogger"]

View file

@ -1,7 +1,7 @@
package org.bitcoins.db
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.ContractInfo
import org.bitcoins.commons.jsonmodels.dlc.SigningVersion
import org.bitcoins.commons.jsonmodels.dlc.{DLCState, SigningVersion}
import org.bitcoins.commons.jsonmodels.wallet.{
WalletStateDescriptor,
WalletStateDescriptorType
@ -121,6 +121,9 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) {
implicit val schnorrNonceMapper: BaseColumnType[SchnorrNonce] =
MappedColumnType.base[SchnorrNonce, String](_.hex, SchnorrNonce.fromHex)
implicit val oracleInfoTLVMapper: BaseColumnType[OracleInfoTLV] =
MappedColumnType.base[OracleInfoTLV, String](_.hex, OracleInfoTLV.fromHex)
implicit val sha256Hash160DigestMapper: BaseColumnType[Sha256Hash160Digest] =
MappedColumnType
.base[Sha256Hash160Digest, String](_.hex, Sha256Hash160Digest.fromHex)
@ -260,6 +263,37 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) {
.base[ContractInfo, String](_.hex, ContractInfo.fromHex)
}
implicit val contractInfoTLVMapper: BaseColumnType[ContractInfoTLV] = {
MappedColumnType
.base[ContractInfoTLV, String](_.hex, ContractInfoTLV.fromHex)
}
implicit val dlcOutcomeTypeMapper: BaseColumnType[DLCOutcomeType] = {
val enumStr = "Enum:"
val unsignedNumStr = "Unsigned:"
MappedColumnType.base[DLCOutcomeType, String](
{
case EnumOutcome(outcome) =>
s"$enumStr$outcome"
case UnsignedNumericOutcome(digits) =>
s"$unsignedNumStr" + digits.mkString("|")
},
str => {
if (str.startsWith(enumStr)) {
EnumOutcome(str.drop(enumStr.length))
} else if (str.startsWith(unsignedNumStr)) {
val data = str.drop(unsignedNumStr.length)
val strVec = data.split('|')
val ints = strVec.map(_.toInt)
UnsignedNumericOutcome(ints.toVector)
} else
throw new RuntimeException("Unknown outcome type")
}
)
}
implicit val blockStampWithFutureMapper: BaseColumnType[BlockTimeStamp] = {
MappedColumnType.base[BlockTimeStamp, Long](
_.toUInt32.toLong,
@ -300,6 +334,18 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) {
SchnorrDigitalSignature.fromHex)
}
implicit val schnorrDigitalSignatureVecMapper: BaseColumnType[
Vector[SchnorrDigitalSignature]] = {
MappedColumnType.base[Vector[SchnorrDigitalSignature], String](
_.foldLeft("")(_ ++ _.hex),
_.toArray
.grouped(64 * 2)
.map(new String(_))
.map(SchnorrDigitalSignature.fromHex)
.toVector
)
}
implicit val walletStateDescriptorTypeMapper: BaseColumnType[
WalletStateDescriptorType] =
MappedColumnType.base[WalletStateDescriptorType, String](
@ -312,6 +358,17 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) {
_.toString,
WalletStateDescriptor.fromString)
implicit val ecAdaptorSignatureMapper: BaseColumnType[ECAdaptorSignature] = {
MappedColumnType.base[ECAdaptorSignature, String](
_.hex,
ECAdaptorSignature.fromHex)
}
implicit val dlcStateMapper: BaseColumnType[DLCState] = {
MappedColumnType
.base[DLCState, String](_.toString, DLCState.fromString)
}
implicit val eventDescriptorTLVMapper: BaseColumnType[EventDescriptorTLV] = {
MappedColumnType.base[EventDescriptorTLV, String](
_.hex,

View file

@ -37,6 +37,8 @@ object Deps {
val scalaCollectionCompatV = "2.3.1"
val pgEmbeddedV = "0.13.3"
val breezeV = "1.1"
val newMicroPickleV = "0.8.0"
val newMicroJsonV = newMicroPickleV
@ -123,6 +125,10 @@ object Deps {
javaFxSwing,
javaFxWeb)
val breezeViz =
("org.scalanlp" %% "breeze-viz" % V.breezeV withSources () withJavadoc ())
.exclude("bouncycastle", "bcprov-jdk14")
val playJson =
"com.typesafe.play" %% "play-json" % V.playv withSources () withJavadoc ()
@ -309,7 +315,7 @@ object Deps {
Compile.codehaus
)
val gui = List(Compile.scalaFx) ++ Compile.javaFxDeps
val gui = List(Compile.breezeViz, Compile.scalaFx) ++ Compile.javaFxDeps
def server(scalaVersion: String) =
List(

View file

@ -12,6 +12,7 @@ import org.bitcoins.crypto.{
AesPassword,
CryptoUtil,
DoubleSha256Digest,
DoubleSha256DigestBE,
ECAdaptorSignature,
ECDigitalSignature,
ECPrivateKey,
@ -21,6 +22,7 @@ import org.bitcoins.crypto.{
SchnorrNonce,
SchnorrPublicKey,
Sha256Digest,
Sha256DigestBE,
Sha256Hash160Digest
}
import org.scalacheck.Gen
@ -222,6 +224,15 @@ sealed abstract class CryptoGenerators {
hash <- CryptoGenerators.doubleSha256Digest
} yield privKey.sign(hash)
def digitalSignatureWithSigHash: Gen[ECDigitalSignature] = {
for {
sig <- digitalSignature
sigHash <- hashType
} yield {
ECDigitalSignature(sig.bytes :+ sigHash.byte)
}
}
def schnorrDigitalSignature: Gen[SchnorrDigitalSignature] = {
for {
privKey <- privateKey
@ -247,6 +258,10 @@ sealed abstract class CryptoGenerators {
digest = CryptoUtil.sha256(bytes)
} yield digest
def sha256DigestBE: Gen[Sha256DigestBE] = {
sha256Digest.map(_.flip)
}
/** Generates a random [[DoubleSha256Digest DoubleSha256Digest]] */
def doubleSha256Digest: Gen[DoubleSha256Digest] =
for {
@ -254,6 +269,10 @@ sealed abstract class CryptoGenerators {
digest = CryptoUtil.doubleSHA256(key.bytes)
} yield digest
def doubleSha256DigestBE: Gen[DoubleSha256DigestBE] = {
doubleSha256Digest.map(_.flip)
}
/**
* Generates a sequence of [[DoubleSha256Digest DoubleSha256Digest]]
* @param num the number of digets to generate

View file

@ -1,11 +1,6 @@
package org.bitcoins.testkit.core.gen
import org.bitcoins.core.currency.{
Bitcoins,
CurrencyUnit,
CurrencyUnits,
Satoshis
}
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, Satoshis}
import org.bitcoins.core.protocol.ln.currency._
import org.scalacheck.Gen
@ -24,7 +19,7 @@ trait CurrencyUnitGenerator {
def currencyUnit: Gen[CurrencyUnit] = Gen.oneOf(satoshis, bitcoins)
def positiveSatoshis: Gen[Satoshis] =
satoshis.suchThat(_ >= CurrencyUnits.zero)
Gen.choose(0, Long.MaxValue).map(Satoshis.apply)
/**
* Generates a postiive satoshi value that is 'realistic'. This current 'realistic' range

View file

@ -0,0 +1,68 @@
package org.bitcoins.testkit.core.gen
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.core.protocol.tlv._
import org.scalacheck.Gen
trait LnMessageGen extends TLVGen {
override def unknownTpe: Gen[BigSizeUInt] = {
NumberGenerator.uInt16
.map(num => BigSizeUInt(num.toInt))
.suchThat(!TLV.knownTypes.contains(_))
}
def unknownMessage: Gen[LnMessage[UnknownTLV]] = {
unknownTLV.map(LnMessage.apply)
}
def errorMessage: Gen[LnMessage[ErrorTLV]] = {
errorTLV.map(LnMessage.apply)
}
def pingMessage: Gen[LnMessage[PingTLV]] = {
pingTLV.map(LnMessage.apply)
}
def pongMessage: Gen[LnMessage[PongTLV]] = {
pongTLV.map(LnMessage.apply)
}
def dlcOfferMessage: Gen[LnMessage[DLCOfferTLV]] = {
dlcOfferTLV.map(LnMessage.apply)
}
def dlcAcceptMessage: Gen[LnMessage[DLCAcceptTLV]] = {
dlcAcceptTLV.map(LnMessage.apply)
}
def dlcAcceptMessage(offer: DLCOfferTLV): Gen[LnMessage[DLCAcceptTLV]] = {
dlcAcceptTLV(offer).map(LnMessage.apply)
}
def dlcOfferMessageAcceptMessage: Gen[
(LnMessage[DLCOfferTLV], LnMessage[DLCAcceptTLV])] = {
dlcOfferTLVAcceptTLV.map {
case (offer, accept) =>
(LnMessage(offer), LnMessage(accept))
}
}
def dlcSignMessage: Gen[LnMessage[DLCSignTLV]] = {
dlcSignTLV.map(LnMessage.apply)
}
def lnMessage: Gen[LnMessage[TLV]] = {
Gen.oneOf(
unknownMessage,
errorMessage,
pingMessage,
pongMessage,
dlcOfferMessage,
dlcAcceptMessage,
dlcSignMessage
)
}
}
object LnMessageGen extends LnMessageGen

View file

@ -1,7 +1,20 @@
package org.bitcoins.testkit.core.gen
import org.bitcoins.core.protocol.BigSizeUInt
import org.bitcoins.commons.jsonmodels.dlc.DLCFundingInputP2WPKHV0
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.{
ContractInfo,
DLCOffer,
SingleNonceContractInfo
}
import org.bitcoins.core.config.Networks
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.{BigSizeUInt, BlockTimeStamp}
import org.bitcoins.core.util.NumberUtil
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto.ECPrivateKey
import org.scalacheck.Gen
trait TLVGen {
@ -98,6 +111,277 @@ trait TLVGen {
} yield OracleAnnouncementV0TLV(sig, pubkey, eventTLV)
}
def contractInfoV0TLV: Gen[ContractInfoV0TLV] = {
def genValues(size: Int, totalAmount: CurrencyUnit): Vector[Satoshis] = {
val vals = if (size < 2) {
throw new IllegalArgumentException(
s"Size must be at least two, got $size")
} else if (size == 2) {
Vector(totalAmount.satoshis, Satoshis.zero)
} else {
(0 until size - 2).map { _ =>
Satoshis(NumberUtil.randomLong(totalAmount.satoshis.toLong))
}.toVector :+ totalAmount.satoshis :+ Satoshis.zero
}
val valsWithOrder = vals.map(_ -> scala.util.Random.nextDouble())
valsWithOrder.sortBy(_._2).map(_._1)
}
def genContractInfos(outcomes: Vector[String], totalInput: CurrencyUnit): (
SingleNonceContractInfo,
SingleNonceContractInfo) = {
val outcomeMap =
outcomes
.map(EnumOutcome.apply)
.zip(genValues(outcomes.length, totalInput))
val info = SingleNonceContractInfo(outcomeMap)
val remoteInfo = info.flip(totalInput.satoshis)
(info, remoteInfo)
}
for {
numOutcomes <- Gen.choose(2, 10)
outcomes <- Gen.listOfN(numOutcomes, StringGenerators.genString)
totalInput <-
Gen
.choose(numOutcomes + 1, Long.MaxValue / 10000L)
.map(Satoshis.apply)
(contractInfo, _) = genContractInfos(outcomes.toVector, totalInput)
} yield {
ContractInfoV0TLV(contractInfo.outcomeValueMap.map {
case (outcome, amt) => outcome.outcome -> amt
})
}
}
def oracleInfoV0TLV: Gen[OracleInfoV0TLV] = {
for {
pubKey <- CryptoGenerators.schnorrPublicKey
rValue <- CryptoGenerators.schnorrNonce
} yield OracleInfoV0TLV(pubKey, rValue)
}
def oracleInfoV0TLVWithKeys: Gen[
(OracleInfoV0TLV, ECPrivateKey, ECPrivateKey)] = {
for {
privKey <- CryptoGenerators.privateKey
kValue <- CryptoGenerators.privateKey
} yield {
(OracleInfoV0TLV(privKey.schnorrPublicKey, kValue.schnorrNonce),
privKey,
kValue)
}
}
def fundingInputP2WPKHTLV: Gen[FundingInputV0TLV] = {
for {
prevTx <- TransactionGenerators.realisticTransactionWitnessOut
prevTxVout <- Gen.choose(0, prevTx.outputs.length - 1)
sequence <- NumberGenerator.uInt32s
(spk, _) <- ScriptGenerators.p2wpkhSPKV0
newOutput = prevTx.outputs(prevTxVout).copy(scriptPubKey = spk)
newPrevTx = prevTx match {
case transaction: NonWitnessTransaction =>
BaseTransaction(transaction.version,
transaction.inputs,
transaction.outputs.updated(prevTxVout, newOutput),
transaction.lockTime)
case wtx: WitnessTransaction =>
wtx.copy(outputs = wtx.outputs.updated(prevTxVout, newOutput))
}
} yield {
DLCFundingInputP2WPKHV0(newPrevTx, UInt32(prevTxVout), sequence).toTLV
}
}
def fundingInputV0TLV: Gen[FundingInputV0TLV] = {
fundingInputP2WPKHTLV // Soon to be Gen.oneOf
}
def fundingInputV0TLVs(
collateralNeeded: CurrencyUnit): Gen[Vector[FundingInputV0TLV]] = {
for {
numInputs <- Gen.choose(0, 5)
inputs <- Gen.listOfN(numInputs, fundingInputV0TLV)
input <- fundingInputV0TLV
} yield {
val totalFunding =
inputs.foldLeft[CurrencyUnit](Satoshis.zero)(_ + _.output.value)
if (totalFunding <= collateralNeeded) {
val output = input.prevTx.outputs(input.prevTxVout.toInt)
val newOutput =
output.copy(value = collateralNeeded - totalFunding + Bitcoins.one)
val newOutputs =
input.prevTx.outputs.updated(input.prevTxVout.toInt, newOutput)
val newPrevTx = input.prevTx match {
case tx: BaseTransaction =>
tx.copy(outputs = newOutputs)
case wtx: WitnessTransaction =>
wtx.copy(outputs = newOutputs)
case EmptyTransaction =>
throw new RuntimeException(
"FundingInputV0TLV generator malfunction")
}
inputs.toVector :+ input.copy(prevTx = newPrevTx)
} else {
inputs.toVector
}
}
}
def cetSignaturesV0TLV: Gen[CETSignaturesV0TLV] = {
Gen
.listOf(CryptoGenerators.adaptorSignature)
.map(sigs => CETSignaturesV0TLV(sigs.toVector))
}
def cetSignaturesV0TLV(numCETs: Int): Gen[CETSignaturesV0TLV] = {
Gen
.listOfN(numCETs, CryptoGenerators.adaptorSignature)
.map(sigs => CETSignaturesV0TLV(sigs.toVector))
}
def fundingSignaturesV0TLV: Gen[FundingSignaturesV0TLV] = {
Gen.choose(1, 10).flatMap(fundingSignaturesV0TLV)
}
def fundingSignaturesV0TLV(numWitnesses: Int): Gen[FundingSignaturesV0TLV] = {
Gen
.listOfN(
numWitnesses,
WitnessGenerators.p2wpkhWitnessV0 // TODO: make more general
)
.map(witnesses => FundingSignaturesV0TLV(witnesses.toVector))
}
def dlcOfferTLV: Gen[DLCOfferTLV] = {
for {
chainHash <- Gen.oneOf(
Networks.knownNetworks.map(_.chainParams.genesisBlock.blockHeader.hash))
contractInfo <- contractInfoV0TLV
oracleInfo <- oracleInfoV0TLV
fundingPubKey <- CryptoGenerators.publicKey
payoutAddress <- AddressGenerator.bitcoinAddress
totalCollateralSatoshis <- CurrencyUnitGenerator.positiveRealistic
fundingInputs <- fundingInputV0TLVs(totalCollateralSatoshis)
changeAddress <- AddressGenerator.bitcoinAddress
feeRate <- CurrencyUnitGenerator.positiveRealistic.map(
SatoshisPerVirtualByte.apply)
timeout1 <- NumberGenerator.uInt32s
timeout2 <- NumberGenerator.uInt32s
} yield {
val (contractMaturityBound, contractTimeout) = if (timeout1 < timeout2) {
(BlockTimeStamp(timeout1), BlockTimeStamp(timeout2))
} else {
(BlockTimeStamp(timeout2), BlockTimeStamp(timeout1))
}
DLCOfferTLV(
0.toByte,
chainHash,
contractInfo,
oracleInfo,
fundingPubKey,
payoutAddress.scriptPubKey,
totalCollateralSatoshis,
fundingInputs,
changeAddress.scriptPubKey,
feeRate,
contractMaturityBound,
contractTimeout
)
}
}
def dlcOfferTLVWithOracleKeys: Gen[
(DLCOfferTLV, ECPrivateKey, ECPrivateKey)] = {
for {
offer <- dlcOfferTLV
(oracleInfo, oraclePrivKey, oracleRValue) <- oracleInfoV0TLVWithKeys
} yield {
(offer.copy(oracleInfo = oracleInfo), oraclePrivKey, oracleRValue)
}
}
def dlcAcceptTLV: Gen[DLCAcceptTLV] = {
for {
tempContractId <- CryptoGenerators.sha256Digest
totalCollateralSatoshis <- CurrencyUnitGenerator.positiveRealistic
fundingPubKey <- CryptoGenerators.publicKey
payoutAddress <- AddressGenerator.bitcoinAddress
fundingInputs <- fundingInputV0TLVs(totalCollateralSatoshis)
changeAddress <- AddressGenerator.bitcoinAddress
cetSigs <- cetSignaturesV0TLV
refundSig <- CryptoGenerators.digitalSignature
} yield {
DLCAcceptTLV(tempContractId,
totalCollateralSatoshis,
fundingPubKey,
payoutAddress.scriptPubKey,
fundingInputs,
changeAddress.scriptPubKey,
cetSigs,
refundSig)
}
}
def dlcAcceptTLV(offer: DLCOfferTLV): Gen[DLCAcceptTLV] = {
val contractInfo = ContractInfo.fromTLV(offer.contractInfo)
for {
fundingPubKey <- CryptoGenerators.publicKey
payoutAddress <- AddressGenerator.bitcoinAddress
totalCollateralSatoshis <- CurrencyUnitGenerator.positiveRealistic
totalCollateral = scala.math.max(
(contractInfo.max - offer.totalCollateralSatoshis).satoshis.toLong,
totalCollateralSatoshis.toLong)
fundingInputs <- fundingInputV0TLVs(Satoshis(totalCollateral))
changeAddress <- AddressGenerator.bitcoinAddress
cetSigs <- cetSignaturesV0TLV(contractInfo.allOutcomes.length)
refundSig <- CryptoGenerators.digitalSignature
} yield {
DLCAcceptTLV(
DLCOffer.fromTLV(offer).tempContractId,
Satoshis(totalCollateral),
fundingPubKey,
payoutAddress.scriptPubKey,
fundingInputs,
changeAddress.scriptPubKey,
cetSigs,
refundSig
)
}
}
def dlcOfferTLVAcceptTLV: Gen[(DLCOfferTLV, DLCAcceptTLV)] = {
for {
offer <- dlcOfferTLV
accept <- dlcAcceptTLV(offer)
} yield (offer, accept)
}
def dlcOfferTLVAcceptTLVWithOracleKeys: Gen[
(DLCOfferTLV, DLCAcceptTLV, ECPrivateKey, ECPrivateKey)] = {
for {
(offer, privKey, kVal) <- dlcOfferTLVWithOracleKeys
accept <- dlcAcceptTLV(offer)
} yield (offer, accept, privKey, kVal)
}
def dlcSignTLV: Gen[DLCSignTLV] = {
for {
contractId <- NumberGenerator.bytevector(32)
cetSigs <- cetSignaturesV0TLV
refundSig <- CryptoGenerators.digitalSignature
fundingSigs <- fundingSignaturesV0TLV
} yield {
DLCSignTLV(contractId, cetSigs, refundSig, fundingSigs)
}
}
def tlv: Gen[TLV] = {
Gen.oneOf(
unknownTLV,
@ -105,7 +389,16 @@ trait TLVGen {
pingTLV,
pongTLV,
oracleEventV0TLV,
eventDescriptorTLV
eventDescriptorTLV,
oracleAnnouncementV0TLV,
contractInfoV0TLV,
oracleInfoV0TLV,
fundingInputV0TLV,
cetSignaturesV0TLV,
fundingSignaturesV0TLV,
dlcOfferTLV,
dlcAcceptTLV,
dlcSignTLV
)
}
}

View file

@ -54,7 +54,18 @@ object TransactionGenerators extends BitcoinSLogger {
}
def realisticOutputs: Gen[Seq[TransactionOutput]] =
Gen.choose(0, 5).flatMap(n => Gen.listOfN(n, realisticOutput))
Gen.choose(1, 5).flatMap(n => Gen.listOfN(n, realisticOutput))
def realisticWitnessOutput: Gen[TransactionOutput] = {
CurrencyUnitGenerator.positiveRealistic.flatMap { amt =>
ScriptGenerators.witnessScriptPubKeyV0.map(spk =>
TransactionOutput(amt, spk._1))
}
}
def realisticWitnessOutputs: Gen[Seq[TransactionOutput]] = {
Gen.choose(1, 5).flatMap(n => Gen.listOfN(n, realisticWitnessOutput))
}
/** Generates a small list of TX outputs paying to the given SPK */
def smallOutputsTo(spk: ScriptPubKey): Gen[Seq[TransactionOutput]] =
@ -150,6 +161,20 @@ object TransactionGenerators extends BitcoinSLogger {
def smallInputsNonEmpty: Gen[Seq[TransactionInput]] =
Gen.choose(1, 5).flatMap(i => Gen.listOfN(i, input))
def outputReference: Gen[OutputReference] = {
for {
outPoint <- outPoint
output <- output
} yield OutputReference(outPoint, output)
}
def realisticOutputReference: Gen[OutputReference] = {
for {
outPoint <- outPoint
output <- realisticOutput
} yield OutputReference(outPoint, output)
}
/**
* Generates an arbitrary [[org.bitcoins.core.protocol.transaction.Transaction Transaction]]
* This transaction's [[org.bitcoins.core.protocol.transaction.TransactionInput TransactionInput]]s
@ -169,6 +194,13 @@ object TransactionGenerators extends BitcoinSLogger {
def transaction: Gen[Transaction] =
Gen.oneOf(baseTransaction, witnessTransaction)
def realisticTransaction: Gen[Transaction] =
Gen.oneOf(realisticBaseTransaction, realisiticWitnessTransaction)
def realisticTransactionWitnessOut: Gen[Transaction] =
Gen.oneOf(realisticBaseTransactionWitnessOut,
realisiticWitnessTransactionWitnessOut)
/** Generates a transaction where at least one output pays to the given SPK */
def transactionTo(spk: ScriptPubKey) =
Gen.oneOf(baseTransactionTo(spk), witnessTransactionTo(spk))
@ -185,6 +217,24 @@ object TransactionGenerators extends BitcoinSLogger {
lockTime <- NumberGenerator.uInt32s
} yield BaseTransaction(version, is, os, lockTime)
def realisticBaseTransaction: Gen[BaseTransaction] = {
for {
version <- NumberGenerator.int32s
is <- smallInputs
os <- realisticOutputs
lockTime <- NumberGenerator.uInt32s
} yield BaseTransaction(version, is, os, lockTime)
}
def realisticBaseTransactionWitnessOut: Gen[BaseTransaction] = {
for {
version <- NumberGenerator.int32s
is <- smallInputs
os <- realisticWitnessOutputs
lockTime <- NumberGenerator.uInt32s
} yield BaseTransaction(version, is, os, lockTime)
}
/** Generates a legacy transaction with at least one output paying to the given SPK */
def baseTransactionTo(spk: ScriptPubKey): Gen[BaseTransaction] =
for {
@ -221,6 +271,18 @@ object TransactionGenerators extends BitcoinSLogger {
tx <- witnessTxHelper(os)
} yield tx
def realisiticWitnessTransaction: Gen[WitnessTransaction] =
for {
os <- realisticOutputs
tx <- witnessTxHelper(os)
} yield tx
def realisiticWitnessTransactionWitnessOut: Gen[WitnessTransaction] =
for {
os <- realisticWitnessOutputs
tx <- witnessTxHelper(os)
} yield tx
/** Generates a SegWit TX where at least one output pays to the given SPK */
def witnessTransactionTo(spk: ScriptPubKey): Gen[WitnessTransaction] = {
for {

View file

@ -236,6 +236,58 @@ trait BaseAsyncTest
}
}
/** Runs all property based tests in parallel. This is a convenient optimization
* for synchronous property based tests
*/
def forAllParallel[A, B, C, D, E](
genA: Gen[A],
genB: Gen[B],
genC: Gen[C],
genD: Gen[D])(func: (A, B, C, D) => Assertion): Future[Assertion] = {
forAllAsync(genA, genB, genC, genD) {
case (inputA, inputB, inputC, inputD) =>
Future {
func(inputA, inputB, inputC, inputD)
}
}
}
/** Runs all property based tests in parallel. This is a convenient optimization
* for synchronous property based tests
*/
def forAllParallel[A, B, C, D, E](
genA: Gen[A],
genB: Gen[B],
genC: Gen[C],
genD: Gen[D],
genE: Gen[E])(func: (A, B, C, D, E) => Assertion): Future[Assertion] = {
forAllAsync(genA, genB, genC, genD, genE) {
case (inputA, inputB, inputC, inputD, inputE) =>
Future {
func(inputA, inputB, inputC, inputD, inputE)
}
}
}
/** Runs all property based tests in parallel. This is a convenient optimization
* for synchronous property based tests
*/
def forAllParallel[A, B, C, D, E, F](
genA: Gen[A],
genB: Gen[B],
genC: Gen[C],
genD: Gen[D],
genE: Gen[E],
genF: Gen[F])(
func: (A, B, C, D, E, F) => Assertion): Future[Assertion] = {
forAllAsync(genA, genB, genC, genD, genE, genF) {
case (inputA, inputB, inputC, inputD, inputE, inputF) =>
Future {
func(inputA, inputB, inputC, inputD, inputE, inputF)
}
}
}
/** Makes sure we have aggregated all of our test runs */
private def forAllHelper(
testRunsF: java.util.concurrent.CopyOnWriteArrayList[

View file

@ -432,7 +432,9 @@ abstract class Wallet
inputs = InputUtil.calcSequenceForInputs(utxos)
txBuilder = RawTxBuilder() ++= inputs += dummyOutput
finalizer = SubtractFeeFromOutputsFinalizer(inputInfos, feeRate)
finalizer = SubtractFeeFromOutputsFinalizer(inputInfos,
feeRate,
Vector(address.scriptPubKey))
.andThen(ShuffleFinalizer)
.andThen(AddWitnessDataFinalizer(inputInfos))

View file

@ -83,19 +83,23 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
/////////////////////
// Internal wallet API
protected def insertTransaction(tx: Transaction): Future[TransactionDb] = {
val txDb = TransactionDbHelper.fromTransaction(tx)
transactionDAO.upsert(txDb)
}
private[wallet] def insertOutgoingTransaction(
transaction: Transaction,
feeRate: FeeUnit,
inputAmount: CurrencyUnit,
sentAmount: CurrencyUnit): Future[OutgoingTransactionDb] = {
val txDb = TransactionDbHelper.fromTransaction(transaction)
val outgoingDb =
OutgoingTransactionDb.fromTransaction(transaction,
inputAmount,
sentAmount,
feeRate.calc(transaction))
for {
_ <- transactionDAO.upsert(txDb)
_ <- insertTransaction(transaction)
written <- outgoingTxDAO.upsert(outgoingDb)
} yield written
}
@ -164,7 +168,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
}
}
private def processIncomingUtxos(
protected def processIncomingUtxos(
transaction: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE],
newTags: Vector[AddressTag]): Future[Vector[SpendingInfoDb]] =
@ -183,7 +187,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
.map(_.toVector)
}
def processOutgoingUtxos(
protected def processOutgoingUtxos(
transaction: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[
Vector[SpendingInfoDb]] = {
@ -394,17 +398,16 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
private[wallet] def insertIncomingTransaction(
transaction: Transaction,
incomingAmount: CurrencyUnit): Future[IncomingTransactionDb] = {
val txDb = TransactionDbHelper.fromTransaction(transaction)
val incomingDb = IncomingTransactionDb(transaction.txIdBE, incomingAmount)
for {
_ <- transactionDAO.upsert(txDb)
_ <- insertTransaction(transaction)
written <- incomingTxDAO.upsert(incomingDb)
} yield written
}
private def getRelevantOutputs(
transaction: Transaction): Future[Seq[OutputWithIndex]] = {
addressDAO.findAllAddresses().map { addrs =>
scriptPubKeyDAO.findAll().map { addrs =>
val withIndex =
transaction.outputs.zipWithIndex
withIndex.collect {
@ -446,18 +449,26 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
for {
_ <- insertIncomingTransaction(transaction, totalIncoming)
addrs <- addressDAO.findAllAddresses()
ourOutputs = outputsWithIndex.collect {
case OutputWithIndex(out, idx)
if addrs.map(_.scriptPubKey).contains(out.scriptPubKey) =>
OutputWithIndex(out, idx)
}
prevTagDbs <- addressTagDAO.findTx(transaction, networkParameters)
prevTags = prevTagDbs.map(_.addressTag)
tagsToUse =
prevTags
.filterNot(tag => newTags.contains(tag)) ++ newTags
newTagDbs = outputsWithIndex.flatMap { out =>
newTagDbs = ourOutputs.flatMap { out =>
val address = BitcoinAddress
.fromScriptPubKey(out.output.scriptPubKey, networkParameters)
tagsToUse.map(tag => AddressTagDb(address, tag))
}
_ <- addressTagDAO.createAll(newTagDbs.toVector)
utxos <- addUTXOsFut(outputsWithIndex, transaction, blockHashOpt)
utxos <- addUTXOsFut(ourOutputs, transaction, blockHashOpt)
} yield utxos
}
}