Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Fixed fee calculation for a change address #75

Merged
merged 1 commit into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
defaultUserAgent = "bux: " + version // Default user agent
defaultSleepForNewBlockHeaders = 30 * time.Second // Default wait before checking for a new unprocessed block
dustLimit = uint64(546) // Dust limit
changeOutputSize = uint64(35) // Average size in bytes of a change output
mongoTestVersion = "4.2.1" // Mongo Testing Version
sqliteTestVersion = "3.37.0" // SQLite Testing Version (dummy version for now)
version = "v0.2.9" // bux version
Expand Down
14 changes: 9 additions & 5 deletions model_draft_transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func (m *DraftTransaction) createTransactionHex(ctx context.Context) (err error)
var reservedUtxos []*Utxo
feePerByte := float64(m.Configuration.FeeUnit.Satoshis / m.Configuration.FeeUnit.Bytes)

reserveSatoshis := satoshisNeeded + m.estimateFee(m.Configuration.FeeUnit) + dustLimit
reserveSatoshis := satoshisNeeded + m.estimateFee(m.Configuration.FeeUnit, 0) + dustLimit
if reservedUtxos, err = reserveUtxos(
ctx, m.XpubID, m.ID, reserveSatoshis, feePerByte, m.Configuration.FromUtxos, opts...,
); err != nil {
Expand Down Expand Up @@ -268,7 +268,7 @@ func (m *DraftTransaction) createTransactionHex(ctx context.Context) (err error)
}

