Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for service fees #474

Merged
merged 22 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
cp .env_example .env

build:
CGO_ENABLED=0 go build -o lndhub
CGO_ENABLED=0 go build -o lndhub ./cmd/server

test:
go test -p 1 -v -covermode=atomic -coverprofile=coverage.out -cover -coverpkg=./... ./...
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ vim .env # edit your config
+ `MAX_ACCOUNT_BALANCE`: (default: 0 = no limit) Set maximum balance (in satoshi) for each account
+ `MAX_SEND_VOLUME`: (default: 0 = no limit) Set maximum volume (in satoshi) for sending for each account
+ `MAX_RECEIVE_VOLUME`: (default: 0 = no limit) Set maximum volume (in satoshi) for receiving for each account
+ `SERVICE_FEE`: (default: 0 = no service fee) Set the service fee for each outgoing transaction in 1/1000 (e.g. 1 means a fee of 1sat for 1000sats - rounded up to the next bigger integer)
+ `NO_SERVICE_FEE_UP_TO_AMOUNT` (default: 0 = no free transactions) the amount in sats up to which no service fee should be charged

### Macaroon

Expand Down
7 changes: 7 additions & 0 deletions db/migrations/20231229091800_add_service_fee.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
alter table invoices ADD COLUMN service_fee bigint default 0;
alter table invoices ADD COLUMN routing_fee bigint default 0;

-- maybe manually migrate existing data?
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
-- alter table invoices ALTER COLUMN fee SET DEFAULT 0;
-- update invoices set fee = 0 where fee IS NULL;
-- update invoices set routing_fee = fee where routing_fee=0;
10 changes: 10 additions & 0 deletions db/models/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type Invoice struct {
User *User `json:"-" bun:"rel:belongs-to,join:user_id=id"`
Amount int64 `json:"amount" validate:"gte=0"`
Fee int64 `json:"fee" bun:",nullzero"`
ServiceFee int64 `json:"service_fee" bun:",nullzero"`
RoutingFee int64 `json:"routing_fee" bun:",nullzero"`
Memo string `json:"memo" bun:",nullzero"`
DescriptionHash string `json:"description_hash,omitempty" bun:",nullzero"`
PaymentRequest string `json:"payment_request" bun:",nullzero"`
Expand All @@ -33,6 +35,14 @@ type Invoice struct {
SettledAt bun.NullTime `json:"settled_at"`
}

func (i *Invoice) SetFee(txEntry TransactionEntry, routingFee int64) {
i.RoutingFee = routingFee
i.Fee = routingFee
if txEntry.ServiceFee != nil {
i.Fee += txEntry.ServiceFee.Amount
bumi marked this conversation as resolved.
Show resolved Hide resolved
}
}

func (i *Invoice) BeforeAppendModel(ctx context.Context, query bun.Query) error {
switch query.(type) {
case *bun.UpdateQuery:
Expand Down
3 changes: 3 additions & 0 deletions db/models/transactionentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const (
EntryTypeOutgoing = "outgoing"
EntryTypeFee = "fee"
EntryTypeFeeReserve = "fee_reserve"
EntryTypeServiceFee = "service_fee"
EntryTypeServiceFeeReversal = "service_fee_reversal"
EntryTypeFeeReserveReversal = "fee_reserve_reversal"
EntryTypeOutgoingReversal = "outgoing_reversal"
)
Expand All @@ -24,6 +26,7 @@ type TransactionEntry struct {
Parent *TransactionEntry `bun:"rel:belongs-to"`
CreditAccountID int64 `bun:",notnull"`
FeeReserve *TransactionEntry `bun:"rel:belongs-to"`
ServiceFee *TransactionEntry `bun:"rel:belongs-to"`
CreditAccount *Account `bun:"rel:belongs-to,join:credit_account_id=id"`
DebitAccountID int64 `bun:",notnull"`
DebitAccount *Account `bun:"rel:belongs-to,join:debit_account_id=id"`
Expand Down
5 changes: 4 additions & 1 deletion integration_tests/keysend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ func (suite *KeySendTestSuite) TearDownSuite() {
}

func (suite *KeySendTestSuite) TestKeysendPayment() {
suite.service.Config.ServiceFee = 1
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
aliceFundingSats := 1000
externalSatRequested := 500
expectedServiceFee := 1
//fund alice account
invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken)
err := suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil)
Expand All @@ -95,7 +97,8 @@ func (suite *KeySendTestSuite) TestKeysendPayment() {
if err != nil {
fmt.Printf("Error when getting balance %v\n", err.Error())
}
assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested+int(suite.mlnd.fee)), aliceBalance)
assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested+int(suite.mlnd.fee)+expectedServiceFee), aliceBalance)
suite.service.Config.ServiceFee = 0
}

