From 317b4e11300dee4c2975d8fc8cb3a06b0807fa97 Mon Sep 17 00:00:00 2001 From: Siggi Date: Thu, 21 Apr 2022 22:31:47 +0200 Subject: [PATCH] Fixed fee calculation for a change address --- definitions.go | 1 + model_draft_transactions.go | 14 +- model_draft_transactions_test.go | 148 +++++++++++++++------ model_transactions_test.go | 4 +- tests/model_draft_transaction_low_fee.json | 132 ++++++++++++++++++ 5 files changed, 251 insertions(+), 48 deletions(-) create mode 100644 tests/model_draft_transaction_low_fee.json diff --git a/definitions.go b/definitions.go index 8665eef3..c904bd9b 100644 --- a/definitions.go +++ b/definitions.go @@ -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 diff --git a/model_draft_transactions.go b/model_draft_transactions.go index 6169f4f6..08806c06 100644 --- a/model_draft_transactions.go +++ b/model_draft_transactions.go @@ -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 { @@ -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 @@ -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 } } @@ -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)))) } diff --git a/model_draft_transactions_test.go b/model_draft_transactions_test.go index 3c38a4f7..4f07fb88 100644 --- a/model_draft_transactions_test.go +++ b/model_draft_transactions_test.go @@ -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)) @@ -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 @@ -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 @@ -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() @@ -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) @@ -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) }) } @@ -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() diff --git a/model_transactions_test.go b/model_transactions_test.go index e6931397..7652688f 100644 --- a/model_transactions_test.go +++ b/model_transactions_test.go @@ -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) }) } diff --git a/tests/model_draft_transaction_low_fee.json b/tests/model_draft_transaction_low_fee.json new file mode 100644 index 00000000..3af175e3 --- /dev/null +++ b/tests/model_draft_transaction_low_fee.json @@ -0,0 +1,132 @@ +{ + "change_destinations": [ + { + "created_at": "2022-04-21T18:16:13.876238782Z", + "updated_at": "2022-04-21T14:16:13.876-04:00", + "deleted_at": null, + "id": "60511f9c262f35306f50093e2c9ace4b8416b7ea191f800e85096b8a117cdda2", + "xpub_id": "d11a91246b358d72b9bc24c3b303595941e305a2c8ab21cfbc3ed1913b4d9a72", + "locking_script": "76a9145441466c3085a2d97dc283f77d6c8bd1db5c2d8c88ac", + "type": "pubkeyhash", + "chain": 1, + "num": 6, + "address": "18gVxWNYVdHCianqiwgAYMNmQK7vLheCJg", + "draft_id": "21b2569f420b92fffa833044c3ee491a8b52018f604b1ee682255fbe3a127826", + "monitor": null + } + ], + "change_destinations_strategy": "default", + "change_minimum_satoshis": 0, + "change_number_of_destinations": 0, + "change_satoshis": 265190, + "expires_in": 5000000000, + "fee": 210, + "fee_unit": { + "satoshis": 1, + "bytes": 2 + }, + "from_utxos": null, + "include_utxos": null, + "inputs": [ + { + "created_at": "2022-04-08T11:53:16-04:00", + "updated_at": "2022-04-21T18:16:13.855368654Z", + "deleted_at": null, + "transaction_id": "f4e5e746cac97348d7e2eeb201a5f2bb3c501f8eac7aa1ca83cdd82f59f7af0e", + "output_index": 0, + "id": "035901370f0fad940085b9c816941303b366b2019db1d4da16f313814ef3d897", + "xpub_id": "d11a91246b358d72b9bc24c3b303595941e305a2c8ab21cfbc3ed1913b4d9a72", + "satoshis": 278454, + "script_pub_key": "76a9140e7422c38f75344d203c966e72503c5414d6e88588ac", + "type": "pubkeyhash", + "draft_id": "21b2569f420b92fffa833044c3ee491a8b52018f604b1ee682255fbe3a127826", + "reserved_at": "2022-04-21T18:16:13.853939468Z", + "spending_tx_id": "", + "destination": { + "created_at": "2022-04-08T11:53:14-04:00", + "updated_at": "2022-04-08T11:53:14-04:00", + "metadata": { + "domain": "staging.tonicpow.com", + "ip_address": "18.198.131.132", + "paymail_request": "CreateP2PDestinationResponse", + "reference_id": "661a7ff9e9e097073293d538d4d71363", + "satoshis": 278454, + "user_agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" + }, + "deleted_at": null, + "id": "a787d6fba8a972cedc0777f4310f70e9cdd3e42307aed62b758d3d91a39b3746", + "xpub_id": "d11a91246b358d72b9bc24c3b303595941e305a2c8ab21cfbc3ed1913b4d9a72", + "locking_script": "76a9140e7422c38f75344d203c966e72503c5414d6e88588ac", + "type": "pubkeyhash", + "chain": 0, + "num": 8, + "address": "12KRawefdXjZxMwWCbJu9A9sZ21dwfXuiz", + "draft_id": "", + "monitor": null + } + } + ], + "miner": "", + "outputs": [ + { + "satoshis": 11867, + "scripts": [ + { + "address": "1AU6q3ZXjRZ8bvsLsSkfmnMCC1UUcztDuv", + "satoshis": 11867, + "script": "76a91467d93a70ac575e15abb31bc8272a00ab1495d48388ac", + "script_type": "pubkeyhash" + } + ], + "to": "1AU6q3ZXjRZ8bvsLsSkfmnMCC1UUcztDuv" + }, + { + "satoshis": 1187, + "scripts": [ + { + "address": "14oB5TYow2dRTxmmkGscWJ57tF7yxcju8Y", + "satoshis": 1187, + "script": "76a91429a3ec5f653e0403737a96261cc01188b0f1f61488ac", + "script_type": "pubkeyhash" + } + ], + "to": "14oB5TYow2dRTxmmkGscWJ57tF7yxcju8Y" + }, + { + "satoshis": 0, + "scripts": [ + { + "script": "006a223150755161374b36324d694b43747373534c4b79316b683536575755374d74555235035345540361707010746f6e6963706f775f73746167696e6704747970650b6f666665725f636c69636b0f6f666665725f636f6e6669675f696403333336106f666665725f73657373696f6e5f69644034663036633131333538653635383665363763373734363763323532613862653931383732313166373034646532363237653438323439343566333166303765", + "script_type": "nulldata" + } + ], + "op_return": { + "map": { + "app": "tonicpow_staging", + "keys": { + "offer_config_id": "336", + "offer_session_id": "4f06c11358e6586e67c77467c252a8be9187211f704de2627e4824945f31f07e" + }, + "type": "offer_click" + } + } + }, + { + "satoshis": 265190, + "scripts": [ + { + "address": "18gVxWNYVdHCianqiwgAYMNmQK7vLheCJg", + "satoshis": 265190, + "script": "76a9145441466c3085a2d97dc283f77d6c8bd1db5c2d8c88ac", + "script_type": "" + } + ], + "to": "18gVxWNYVdHCianqiwgAYMNmQK7vLheCJg" + } + ], + "send_all_to": "", + "sync": { + "broadcast": true, + "sync_on_chain": true + } +}