// Estimate the fee for the transaction
fee := m.estimateFee(m.Configuration.FeeUnit)
fee := m.estimateFee(m.Configuration.FeeUnit, 0)
if m.Configuration.SendAllTo != "" {
if m.Configuration.Outputs[0].Satoshis <= dustLimit {
return ErrOutputValueTooLow
Expand All @@ -286,11 +286,15 @@ func (m *DraftTransaction) createTransactionHex(ctx context.Context) (err error)
satoshisChange := satoshisReserved - satoshisNeeded - fee
m.Configuration.Fee = fee
if satoshisChange > 0 {
// we are adding an extra output and need to add that fee as well
newFee := m.estimateFee(m.Configuration.FeeUnit, changeOutputSize)
feeChange := m.Configuration.Fee - newFee
if err = m.setChangeDestination(
ctx, satoshisChange,
ctx, satoshisChange-feeChange,
); err != nil {
return
}
m.Configuration.Fee = newFee
}
}

Expand Down Expand Up @@ -373,8 +377,8 @@ func (m *DraftTransaction) estimateSize() uint64 {
}

// estimateFee will loop the inputs and outputs and estimate the required fee
func (m *DraftTransaction) estimateFee(unit *utils.FeeUnit) uint64 {
size := m.estimateSize()
func (m *DraftTransaction) estimateFee(unit *utils.FeeUnit, addToSize uint64) uint64 {
size := m.estimateSize() + addToSize
return uint64(math.Ceil(float64(size) * (float64(unit.Satoshis) / float64(unit.Bytes))))
}

Expand Down
148 changes: 107 additions & 41 deletions model_draft_transactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,9 @@ func TestDraftTransaction_createTransaction(t *testing.T) {

assert.Equal(t, testXPubID, draftTransaction.Configuration.ChangeDestinations[0].XpubID)
assert.Equal(t, draftTransaction.ID, draftTransaction.Configuration.ChangeDestinations[0].DraftID)
assert.Equal(t, uint64(98903), draftTransaction.Configuration.ChangeSatoshis)
assert.Equal(t, uint64(98920), draftTransaction.Configuration.ChangeSatoshis)

assert.Equal(t, uint64(97), draftTransaction.Configuration.Fee)
assert.Equal(t, uint64(114), draftTransaction.Configuration.Fee)
assert.Equal(t, defaultFee, draftTransaction.Configuration.FeeUnit)

assert.Equal(t, 1, len(draftTransaction.Configuration.Inputs))
Expand All @@ -277,7 +277,7 @@ func TestDraftTransaction_createTransaction(t *testing.T) {

assert.Equal(t, 2, len(draftTransaction.Configuration.Outputs))
assert.Equal(t, uint64(1000), draftTransaction.Configuration.Outputs[0].Satoshis)
assert.Equal(t, uint64(98903), draftTransaction.Configuration.Outputs[1].Satoshis)
assert.Equal(t, uint64(98920), draftTransaction.Configuration.Outputs[1].Satoshis)
assert.Equal(t, draftTransaction.Configuration.ChangeDestinations[0].LockingScript, draftTransaction.Configuration.Outputs[1].Scripts[0].Script)

var btTx *bt.Tx
Expand All @@ -292,7 +292,7 @@ func TestDraftTransaction_createTransaction(t *testing.T) {
assert.Equal(t, uint64(1000), btTx.Outputs[0].Satoshis)
assert.Equal(t, draftTransaction.Configuration.Outputs[0].Scripts[0].Script, btTx.Outputs[0].LockingScript.String())

assert.Equal(t, uint64(98903), btTx.Outputs[1].Satoshis)
assert.Equal(t, uint64(98920), btTx.Outputs[1].Satoshis)
assert.Equal(t, draftTransaction.Configuration.Outputs[1].Scripts[0].Script, btTx.Outputs[1].LockingScript.String())

var gUtxo *Utxo
Expand Down Expand Up @@ -339,6 +339,48 @@ func TestDraftTransaction_createTransaction(t *testing.T) {
assert.Equal(t, uint64(99903), draftTransaction.Configuration.Outputs[0].Scripts[0].Satoshis)
})

t.Run("fee calculation - MAP", func(t *testing.T) {
ctx, client, deferMe := CreateTestSQLiteClient(t, false, false, WithCustomTaskManager(&taskManagerMockBase{}))
defer deferMe()
xPub := newXpub(testXPub, append(client.DefaultModelOptions(), New())...)
err := xPub.Save(ctx)
require.NoError(t, err)

destination := newDestination(testXPubID, testLockingScript,
append(client.DefaultModelOptions(), New())...)
err = destination.Save(ctx)
require.NoError(t, err)

utxo := newUtxo(testXPubID, testTxID, testLockingScript, 0, 100000,
append(client.DefaultModelOptions(), New())...)
err = utxo.Save(ctx)
require.NoError(t, err)

draftTransaction := newDraftTransaction(testXPub, &TransactionConfig{
Outputs: []*TransactionOutput{{
To: testExternalAddress,
Satoshis: 1000,
}, {
OpReturn: &OpReturn{
Map: &MapProtocol{
App: "tonicpow_staging",
Keys: map[string]interface{}{
"offer_config_id": "336",
"offer_session_id": "4f06c11358e6586e67c77467c252a8be9187211f704de2627e4824945f31f07e",
},
Type: "offer_clicks",
},
},
}},
}, append(client.DefaultModelOptions(), New())...)

err = draftTransaction.createTransactionHex(ctx)
require.NoError(t, err)
fee := draftTransaction.Configuration.Fee
calculateFee := draftTransaction.estimateFee(draftTransaction.Configuration.FeeUnit, 0)
assert.Equal(t, fee, calculateFee)
})

t.Run("send to all - multiple utxos", func(t *testing.T) {
ctx, client, deferMe := CreateTestSQLiteClient(t, false, false, WithCustomTaskManager(&taskManagerMockBase{}))
defer deferMe()
Expand Down Expand Up @@ -483,7 +525,7 @@ func TestDraftTransaction_createTransaction(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, testXPubID, draftTransaction.XpubID)
assert.Equal(t, DraftStatusDraft, draftTransaction.Status)
assert.Equal(t, uint64(1184), draftTransaction.Configuration.Fee)
assert.Equal(t, uint64(1201), draftTransaction.Configuration.Fee)
assert.Len(t, draftTransaction.Configuration.Inputs, 2)
assert.Len(t, draftTransaction.Configuration.Outputs, 3)

Expand All @@ -503,6 +545,8 @@ func TestDraftTransaction_createTransaction(t *testing.T) {
assert.Equal(t, "", draftTransaction.Configuration.Outputs[1].Scripts[0].Address)
assert.Equal(t, uint64(564), draftTransaction.Configuration.Outputs[1].Scripts[0].Satoshis)
assert.Equal(t, testSTASLockingScript, draftTransaction.Configuration.Outputs[1].Scripts[0].Script)

assert.Equal(t, uint64(97269), draftTransaction.Configuration.Outputs[2].Satoshis)
})
}

Expand Down Expand Up @@ -885,48 +929,70 @@ func createDraftTransactionFromHex(hex string, inInfo []interface{}) (*DraftTran
}

func TestDraftTransaction_estimateFees(t *testing.T) {
jsonFile, err := os.Open("./tests/model_draft_transactions_test.json")
require.NoError(t, err)
defer func() {
_ = jsonFile.Close()
}()

byteValue, bErr := ioutil.ReadAll(jsonFile)
require.NoError(t, bErr)
t.Run("json data", func(t *testing.T) {
jsonFile, err := os.Open("./tests/model_draft_transactions_test.json")
require.NoError(t, err)
defer func() {
_ = jsonFile.Close()
}()

var testData map[string]interface{}
err = json.Unmarshal(byteValue, &testData)
require.NoError(t, err)
byteValue, bErr := ioutil.ReadAll(jsonFile)
require.NoError(t, bErr)

feeUnit := utils.FeeUnit{
Satoshis: 1,
Bytes: 2,
}
var testData map[string]interface{}
err = json.Unmarshal(byteValue, &testData)
require.NoError(t, err)

for _, inTx := range testData["rawTransactions"].([]interface{}) {
in := inTx.(map[string]interface{})
txID := in["txId"].(string)
draftTransaction, tx, err2 := createDraftTransactionFromHex(in["hex"].(string), in["inputs"].([]interface{}))
require.NoError(t, err2)
assert.Equal(t, txID, tx.TxID())
assert.IsType(t, DraftTransaction{}, *draftTransaction)
assert.IsType(t, bt.Tx{}, *tx)

realFee := uint64(0)
for _, input := range in["inputs"].([]interface{}) {
i := input.(map[string]interface{})
realFee += uint64(i["satoshis"].(float64))
feeUnit := utils.FeeUnit{
Satoshis: 1,
Bytes: 2,
}
for _, output := range tx.Outputs {
realFee -= output.Satoshis

for _, inTx := range testData["rawTransactions"].([]interface{}) {
in := inTx.(map[string]interface{})
txID := in["txId"].(string)
draftTransaction, tx, err2 := createDraftTransactionFromHex(in["hex"].(string), in["inputs"].([]interface{}))
require.NoError(t, err2)
assert.Equal(t, txID, tx.TxID())
assert.IsType(t, DraftTransaction{}, *draftTransaction)
assert.IsType(t, bt.Tx{}, *tx)

realFee := uint64(0)
for _, input := range in["inputs"].([]interface{}) {
i := input.(map[string]interface{})
realFee += uint64(i["satoshis"].(float64))
}
for _, output := range tx.Outputs {
realFee -= output.Satoshis
}

realSize := uint64(float64(len(in["hex"].(string))) / 2)
sizeEstimate := draftTransaction.estimateSize()
feeEstimate := draftTransaction.estimateFee(&feeUnit, 0)
assert.Greater(t, sizeEstimate, realSize)
assert.Greater(t, feeEstimate, realFee)
}
})

realSize := uint64(float64(len(in["hex"].(string))) / 2)
sizeEstimate := draftTransaction.estimateSize()
feeEstimate := draftTransaction.estimateFee(&feeUnit)
assert.Greater(t, sizeEstimate, realSize)
assert.Greater(t, feeEstimate, realFee)
}
t.Run("json data - low fee", func(t *testing.T) {
jsonFile, err := os.Open("./tests/model_draft_transaction_low_fee.json")
require.NoError(t, err)
defer func() {
_ = jsonFile.Close()
}()
byteValue, bErr := ioutil.ReadAll(jsonFile)
require.NoError(t, bErr)

var testData TransactionConfig
err = json.Unmarshal(byteValue, &testData)
require.NoError(t, err)

draft := &DraftTransaction{Configuration: testData}
assert.IsType(t, DraftTransaction{}, *draft)

fee := draft.estimateFee(draft.Configuration.FeeUnit, 0)
assert.Equal(t, uint64(227), fee)
})
}

// TestDraftTransaction_RegisterTasks will test the method RegisterTasks()
Expand Down
4 changes: 2 additions & 2 deletions model_transactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ func TestEndToEndTransaction(t *testing.T) {

// Check that the transaction was saved
assert.Equal(t, draftTransaction.ID, finalTx.DraftID)
assert.Equal(t, uint64(4903), finalTx.TotalValue)
assert.Equal(t, uint64(97), finalTx.Fee)
assert.Equal(t, uint64(4886), finalTx.TotalValue)
assert.Equal(t, uint64(114), finalTx.Fee)
})
}
Loading