func (suite *KeySendTestSuite) TestKeysendPaymentNonExistentDestination() {
Expand Down
70 changes: 50 additions & 20 deletions integration_tests/outgoing_payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import (
)

func (suite *PaymentTestSuite) TestOutGoingPayment() {
suite.service.Config.ServiceFee = 1
aliceFundingSats := 1000
externalSatRequested := 500
expectedServiceFee := 1
// 1 sat + 1 ppm
suite.mlnd.fee = 1
//fund alice account
Expand Down Expand Up @@ -45,10 +47,10 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() {
if err != nil {
fmt.Printf("Error when getting balance %v\n", err.Error())
}
assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested+int(suite.mlnd.fee)), aliceBalance)
assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested+int(suite.mlnd.fee)+expectedServiceFee), aliceBalance)

// check that no additional transaction entry was created
transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId)
transactionEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId)
if err != nil {
fmt.Printf("Error when getting transaction entries %v\n", err.Error())
}
Expand All @@ -63,29 +65,56 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() {
assert.Equal(suite.T(), 1, len(outgoingInvoices))
assert.Equal(suite.T(), 1, len(incomingInvoices))

assert.Equal(suite.T(), 5, len(transactonEntries))
// check if there are 6 transaction entries:
// - [0] incoming
// - [1] outgoing
// - [2] fee_reserve
// - [3] service_fee
// - [4] fee_reserve_reversal
// - [5] fee
//
assert.Equal(suite.T(), 6, len(transactionEntries))

// the incoming funding
assert.Equal(suite.T(), int64(aliceFundingSats), transactionEntries[0].Amount)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[0].CreditAccountID)
assert.Equal(suite.T(), incomingAccount.ID, transactionEntries[0].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactionEntries[0].ParentID)
assert.Equal(suite.T(), incomingInvoices[0].ID, transactionEntries[0].InvoiceID)

assert.Equal(suite.T(), int64(aliceFundingSats), transactonEntries[0].Amount)
assert.Equal(suite.T(), currentAccount.ID, transactonEntries[0].CreditAccountID)
assert.Equal(suite.T(), incomingAccount.ID, transactonEntries[0].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactonEntries[0].ParentID)
assert.Equal(suite.T(), incomingInvoices[0].ID, transactonEntries[0].InvoiceID)
// the outgoing payment
assert.Equal(suite.T(), int64(externalSatRequested), transactionEntries[1].Amount)
assert.Equal(suite.T(), outgoingAccount.ID, transactionEntries[1].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[1].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactionEntries[1].ParentID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[1].InvoiceID)

// fee
assert.Equal(suite.T(), int64(suite.mlnd.fee), transactionEntries[5].Amount)
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[5].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[5].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[5].InvoiceID)

