Update project structure and build process

This commit is contained in:
Juan Pablo Civile
2025-05-13 11:10:08 -03:00
parent 124e9fa1bc
commit d9f3e925a4
277 changed files with 15321 additions and 930 deletions

141
libwallet/fees/fees.go Normal file
View 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
View 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)
}
})
}
}