muun-recovery/libwallet/newop/state_test.go
2025-05-13 17:49:44 -03:00

1399 lines
49 KiB
Go

package newop
import (
"testing"
"time"
"github.com/muun/libwallet"
"github.com/shopspring/decimal"
)
type testListener struct {
ch chan State
}
func newTestListener() *testListener {
return &testListener{ch: make(chan State, 10)}
}
func (t *testListener) OnStart(nextState *StartState) { t.ch <- nextState }
func (t *testListener) OnResolve(nextState *ResolveState) { t.ch <- nextState }
func (t *testListener) OnEnterAmount(nextState *EnterAmountState) { t.ch <- nextState }
func (t *testListener) OnEnterDescription(nextState *EnterDescriptionState) { t.ch <- nextState }
func (t *testListener) OnValidate(nextState *ValidateState) { t.ch <- nextState }
func (t *testListener) OnValidateLightning(nextState *ValidateLightningState) { t.ch <- nextState }
func (t *testListener) OnConfirm(nextState *ConfirmState) { t.ch <- nextState }
func (t *testListener) OnConfirmLightning(nextState *ConfirmLightningState) { t.ch <- nextState }
func (t *testListener) OnEditFee(nextState *EditFeeState) { t.ch <- nextState }
func (t *testListener) OnError(nextState *ErrorState) { t.ch <- nextState }
func (t *testListener) OnBalanceError(nextState *BalanceErrorState) { t.ch <- nextState }
func (t *testListener) OnAbort(nextState *AbortState) { t.ch <- nextState }
func (t *testListener) next() State {
return <-t.ch
}
var _ TransitionListener = &testListener{}
var testContext = createContext()
func createContext() *PaymentContext {
var context = &PaymentContext{
NextTransactionSize: &NextTransactionSize{},
ExchangeRateWindow: &ExchangeRateWindow{
rates: make(map[string]float64),
},
FeeWindow: &FeeWindow{},
PrimaryCurrency: "BTC",
MinFeeRateInSatsPerVByte: 1.0,
}
context.NextTransactionSize.AddSizeForAmount(&SizeForAmount{
AmountInSat: 100_000_000,
SizeInVByte: 240,
})
context.ExchangeRateWindow.AddRate("BTC", 1)
context.ExchangeRateWindow.AddRate("USD", 32_000)
context.ExchangeRateWindow.AddRate("ARS", 9_074_813.98)
context.FeeWindow.PutTargetedFees(1, 400.0)
context.FeeWindow.PutTargetedFees(15, 120.0)
context.FeeWindow.PutTargetedFees(90, 8.0)
return context
}
//goland:noinspection GoUnhandledErrorResult
func TestBarebonesOnChainFixedAmountFixedFee(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.1&description=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("bar")
confirmState := listener.next().(*ConfirmState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.1 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.10096 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestBarebonesOnChainFixedAmountFixedDescriptionFixedFee(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.1&message=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
validateState := listener.next().(*ValidateState)
validateState.Continue()
confirmState := listener.next().(*ConfirmState)
if confirmState.Note != "foo" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.1 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.10096 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainFixedAmountChangeFee(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.1&description=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("bar")
confirmState := listener.next().(*ConfirmState)
if confirmState.FeeRateInSatsPerVByte != 400 {
t.Fatalf("expected initial fee rate to be 400, got %v", confirmState.FeeRateInSatsPerVByte)
}
newFeeRate := 15.0
confirmState.OpenFeeEditor()
editFeeState := listener.next().(*EditFeeState)
feeState, err := editFeeState.CalculateFee(newFeeRate)
if err != nil {
t.Fatal(err)
}
if feeState.State != FeeStateFinalFee {
t.Fatalf("expected fee state to be FinalFee, got %v", feeState.State)
}
if feeState.Amount.InSat != 3600 {
t.Fatalf("expected fee amount to be 3600, got %v", feeState.Amount.InSat)
}
editFeeState.SetFeeRate(newFeeRate)
validateState = listener.next().(*ValidateState)
validateState.Continue()
confirmState = listener.next().(*ConfirmState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.1 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.000036 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.100036 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainFixedAmountFeeNeedsChange(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.9999&description=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("bar")
confirmState := listener.next().(*ConfirmState)
if confirmState.FeeRateInSatsPerVByte != 400 {
t.Fatalf("expected initial fee rate to be 400, got %v", confirmState.FeeRateInSatsPerVByte)
}
if !confirmState.FeeNeedsChange {
t.Fatalf("expected initial fee to be unpayable and need changing, got %v which is unpayable", confirmState.FeeRateInSatsPerVByte)
}
newFeeRate := 15.0
confirmState.OpenFeeEditor()
editFeeState := listener.next().(*EditFeeState)
feeState, err := editFeeState.CalculateFee(newFeeRate)
if err != nil {
t.Fatal(err)
}
if feeState.State != FeeStateFinalFee {
t.Fatalf("expected fee state to be FinalFee, got %v", feeState.State)
}
if feeState.Amount.InSat != 3600 {
t.Fatalf("expected fee amount to be 3600, got %v", feeState.Amount.InSat)
}
editFeeState.SetFeeRate(newFeeRate)
validateState = listener.next().(*ValidateState)
validateState.Continue()
confirmState = listener.next().(*ConfirmState)
if confirmState.FeeNeedsChange {
t.Fatalf("expected fee to be payable, got %v which is not unpayable", confirmState.FeeRateInSatsPerVByte)
}
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.9999 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.000036 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.999936 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainFixedAmountNoPossibleFee(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.9999999&description=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
validateState := listener.next().(*ValidateState)
validateState.Continue()
balanceErrorState := listener.next().(*BalanceErrorState)
if balanceErrorState.Error != OperationErrorUnpayable {
t.Fatalf("expected initial fee to be unpayable but got %s", balanceErrorState.Error)
}
if balanceErrorState.TotalAmount.String() != "1.0000023 BTC" {
t.Fatalf("expected total to match, got %v", balanceErrorState.TotalAmount)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainFixedAmountTooSmall(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.0000004&description=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorAmountTooSmall {
t.Fatalf("expected amount to be too small but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainFixedAmountGreaterThanbalance(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=2.0&description=foo", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
validateState := listener.next().(*ValidateState)
validateState.Continue()
balanceErrorState := listener.next().(*BalanceErrorState)
if balanceErrorState.Error != OperationErrorAmountGreaterThanBalance {
t.Fatalf("expected amount to be too small but got %s", balanceErrorState.Error)
}
if balanceErrorState.TotalAmount.String() != "2 BTC" {
t.Fatalf("expected total to match, got %v", balanceErrorState.TotalAmount)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainSendZeroFundsWithZeroBalance(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
context := createContext()
context.NextTransactionSize = &NextTransactionSize{}
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(context)
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(0), true)
validateState := listener.next().(*ValidateState)
validateState.Continue()
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorAmountTooSmall {
t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainTFFA(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(100_000_000), true)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("bar")
if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
}
confirmState := listener.next().(*ConfirmState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.99904 BTC" {
t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "1 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
confirmState.Back()
// check we preserve values correctly
enterDescriptionState = listener.next().(*EnterDescriptionState)
if enterDescriptionState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestInvalidAmountEmitsInvalidAddress(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=bananabanana", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorInvalidAddress {
t.Fatalf("expected error to be invalid address but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainBack(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(100_000_000), true)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("bar")
confirmState := listener.next().(*ConfirmState)
confirmState.Back()
enterDescriptionState = listener.next().(*EnterDescriptionState)
enterDescriptionState.Back()
enterAmountState = listener.next().(*EnterAmountState)
// TODO when deleting this method impl (deprecated) rm lines below up until the call to ChangeCurrencyWithAmount
enterAmountState.ChangeCurrency("USD")
enterAmountState = listener.next().(*EnterAmountState)
enterAmountState.Back()
abortState := listener.next().(*AbortState)
if abortState.update != UpdateAll {
t.Fatalf("expected normal/full update , got %v", abortState.update)
}
abortState.Cancel()
enterAmountState = listener.next().(*EnterAmountState)
if enterAmountState.update != UpdateEmpty {
t.Fatalf("expected empty update, got %v", enterAmountState.update)
}
enterAmountState.Back()
abortState = listener.next().(*AbortState)
if abortState.update != UpdateAll {
t.Fatalf("expected normal/full update , got %v", abortState.update)
}
abortState.Cancel()
enterAmountState = listener.next().(*EnterAmountState)
enterAmountState.ChangeCurrencyWithAmount("USD", NewMonetaryAmountFromSatoshis(1_000_000))
enterAmountState = listener.next().(*EnterAmountState)
enterAmountState.Back()
abortState = listener.next().(*AbortState)
if abortState.update != UpdateAll {
t.Fatalf("expected normal/full update , got %v", abortState.update)
}
abortState.Cancel()
enterAmountState = listener.next().(*EnterAmountState)
if enterAmountState.update != UpdateEmpty {
t.Fatalf("expected empty update, got %v", enterAmountState.update)
}
enterAmountState.Back()
abortState = listener.next().(*AbortState)
if abortState.update != UpdateAll {
t.Fatalf("expected normal/full update , got %v", abortState.update)
}
abortState.Cancel()
// One more, just for the giggles
enterAmountState = listener.next().(*EnterAmountState)
enterAmountState.Back()
_ = listener.next().(*AbortState)
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainChangeCurrency(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
enterAmountState := listener.next().(*EnterAmountState)
inputAmountCurrency := enterAmountState.Amount.InInputCurrency.Currency
balanceCurrency := enterAmountState.TotalBalance.InInputCurrency.Currency
if inputAmountCurrency != balanceCurrency {
t.Fatalf("expected amount currency (%v) to match balance currency (%v)", inputAmountCurrency, balanceCurrency)
}
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(100_000_000), true)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
}
enterDescriptionState.Back()
enterAmountState = listener.next().(*EnterAmountState)
// TODO when deleting this method impl (deprecated) rm lines below up until the call to ChangeCurrencyWithAmount
enterAmountState.ChangeCurrency("USD")
enterAmountState = listener.next().(*EnterAmountState)
if enterAmountState.update != UpdateInPlace {
t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
}
if enterAmountState.Amount.InSat != 100_000_000 {
t.Fatalf("expected amount to match 100_000_000, got '%v'", enterAmountState.Amount.InSat)
}
if enterAmountState.Amount.InInputCurrency.String() != "32000 USD" {
t.Fatalf("expected amount to match 32000 USD, got '%v'", enterAmountState.Amount.InInputCurrency.String())
}
if enterAmountState.Amount.InPrimaryCurrency.String() != "1 BTC" {
t.Fatalf("expected amount to match 1 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
}
enterAmountState.ChangeCurrency("BTC")
enterAmountState = listener.next().(*EnterAmountState)
if enterAmountState.update != UpdateInPlace {
t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
}
if enterAmountState.Amount.InSat != 100_000_000 {
t.Fatalf("expected amount to match 100_000_000, got '%v'", enterAmountState.Amount.InSat)
}
if enterAmountState.Amount.InInputCurrency.String() != "1 BTC" {
t.Fatalf("expected amount to match 1 BTC, got '%v'", enterAmountState.Amount.InInputCurrency.String())
}
if enterAmountState.Amount.InPrimaryCurrency.String() != "1 BTC" {
t.Fatalf("expected amount to match 1 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
}
enterAmountState.ChangeCurrencyWithAmount("USD", NewMonetaryAmountFromSatoshis(1_000_000))
enterAmountState = listener.next().(*EnterAmountState)
if enterAmountState.update != UpdateInPlace {
t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
}
if enterAmountState.Amount.InSat != 1_000_000 {
t.Fatalf("expected amount to match 1_000_000, got '%v'", enterAmountState.Amount.InSat)
}
if enterAmountState.Amount.InInputCurrency.String() != "320 USD" {
t.Fatalf("expected amount to match 320 USD, got '%v'", enterAmountState.Amount.InInputCurrency.String())
}
if enterAmountState.Amount.InPrimaryCurrency.String() != "0.01 BTC" {
t.Fatalf("expected amount to match 0.01 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
}
enterAmountState.ChangeCurrencyWithAmount("BTC", enterAmountState.Amount.InInputCurrency)
enterAmountState = listener.next().(*EnterAmountState)
if enterAmountState.update != UpdateInPlace {
t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
}
if enterAmountState.Amount.InSat != 1_000_000 {
t.Fatalf("expected amount to match 1_000_000, got '%v'", enterAmountState.Amount.InSat)
}
if enterAmountState.Amount.InInputCurrency.String() != "0.01 BTC" {
t.Fatalf("expected amount to match 0.01 BTC, got '%v'", enterAmountState.Amount.InInputCurrency.String())
}
if enterAmountState.Amount.InPrimaryCurrency.String() != "0.01 BTC" {
t.Fatalf("expected amount to match 0.01 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
}
enterDescriptionState.EnterDescription("bar")
confirmState := listener.next().(*ConfirmState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.99904 BTC" {
t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "1 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
confirmState.Back()
// check we preserve values correctly
enterDescriptionState = listener.next().(*EnterDescriptionState)
if enterDescriptionState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningSendZeroFunds(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.NextTransactionSize = &NextTransactionSize{}
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100_000,
FeeProportionalMillionth: 1,
FeeBase: 1_000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 25_000,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(0), false)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorAmountTooSmall {
t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningSendZeroFundsTFFA(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.NextTransactionSize = &NextTransactionSize{}
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100_000,
FeeProportionalMillionth: 1,
FeeBase: 1_000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 25_000,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(0), true)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorAmountTooSmall {
t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningSendNegativeFunds(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.NextTransactionSize = &NextTransactionSize{}
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100_000,
FeeProportionalMillionth: 1,
FeeBase: 1_000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 25_000,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(-10), false)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorAmountTooSmall {
t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningSendNegativeFundsWithTFFA(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.NextTransactionSize = &NextTransactionSize{}
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100_000,
FeeProportionalMillionth: 1,
FeeBase: 1_000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 25_000,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(-10), true)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorAmountTooSmall {
t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningExpiredInvoice(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt100u1ps3kdrgpp5klwrzs0u63sqnca8elqu86p98swxycw3fgjtmeddm2ljd7ymrlwsdqqcqzpgsp5zs02vngwrywtqhwygu44wp464lgjtqyc5h76vae073064p72znas9qyyssqz2263utx4n7r7n85s9wg3ma2zmg3xtg46nj3e6nnr6g67tnj6jwn7urvx5qukhqjzmcnuc4t7uqlxhftqwq4hxha3ests23fcmt5evqpazdmg2", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(testContext)
errorState := listener.next().(*ErrorState)
if errorState.Error != OperationErrorInvoiceExpired {
t.Fatalf("expected error to match, got '%v'", errorState.Error)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningInvoiceWithAmount(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt100u1ps3l5eepp5njeddrlmsg9cd2a4v508mqucz7tdge90vvp4f5n23gh7kthnjjdqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qsp52qtk90062t5mha837ulm77vf04ph4kaxerm8xugjdkp9gk6d8yqs9qyyssqw09stp3vy33dfjc6vcrdfmf58trg5pte6efph9pj9gwlg0w7anhz6aelv0p3r9qj6vrjjw9jyj6s9tjujec2fm9k8ag3yvgvwszswxsqhl6equ", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
Fees: &SwapFees{
RoutingFeeInSat: 0,
DebtType: DebtTypeNone,
DebtAmountInSat: 0,
OutputAmountInSat: 10000,
OutputPaddingInSat: 0,
ConfirmationsNeeded: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("foo")
confirmState := listener.next().(*ConfirmLightningState)
if confirmState.Note != "foo" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.0000192 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.0001192 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningWithAmountBack(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt100u1ps3l5eepp5njeddrlmsg9cd2a4v508mqucz7tdge90vvp4f5n23gh7kthnjjdqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qsp52qtk90062t5mha837ulm77vf04ph4kaxerm8xugjdkp9gk6d8yqs9qyyssqw09stp3vy33dfjc6vcrdfmf58trg5pte6efph9pj9gwlg0w7anhz6aelv0p3r9qj6vrjjw9jyj6s9tjujec2fm9k8ag3yvgvwszswxsqhl6equ", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
Fees: &SwapFees{
RoutingFeeInSat: 0,
DebtType: DebtTypeNone,
DebtAmountInSat: 0,
OutputAmountInSat: 10000,
OutputPaddingInSat: 0,
ConfirmationsNeeded: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("foo")
confirmState := listener.next().(*ConfirmLightningState)
confirmState.Back()
enterDescriptionState = listener.next().(*EnterDescriptionState)
if enterDescriptionState.Note != "foo" {
t.Fatalf("expected note to match input, got '%v'", enterDescriptionState.Note)
}
enterDescriptionState.EnterDescription("bar")
confirmState = listener.next().(*ConfirmLightningState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.0000192 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.0001192 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningInvoiceWithAmountAndDescription(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt100u1psjg8k6pp5n895ngj22v4dczwd8jrvvq76qvur2642y29m6x2faq0rgle2zwwsdq2vehk7cnpwgcqzpgsp5x9yeys2j294q402ewq2kfcas6wn63mk5q86ehe79plljzfwhr69s9qyyssq4exlg7ly068zc8dfh6ls5r69x0pmvdy9la70hw2vqwz9p2g4p5fyxr0hlkzrfnkmlx3kjrlecedatk96zuzs8a3cj48qg7vne6zp5ygpgzwd2x", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
Fees: &SwapFees{
RoutingFeeInSat: 0,
DebtType: DebtTypeNone,
DebtAmountInSat: 0,
OutputAmountInSat: 10000,
OutputPaddingInSat: 0,
ConfirmationsNeeded: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
confirmState := listener.next().(*ConfirmLightningState)
if confirmState.Note != "foobar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.0000192 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.0001192 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestLightningAmountlessInvoice(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100_000,
FeeProportionalMillionth: 1,
FeeBase: 1_000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 25_000,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(10_000), false)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("foo")
confirmState := listener.next().(*ConfirmLightningState)
if confirmState.Note != "foo" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.0000292 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.0001292 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
if confirmState.SwapInfo.IsOneConf {
t.Fatalf("expected swap to be 0 conf")
}
// Test a bug where the one conf flag was set after back
confirmState.Back()
enterDescriptionState = listener.next().(*EnterDescriptionState)
if enterDescriptionState.SwapInfo.IsOneConf {
t.Fatalf("expected swap to be 0 conf")
}
}
//goland:noinspection GoUnhandledErrorResult
func TestInvoiceOneConf(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 1000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(10000), false)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("foo")
if !enterDescriptionState.SwapInfo.IsOneConf {
t.Fatalf("expected swap to be 1 conf")
}
confirmState := listener.next().(*ConfirmLightningState)
if confirmState.Note != "foo" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.00097 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.00107 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
if !confirmState.SwapInfo.IsOneConf {
t.Fatalf("expected swap to be 1 conf")
}
}
//goland:noinspection GoUnhandledErrorResult
func TestAmountConversion(t *testing.T) {
// This test repros a bug where we had:
// * Primary currency BTC
// * Fiat input
// Then, for amount/total the amount in sat and in primary currency differed
// in 1 sat.
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 1000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromFiat("2500", "ARS"), false)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("foo")
confirmState := listener.next().(*ConfirmLightningState)
btcToSats := decimal.NewFromInt(100_000_000)
inPrimary := confirmState.Amount.InPrimaryCurrency.Value
// This emulates the conversions app make (and the Right Way™ to do it)
primaryInSats := inPrimary.Mul(btcToSats).RoundBank(0).IntPart()
if primaryInSats != confirmState.Amount.InSat {
t.Fatalf(
"expected amount in primary to match sats: %v != %v",
primaryInSats,
confirmState.Amount.InSat,
)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestInvoiceUnpayable(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
BestRouteFees: []*BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 1000,
},
},
FundingOutputPolicies: &FundingOutputPolicies{
MaximumDebtInSat: 0,
PotentialCollectInSat: 0,
MaxAmountInSatFor0Conf: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(99_999_999), false)
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
errorState := listener.next().(*BalanceErrorState)
if errorState.Error != OperationErrorUnpayable {
t.Fatalf("Expected error to be OperationErrorUnpayable: %v", errorState.Error)
}
if errorState.Balance.String() != "1 BTC" {
t.Fatalf("Expected balance to be 1 BTC: %v", errorState.Balance.String())
}
if errorState.TotalAmount.String() != "1.00097098 BTC" {
t.Fatalf("Expected total amount to be 1 BTC: %v", errorState.TotalAmount.String())
}
}
//goland:noinspection GoUnhandledErrorResult
func TestInvoiceLend(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
invoice, err := libwallet.ParseInvoice("lnbcrt1u1ps5ma8app59ujxjvtj8x34fyd7u7tghuq44dphjth8nqmkzeklg882y9ghjmvqdqqcqzpgsp5stzjqktxfh02dfz8tucfnh6rl3z87ctl2dumr40elhmrskhx5zlq9qyyssqnp4uhukcgxx6l0p5elppz5xc7a97n0hxvfm6lgr6ze06wqc2dnx95pet3vlalc9rqz20lu45y8sqg3n6fm6tqsftvzqp4l2zsrs0ndgpk2fyv5", libwallet.Regtest())
if err != nil {
panic(err)
}
startState.ResolveInvoice(invoice, libwallet.Regtest())
resolveState := listener.next().(*ResolveState)
context := createContext()
context.SubmarineSwap = &SubmarineSwap{
Fees: &SwapFees{
RoutingFeeInSat: 0,
DebtType: DebtTypeLend,
DebtAmountInSat: 100,
OutputAmountInSat: 546,
OutputPaddingInSat: 446,
ConfirmationsNeeded: 0,
},
}
resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
validateState := listener.next().(*ValidateLightningState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("foo")
confirmState := listener.next().(*ConfirmLightningState)
if confirmState.Note != "foo" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.000001 BTC" {
t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.000001 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
if confirmState.SwapInfo.SwapFees.DebtType != DebtTypeLend {
t.Fatalf("Expected debt type to be lend: %v", confirmState.SwapInfo.SwapFees.DebtType)
}
if confirmState.SwapInfo.SwapFees.DebtAmountInSat != 100 {
t.Fatalf("Expected debt amount to be 100 sats: %v", confirmState.SwapInfo.SwapFees.DebtAmountInSat)
}
}
//goland:noinspection GoUnhandledErrorResult
func TestAmountInfo_Mutating(t *testing.T) {
amountInfo := &AmountInfo{
TakeFeeFromAmount: false,
FeeRateInSatsPerVByte: 0,
}
mutated := amountInfo.mutating(func(info *AmountInfo) {
info.TakeFeeFromAmount = true
})
if amountInfo.TakeFeeFromAmount {
t.Fatalf("Expected original to stay the same")
}
if !mutated.TakeFeeFromAmount {
t.Fatalf("Mutated should be mutated")
}
}
//goland:noinspection GoUnhandledErrorResult
func TestOnChainTFFAWithDebtFeeNeedsChangeBecauseOutputAmountLowerThanDust(t *testing.T) {
listener := newTestListener()
startState := NewOperationFlow(listener)
startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
context := createContext()
nts := &NextTransactionSize{}
nts.AddSizeForAmount(&SizeForAmount{
AmountInSat: 5338,
SizeInVByte: 172,
})
nts.ExpectedDebtInSat = 4353
context.NextTransactionSize = nts
context.FeeWindow.PutTargetedFees(100, 1.0)
resolveState := listener.next().(*ResolveState)
resolveState.SetContext(context)
enterAmountState := listener.next().(*EnterAmountState)
enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(985), true)
validateState := listener.next().(*ValidateState)
validateState.Continue()
enterDescriptionState := listener.next().(*EnterDescriptionState)
enterDescriptionState.EnterDescription("bar")
// Amount is not payable with fastes/highest fee, but it is with min fee (1 sat/vbyte)
if enterDescriptionState.Amount.InInputCurrency.String() != "0 BTC" {
t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
}
confirmState := listener.next().(*ConfirmState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0 BTC" {
t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.000688 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.000688 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
if confirmState.FeeNeedsChange != true {
t.Fatalf("expected feedsNeedsChange to be true, got %v", confirmState.FeeNeedsChange)
}
confirmState.OpenFeeEditor()
editFeeState := listener.next().(*EditFeeState)
editFeeState.SetFeeRate(1)
validateState = listener.next().(*ValidateState)
validateState.Continue()
confirmState = listener.next().(*ConfirmState)
if confirmState.Note != "bar" {
t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
}
if confirmState.Amount.InInputCurrency.String() != "0.00000813 BTC" {
t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
}
if confirmState.Fee.InInputCurrency.String() != "0.00000172 BTC" {
t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
}
if confirmState.Total.InInputCurrency.String() != "0.00000985 BTC" {
t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
}
if confirmState.FeeNeedsChange != false {
t.Fatalf("expected feedsNeedsChange to be false, got %v", confirmState.FeeNeedsChange)
}
}