mirror of
https://github.com/muun/recovery.git
synced 2025-11-12 06:50:18 -05:00
Update project structure and build process
This commit is contained in:
141
libwallet/fees/fees.go
Normal file
141
libwallet/fees/fees.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package fees
|
||||
|
||||
import "github.com/btcsuite/btcutil"
|
||||
|
||||
const dustThreshold = 546
|
||||
|
||||
type BestRouteFees struct {
|
||||
MaxCapacity btcutil.Amount
|
||||
FeeProportionalMillionth uint64
|
||||
FeeBase btcutil.Amount
|
||||
}
|
||||
|
||||
type FundingOutputPolicies struct {
|
||||
MaximumDebt btcutil.Amount
|
||||
PotentialCollect btcutil.Amount
|
||||
MaxAmountFor0Conf btcutil.Amount
|
||||
}
|
||||
|
||||
type DebtType string
|
||||
|
||||
const (
|
||||
DebtTypeNone DebtType = "NONE"
|
||||
DebtTypeCollect DebtType = "COLLECT"
|
||||
DebtTypeLend DebtType = "LEND"
|
||||
)
|
||||
|
||||
type SwapFees struct {
|
||||
RoutingFee btcutil.Amount
|
||||
DebtType DebtType
|
||||
DebtAmount btcutil.Amount
|
||||
OutputAmount btcutil.Amount
|
||||
OutputPadding btcutil.Amount
|
||||
ConfirmationsNeeded uint
|
||||
}
|
||||
|
||||
func (p *FundingOutputPolicies) FundingConfirmations(paymentAmount, lightningFee btcutil.Amount) uint {
|
||||
totalAmount := paymentAmount + lightningFee
|
||||
if totalAmount <= p.MaxAmountFor0Conf {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (p *FundingOutputPolicies) DebtType(paymentAmount, lightningFee btcutil.Amount) DebtType {
|
||||
numConfirmations := p.FundingConfirmations(paymentAmount, lightningFee)
|
||||
totalAmount := paymentAmount + lightningFee
|
||||
if numConfirmations == 0 && totalAmount <= p.MaximumDebt {
|
||||
return DebtTypeLend
|
||||
}
|
||||
if p.PotentialCollect > 0 {
|
||||
return DebtTypeCollect
|
||||
}
|
||||
return DebtTypeNone
|
||||
}
|
||||
|
||||
func (p *FundingOutputPolicies) DebtAmount(paymentAmount, lightningFee btcutil.Amount) btcutil.Amount {
|
||||
switch p.DebtType(paymentAmount, lightningFee) {
|
||||
case DebtTypeLend:
|
||||
return paymentAmount + lightningFee
|
||||
case DebtTypeCollect:
|
||||
return p.PotentialCollect
|
||||
case DebtTypeNone:
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p *FundingOutputPolicies) MinFundingAmount(paymentAmount, lightningFee btcutil.Amount) btcutil.Amount {
|
||||
inputAmount := paymentAmount + lightningFee
|
||||
if p.DebtType(paymentAmount, lightningFee) == DebtTypeCollect {
|
||||
inputAmount += p.DebtAmount(paymentAmount, lightningFee)
|
||||
}
|
||||
return inputAmount
|
||||
}
|
||||
|
||||
func (p *FundingOutputPolicies) FundingOutputAmount(paymentAmount, lightningFee btcutil.Amount) btcutil.Amount {
|
||||
minAmount := p.MinFundingAmount(paymentAmount, lightningFee)
|
||||
if minAmount < dustThreshold {
|
||||
return dustThreshold
|
||||
}
|
||||
return minAmount
|
||||
}
|
||||
|
||||
func (p *FundingOutputPolicies) FundingOutputPadding(paymentAmount, lightningFee btcutil.Amount) btcutil.Amount {
|
||||
minAmount := p.MinFundingAmount(paymentAmount, lightningFee)
|
||||
outputAmount := p.FundingOutputAmount(paymentAmount, lightningFee)
|
||||
return outputAmount - minAmount
|
||||
}
|
||||
|
||||
func ComputeSwapFees(amount btcutil.Amount, bestRouteFees []BestRouteFees, policies *FundingOutputPolicies, takeFeeFromAmount bool) *SwapFees {
|
||||
if takeFeeFromAmount {
|
||||
// Handle edge cases for TFFA swaps. We don't allow lend for TFFA. This impacts sub-dust
|
||||
// swaps because we don't allow debt for output padding. Except, the very special case of
|
||||
// sub-dust TFFA swaps, in which you cant have output padding > 0 since you are using all
|
||||
// your balance and all your balance is < dust. In this case, since we can't use debt nor
|
||||
// output padding, if its necessary, the payment is unpayable.
|
||||
policies = &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: policies.PotentialCollect,
|
||||
MaxAmountFor0Conf: policies.MaxAmountFor0Conf,
|
||||
}
|
||||
}
|
||||
|
||||
lightningFee := computeLightningFee(amount, bestRouteFees)
|
||||
outputPadding := policies.FundingOutputPadding(amount, lightningFee)
|
||||
|
||||
offchainFee := lightningFee + outputPadding
|
||||
outputAmount := amount + offchainFee
|
||||
|
||||
debtType := policies.DebtType(amount, lightningFee)
|
||||
debtAmount := policies.DebtAmount(amount, lightningFee)
|
||||
if debtType == DebtTypeCollect {
|
||||
outputAmount += debtAmount
|
||||
} else if debtType == DebtTypeLend {
|
||||
outputAmount = 0
|
||||
}
|
||||
|
||||
return &SwapFees{
|
||||
RoutingFee: lightningFee,
|
||||
OutputPadding: outputPadding,
|
||||
DebtType: debtType,
|
||||
DebtAmount: debtAmount,
|
||||
ConfirmationsNeeded: policies.FundingConfirmations(amount, lightningFee),
|
||||
OutputAmount: outputAmount,
|
||||
}
|
||||
}
|
||||
|
||||
func computeLightningFee(amount btcutil.Amount, bestRouteFees []BestRouteFees) btcutil.Amount {
|
||||
for _, fee := range bestRouteFees {
|
||||
if amount <= fee.MaxCapacity {
|
||||
return fee.ForAmount(amount)
|
||||
}
|
||||
}
|
||||
lastRouteFee := bestRouteFees[len(bestRouteFees)-1]
|
||||
return lastRouteFee.ForAmount(amount)
|
||||
}
|
||||
|
||||
func (f *BestRouteFees) ForAmount(amount btcutil.Amount) btcutil.Amount {
|
||||
return (btcutil.Amount(f.FeeProportionalMillionth)*amount)/1000000 + f.FeeBase
|
||||
}
|
||||
363
libwallet/fees/fees_test.go
Normal file
363
libwallet/fees/fees_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package fees
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
func TestComputeSwapFees(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
amount btcutil.Amount
|
||||
bestRouteFees []BestRouteFees
|
||||
policies *FundingOutputPolicies
|
||||
takeFeeFromAmount bool
|
||||
expected *SwapFees
|
||||
}{
|
||||
{
|
||||
desc: "smoke test",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 1010,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "qualifies for 0-conf",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 1000000,
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
ConfirmationsNeeded: 0,
|
||||
OutputAmount: 1010,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "qualifies for debt lend",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 1000000,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 1000000,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeLend,
|
||||
DebtAmount: 1010,
|
||||
OutputAmount: 0,
|
||||
ConfirmationsNeeded: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "debt collect",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 100,
|
||||
PotentialCollect: 1010,
|
||||
MaxAmountFor0Conf: 1000000,
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeCollect,
|
||||
DebtAmount: 1010,
|
||||
OutputAmount: 2020,
|
||||
ConfirmationsNeeded: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "dust threshold",
|
||||
amount: 50,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 486,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 546,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sub-dust lend",
|
||||
amount: 50,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 1000000,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 1000000,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 486,
|
||||
DebtType: DebtTypeLend,
|
||||
DebtAmount: 60,
|
||||
OutputAmount: 0,
|
||||
ConfirmationsNeeded: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "uses last route if route with enough capacity",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 900,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
{
|
||||
MaxCapacity: 900,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 20,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: false,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 20,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 1020,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "smoke test TFFA",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
takeFeeFromAmount: true,
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 1010,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "qualifies for 0-conf TFFA",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
takeFeeFromAmount: true,
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 1010,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "qualifies for debt lend TFFA",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: true,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 1000000,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 1000000,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 1010,
|
||||
ConfirmationsNeeded: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "debt collect TFFA",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 100,
|
||||
PotentialCollect: 1010,
|
||||
MaxAmountFor0Conf: 1000000,
|
||||
},
|
||||
takeFeeFromAmount: true,
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeCollect,
|
||||
DebtAmount: 1010,
|
||||
OutputAmount: 2020,
|
||||
ConfirmationsNeeded: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "dust threshold TFFA",
|
||||
amount: 50,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 100000,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: true,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 10,
|
||||
OutputPadding: 486,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 546,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "uses last route if route with enough capacity",
|
||||
amount: 1000,
|
||||
bestRouteFees: []BestRouteFees{
|
||||
{
|
||||
MaxCapacity: 900,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 10,
|
||||
},
|
||||
{
|
||||
MaxCapacity: 900,
|
||||
FeeProportionalMillionth: 1,
|
||||
FeeBase: 20,
|
||||
},
|
||||
},
|
||||
takeFeeFromAmount: true,
|
||||
policies: &FundingOutputPolicies{
|
||||
MaximumDebt: 0,
|
||||
PotentialCollect: 0,
|
||||
MaxAmountFor0Conf: 0,
|
||||
},
|
||||
expected: &SwapFees{
|
||||
RoutingFee: 20,
|
||||
OutputPadding: 0,
|
||||
DebtType: DebtTypeNone,
|
||||
DebtAmount: 0,
|
||||
OutputAmount: 1020,
|
||||
ConfirmationsNeeded: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.desc, func(t *testing.T) {
|
||||
fees := ComputeSwapFees(tC.amount, tC.bestRouteFees, tC.policies, tC.takeFeeFromAmount)
|
||||
if !reflect.DeepEqual(fees, tC.expected) {
|
||||
t.Errorf("fees do not equal expected fees (%+v != %+v)", fees, tC.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user