assert.Equal(suite.T(), int64(externalSatRequested), transactonEntries[1].Amount)
assert.Equal(suite.T(), outgoingAccount.ID, transactonEntries[1].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactonEntries[1].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactonEntries[1].ParentID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactonEntries[1].InvoiceID)
// fee reserve + fee reserve reversal
assert.Equal(suite.T(), transactionEntries[4].Amount, transactionEntries[2].Amount) // the amount of the fee_reserve and the fee_reserve_reversal must be equal
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[2].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[2].DebitAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[4].CreditAccountID)
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[4].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[2].InvoiceID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[4].InvoiceID)

assert.Equal(suite.T(), int64(suite.mlnd.fee), transactonEntries[4].Amount)
assert.Equal(suite.T(), feeAccount.ID, transactonEntries[2].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactonEntries[2].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactonEntries[2].InvoiceID)
// service fee
assert.Equal(suite.T(), int64(expectedServiceFee), transactionEntries[3].Amount)
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[3].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[3].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[3].InvoiceID)

// make sure fee entry parent id is previous entry
assert.Equal(suite.T(), transactonEntries[1].ID, transactonEntries[4].ParentID)
assert.Equal(suite.T(), transactionEntries[1].ID, transactionEntries[5].ParentID)
assert.Equal(suite.T(), transactionEntries[1].ID, transactionEntries[3].ParentID)

//fetch transactions, make sure the fee is there
// fetch transactions, make sure the fee is there
// check invoices again
req := httptest.NewRequest(http.MethodGet, "/gettxs", nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", suite.aliceToken))
Expand All @@ -94,7 +123,8 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() {
responseBody := &[]ExpectedOutgoingInvoice{}
assert.Equal(suite.T(), http.StatusOK, rec.Code)
assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(&responseBody))
assert.Equal(suite.T(), int64(suite.mlnd.fee), (*responseBody)[0].Fee)
assert.Equal(suite.T(), int64(suite.mlnd.fee)+int64(expectedServiceFee), (*responseBody)[0].Fee)
suite.service.Config.ServiceFee = 0 // reset ServiceFee config (we don't expect the service fee everywhere)
}

func (suite *PaymentTestSuite) TestOutGoingPaymentWithNegativeBalance() {
Expand Down
92 changes: 76 additions & 16 deletions integration_tests/payment_failure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() {
}

func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() {
suite.service.Config.ServiceFee = 1
userFundingSats := 1000
externalSatRequested := 500
expectedServiceFee := 1
//fund user account
invoiceResponse := suite.createAddInvoiceReq(userFundingSats, "integration test external payment user", suite.userToken)
err := suite.mlnd.mockPaidInvoice(invoiceResponse, 0, false, nil)
Expand Down Expand Up @@ -130,13 +132,23 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() {

userId := getUserIdFromToken(suite.userToken)

invoices, err := invoicesFor(suite.service, userId, common.InvoiceTypeOutgoing)
// verify transaction entries data
feeAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeFees, userId)
incomingAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeIncoming, userId)
outgoingAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeOutgoing, userId)
currentAccount, _ := suite.service.AccountFor(context.Background(), common.AccountTypeCurrent, userId)

outgoingInvoices, err := invoicesFor(suite.service, userId, common.InvoiceTypeOutgoing)
if err != nil {
fmt.Printf("Error when getting invoices %v\n", err.Error())
}
assert.Equal(suite.T(), 1, len(invoices))
assert.Equal(suite.T(), common.InvoiceStateError, invoices[0].State)
assert.Equal(suite.T(), SendPaymentMockError, invoices[0].ErrorMessage)
incomingInvoices, err := invoicesFor(suite.service, userId, common.InvoiceTypeIncoming)
if err != nil {
fmt.Printf("Error when getting invoices %v\n", err.Error())
}
assert.Equal(suite.T(), 1, len(outgoingInvoices))
assert.Equal(suite.T(), common.InvoiceStateError, outgoingInvoices[0].State)
assert.Equal(suite.T(), SendPaymentMockError, outgoingInvoices[0].ErrorMessage)

transactionEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId)
if err != nil {
Expand All @@ -148,21 +160,69 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() {
fmt.Printf("Error when getting balance %v\n", err.Error())
}

// check if there are 5 transaction entries:
// - the incoming payment
// - the outgoing payment
// - the fee reserve + the fee reserve reversal
// - the outgoing payment reversal
// with reversed credit and debit account ids for payment 2/5 & payment 3/4
assert.Equal(suite.T(), 5, len(transactionEntries))
assert.Equal(suite.T(), transactionEntries[1].CreditAccountID, transactionEntries[4].DebitAccountID)
assert.Equal(suite.T(), transactionEntries[1].DebitAccountID, transactionEntries[4].CreditAccountID)
assert.Equal(suite.T(), transactionEntries[2].CreditAccountID, transactionEntries[3].DebitAccountID)
assert.Equal(suite.T(), transactionEntries[2].DebitAccountID, transactionEntries[3].CreditAccountID)
// check if there are 7 transaction entries:
// - [0] incoming
// - [1] outgoing
// - [2] fee_reserve
// - [3] service_fee
// - [4] fee_reserve_reversal
// - [5] service_fee_reversal
// - [6] outgoing_reversal
//

assert.Equal(suite.T(), 7, len(transactionEntries))

// the incoming funding
assert.Equal(suite.T(), int64(userFundingSats), transactionEntries[0].Amount)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[0].CreditAccountID)
assert.Equal(suite.T(), incomingAccount.ID, transactionEntries[0].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactionEntries[0].ParentID)
assert.Equal(suite.T(), incomingInvoices[0].ID, transactionEntries[0].InvoiceID)

// the outgoing payment
assert.Equal(suite.T(), int64(externalSatRequested), transactionEntries[1].Amount)
assert.Equal(suite.T(), outgoingAccount.ID, transactionEntries[1].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[1].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactionEntries[1].ParentID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[1].InvoiceID)

// fee reserve + fee reserve reversal
assert.Equal(suite.T(), transactionEntries[4].Amount, transactionEntries[2].Amount) // the amount of the fee_reserve and the fee_reserve_reversal must be equal
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[2].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[2].DebitAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[4].CreditAccountID)
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[4].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[2].InvoiceID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[4].InvoiceID)

// service fee
assert.Equal(suite.T(), int64(expectedServiceFee), transactionEntries[3].Amount)
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[3].CreditAccountID)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[3].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[3].InvoiceID)
// service fee reversal
assert.Equal(suite.T(), int64(expectedServiceFee), transactionEntries[5].Amount)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[5].CreditAccountID)
assert.Equal(suite.T(), feeAccount.ID, transactionEntries[5].DebitAccountID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[5].InvoiceID)

// the outgoing payment reversal
assert.Equal(suite.T(), int64(externalSatRequested), transactionEntries[6].Amount)
assert.Equal(suite.T(), currentAccount.ID, transactionEntries[6].CreditAccountID)
assert.Equal(suite.T(), outgoingAccount.ID, transactionEntries[6].DebitAccountID)
assert.Equal(suite.T(), int64(0), transactionEntries[1].ParentID)
assert.Equal(suite.T(), outgoingInvoices[0].ID, transactionEntries[6].InvoiceID)

// outgoing debit account must be the outgoing reversal credit account
assert.Equal(suite.T(), transactionEntries[1].CreditAccountID, transactionEntries[6].DebitAccountID)
assert.Equal(suite.T(), transactionEntries[1].DebitAccountID, transactionEntries[6].CreditAccountID)
// outgoing amounts and reversal amounts
assert.Equal(suite.T(), transactionEntries[1].Amount, int64(externalSatRequested))
assert.Equal(suite.T(), transactionEntries[4].Amount, int64(externalSatRequested))
assert.Equal(suite.T(), transactionEntries[6].Amount, int64(externalSatRequested))
// assert that balance is the same
assert.Equal(suite.T(), int64(userFundingSats), userBalance)
bumi marked this conversation as resolved.
Show resolved Hide resolved

suite.service.Config.ServiceFee = 0 // reset service fee - we don't expect this everywhere
}

func (suite *PaymentTestErrorsSuite) TearDownSuite() {
Expand Down
5 changes: 4 additions & 1 deletion integration_tests/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ const (
)

func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) {
dbUri := "postgresql://user:password@localhost/lndhub?sslmode=disable"
dbUri, ok := os.LookupEnv("DATABASE_URI")
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
dbUri = "postgresql://user:password@localhost/lndhub?sslmode=disable"
}
c := &service.Config{
DatabaseUri: dbUri,
DatabaseMaxConns: 1,
Expand Down
22 changes: 18 additions & 4 deletions lib/service/checkpayments.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func (svc *LndhubService) CheckPendingOutgoingPayments(ctx context.Context, pend
func (svc *LndhubService) GetTransactionEntryByInvoiceId(ctx context.Context, id int64) (models.TransactionEntry, error) {
entry := models.TransactionEntry{}
feeReserveEntry := models.TransactionEntry{}
serviceFeeEntry := models.TransactionEntry{}

err := svc.DB.NewSelect().Model(&entry).Where("invoice_id = ? and entry_type = ?", id, models.EntryTypeOutgoing).Limit(1).Scan(ctx)
if err != nil {
Expand All @@ -70,11 +71,24 @@ func (svc *LndhubService) GetTransactionEntryByInvoiceId(ctx context.Context, id
return entry, err
}
err = svc.DB.NewSelect().Model(&feeReserveEntry).Where("invoice_id = ? and entry_type = ?", id, models.EntryTypeFeeReserve).Limit(1).Scan(ctx)
if err != nil {
// The fee reserve transaction entry is optional thus we ignore NoRow errors.
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return entry, err
}
entry.FeeReserve = &feeReserveEntry
return entry, err
if err == nil {
entry.FeeReserve = &feeReserveEntry
}

err = svc.DB.NewSelect().Model(&serviceFeeEntry).Where("invoice_id = ? and entry_type = ?", id, models.EntryTypeServiceFee).Limit(1).Scan(ctx)
// The service fee transaction entry is optional thus we ignore NoRow errors.
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return entry, err
}
if err == nil {
entry.ServiceFee = &serviceFeeEntry
}

return entry, nil
}

// Should be called in a goroutine as the tracking can potentially take a long time
Expand Down Expand Up @@ -123,7 +137,7 @@ func (svc *LndhubService) TrackOutgoingPaymentstatus(ctx context.Context, invoic
return
}
if payment.Status == lnrpc.Payment_SUCCEEDED {
invoice.Fee = payment.FeeSat
invoice.SetFee(entry, payment.FeeSat)
invoice.Preimage = payment.PaymentPreimage
svc.Logger.Infof("Completed payment detected: hash %s", payment.PaymentHash)
err = svc.HandleSuccessfulPayment(ctx, invoice, entry)
Expand Down
2 changes: 2 additions & 0 deletions lib/service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type Config struct {
PrometheusPort int `envconfig:"PROMETHEUS_PORT" default:"9092"`
WebhookUrl string `envconfig:"WEBHOOK_URL"`
FeeReserve bool `envconfig:"FEE_RESERVE" default:"false"`
ServiceFee int `envconfig:"SERVICE_FEE" default:"0"`
NoServiceFeeUpToAmount int `envconfig:"NO_SERVICE_FEE_UP_TO_AMOUNT" default:"0"`
AllowAccountCreation bool `envconfig:"ALLOW_ACCOUNT_CREATION" default:"true"`
MinPasswordEntropy int `envconfig:"MIN_PASSWORD_ENTROPY" default:"0"`
MaxReceiveAmount int64 `envconfig:"MAX_RECEIVE_AMOUNT" default:"0"`
Expand Down
Loading
Loading