mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-18 21:22:04 +01:00
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:
parent
bd3584eb43
commit
49a281acaf
86 changed files with 7277 additions and 1748 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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("")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
161
app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCPlotUtil.scala
Normal file
161
app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCPlotUtil.scala
Normal 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
|
||||
}
|
||||
}
|
121
app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCTableView.scala
Normal file
121
app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCTableView.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = ""
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
18
app/gui/src/main/scala/org/bitcoins/gui/util/GUIUtil.scala
Normal file
18
app/gui/src/main/scala/org/bitcoins/gui/util/GUIUtil.scala
Normal 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]", ""))
|
||||
}
|
||||
}
|
||||
}
|
8
app/oracle-server/src/main/resources/reference.conf
Normal file
8
app/oracle-server/src/main/resources/reference.conf
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -183,3 +183,10 @@ case class RawTxBuilderWithFinalizer[F <: RawTxFinalizer](
|
|||
this
|
||||
}
|
||||
}
|
||||
|
||||
object RawTxBuilderWithFinalizer {
|
||||
|
||||
def apply[F <: RawTxFinalizer](finalizer: F): RawTxBuilderWithFinalizer[F] = {
|
||||
RawTxBuilderWithFinalizer(RawTxBuilder(), finalizer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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[
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue