diff --git a/rpcserver.go b/rpcserver.go index 0e9a0feae..b302b1546 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -994,6 +994,6 @@ func (r *rpcServer) SendAsset(ctx context.Context, PrevInputs: prevInputs, NewOutputs: newOutputs, }, - TotalFeeSats: 0, + TotalFeeSats: int64(resp.TotalFees), }, nil } diff --git a/tarodb/asset_minting.go b/tarodb/asset_minting.go index 9145a99dc..31a447bf5 100644 --- a/tarodb/asset_minting.go +++ b/tarodb/asset_minting.go @@ -712,8 +712,9 @@ func (a *AssetMintingStore) CommitSignedGenesisTx(ctx context.Context, // chain transaction, as that chain transaction will be // referenced by the managed UTXO. chainTXID, err := q.UpsertChainTx(ctx, ChainTx{ - Txid: genTXID[:], - RawTx: txBuf.Bytes(), + Txid: genTXID[:], + RawTx: txBuf.Bytes(), + ChainFees: genesisPkt.ChainFees, }) if err != nil { return fmt.Errorf("unable to insert chain tx: %w", err) diff --git a/tarodb/asset_minting_test.go b/tarodb/asset_minting_test.go index 943e0b669..617afc109 100644 --- a/tarodb/asset_minting_test.go +++ b/tarodb/asset_minting_test.go @@ -295,6 +295,7 @@ func randGenesisPacket(t *testing.T) *tarogarden.FundedPsbt { return &tarogarden.FundedPsbt{ Pkt: psbt, ChangeOutputIndex: 1, + ChainFees: 100, } } @@ -454,6 +455,7 @@ func TestCommitBatchChainActions(t *testing.T) { require.NoError(t, err) require.Equal(t, genTXID[:], dbGenTx.Txid[:]) require.Equal(t, rawTxBytes.Bytes(), dbGenTx.RawTx) + require.Equal(t, genesisPkt.ChainFees, dbGenTx.ChainFees) // Now that we have the primary key for the chain transaction inserted // above, we'll use that to confirm that the managed UTXO has been diff --git a/tarodb/assets_store.go b/tarodb/assets_store.go index c37da7a88..60a8b0db1 100644 --- a/tarodb/assets_store.go +++ b/tarodb/assets_store.go @@ -1263,8 +1263,9 @@ func (a *AssetStore) LogPendingParcel(ctx context.Context, // Next, we'll insert the new transaction that anchors the new // anchor point (commits to the set of new outputs). txnID, err := q.UpsertChainTx(ctx, ChainTx{ - Txid: newAnchorTXID[:], - RawTx: anchorTxBytes, + Txid: newAnchorTXID[:], + RawTx: anchorTxBytes, + ChainFees: spend.ChainFees, }) if err != nil { return fmt.Errorf("unable to insert new chain "+ @@ -1640,6 +1641,7 @@ func (a *AssetStore) QueryParcels(ctx context.Context, AnchorTx: anchorTx, AssetSpendDeltas: spendDeltas, TransferTime: xfer.TransferTimeUnix, + ChainFees: xfer.ChainFees, }) } diff --git a/tarodb/assets_store_test.go b/tarodb/assets_store_test.go index b7e12fbd5..48c2171f8 100644 --- a/tarodb/assets_store_test.go +++ b/tarodb/assets_store_test.go @@ -811,6 +811,8 @@ func TestAssetExportLog(t *testing.T) { SplitCommitment: nil, } + chainFees := int64(100) + // With the assets inserted, we'll now construct the struct we'll used // to commit a new spend on disk. anchorTxHash := newAnchorTx.TxHash() @@ -849,6 +851,7 @@ func TestAssetExportLog(t *testing.T) { ReceiverAssetProof: receiverBlob, }, }, + ChainFees: int64(chainFees), } require.NoError(t, assetsStore.LogPendingParcel(ctx, spendDelta)) @@ -941,6 +944,7 @@ func TestAssetExportLog(t *testing.T) { t, uint32(blockHeight), extractSqlInt32[uint32](anchorTx.BlockHeight), ) require.Equal(t, uint32(txIndex), extractSqlInt32[uint32](anchorTx.TxIndex)) + require.Equal(t, chainFees, anchorTx.ChainFees) // At this point, there should be no more pending parcels. parcels, err = assetsStore.PendingParcels(ctx) diff --git a/tarodb/sqlite/assets.sql.go b/tarodb/sqlite/assets.sql.go index 5b71bfd55..d95b7d6d9 100644 --- a/tarodb/sqlite/assets.sql.go +++ b/tarodb/sqlite/assets.sql.go @@ -720,7 +720,7 @@ func (q *Queries) FetchAssetsForBatch(ctx context.Context, rawKey []byte) ([]Fet } const fetchChainTx = `-- name: FetchChainTx :one -SELECT txn_id, txid, raw_tx, block_height, block_hash, tx_index +SELECT txn_id, txid, chain_fees, raw_tx, block_height, block_hash, tx_index FROM chain_txns WHERE txid = ? ` @@ -731,6 +731,7 @@ func (q *Queries) FetchChainTx(ctx context.Context, txid []byte) (ChainTxn, erro err := row.Scan( &i.TxnID, &i.Txid, + &i.ChainFees, &i.RawTx, &i.BlockHeight, &i.BlockHash, @@ -1630,9 +1631,9 @@ func (q *Queries) UpsertAssetProof(ctx context.Context, arg UpsertAssetProofPara const upsertChainTx = `-- name: UpsertChainTx :one INSERT INTO chain_txns ( - txid, raw_tx, block_height, block_hash, tx_index + txid, raw_tx, chain_fees, block_height, block_hash, tx_index ) VALUES ( - ?, ?, ?, ?, + ?, ?, ?, ?, ?, ? ) ON CONFLICT (txid) -- Not a NOP but instead update any nullable fields that aren't null in the @@ -1646,6 +1647,7 @@ RETURNING txn_id type UpsertChainTxParams struct { Txid []byte RawTx []byte + ChainFees int64 BlockHeight sql.NullInt32 BlockHash []byte TxIndex sql.NullInt32 @@ -1655,6 +1657,7 @@ func (q *Queries) UpsertChainTx(ctx context.Context, arg UpsertChainTxParams) (i row := q.db.QueryRowContext(ctx, upsertChainTx, arg.Txid, arg.RawTx, + arg.ChainFees, arg.BlockHeight, arg.BlockHash, arg.TxIndex, diff --git a/tarodb/sqlite/migrations/000002_assets.up.sql b/tarodb/sqlite/migrations/000002_assets.up.sql index 35cbe8dce..125a587e8 100644 --- a/tarodb/sqlite/migrations/000002_assets.up.sql +++ b/tarodb/sqlite/migrations/000002_assets.up.sql @@ -7,13 +7,15 @@ CREATE TABLE IF NOT EXISTS chain_txns ( txid BLOB UNIQUE NOT NULL, + chain_fees BIGINT NOT NULL, + raw_tx BLOB NOT NULL, block_height INTEGER, block_hash BLOB, - tx_index INTEGER + tx_index INTEGER ); -- genesis_points stores all genesis_points relevant to tardo, which is the diff --git a/tarodb/sqlite/models.go b/tarodb/sqlite/models.go index 7ed7a188d..4f1b48de4 100644 --- a/tarodb/sqlite/models.go +++ b/tarodb/sqlite/models.go @@ -123,6 +123,7 @@ type AssetWitness struct { type ChainTxn struct { TxnID int32 Txid []byte + ChainFees int64 RawTx []byte BlockHeight sql.NullInt32 BlockHash []byte diff --git a/tarodb/sqlite/queries/assets.sql b/tarodb/sqlite/queries/assets.sql index 68cffbb16..e4bd2b416 100644 --- a/tarodb/sqlite/queries/assets.sql +++ b/tarodb/sqlite/queries/assets.sql @@ -331,9 +331,9 @@ WHERE batch_id in (SELECT batch_id FROM target_batch); -- name: UpsertChainTx :one INSERT INTO chain_txns ( - txid, raw_tx, block_height, block_hash, tx_index + txid, raw_tx, chain_fees, block_height, block_hash, tx_index ) VALUES ( - ?, ?, sqlc.narg('block_height'), sqlc.narg('block_hash'), + ?, ?, ?, sqlc.narg('block_height'), sqlc.narg('block_hash'), sqlc.narg('tx_index') ) ON CONFLICT (txid) -- Not a NOP but instead update any nullable fields that aren't null in the diff --git a/tarodb/sqlite/queries/transfers.sql b/tarodb/sqlite/queries/transfers.sql index c7a68b8ee..80baf9e51 100644 --- a/tarodb/sqlite/queries/transfers.sql +++ b/tarodb/sqlite/queries/transfers.sql @@ -23,12 +23,12 @@ INSERT INTO transfer_proofs ( -- name: QueryAssetTransfers :many SELECT asset_transfers.old_anchor_point, utxos.outpoint AS new_anchor_point, - utxos.taro_root, utxos.tapscript_sibling, utxos.utxo_id AS new_anchor_utxo_id, - txns.raw_tx AS anchor_tx_bytes, txns.txid AS anchor_txid, - txns.txn_id AS anchor_tx_primary_key, transfer_time_unix, - keys.raw_key AS internal_key_bytes, keys.key_family AS internal_key_fam, - keys.key_index AS internal_key_index, id AS transfer_id, - transfer_time_unix + utxos.taro_root, utxos.tapscript_sibling, + utxos.utxo_id AS new_anchor_utxo_id, txns.raw_tx AS anchor_tx_bytes, + txns.txid AS anchor_txid, txns.txn_id AS anchor_tx_primary_key, + txns.chain_fees, transfer_time_unix, keys.raw_key AS internal_key_bytes, + keys.key_family AS internal_key_fam, keys.key_index AS internal_key_index, + id AS transfer_id, transfer_time_unix FROM asset_transfers JOIN internal_keys keys ON asset_transfers.new_internal_key = keys.key_id diff --git a/tarodb/sqlite/transfers.sql.go b/tarodb/sqlite/transfers.sql.go index 3e39b7908..5ea4a6f66 100644 --- a/tarodb/sqlite/transfers.sql.go +++ b/tarodb/sqlite/transfers.sql.go @@ -312,12 +312,12 @@ func (q *Queries) InsertSpendProofs(ctx context.Context, arg InsertSpendProofsPa const queryAssetTransfers = `-- name: QueryAssetTransfers :many SELECT asset_transfers.old_anchor_point, utxos.outpoint AS new_anchor_point, - utxos.taro_root, utxos.tapscript_sibling, utxos.utxo_id AS new_anchor_utxo_id, - txns.raw_tx AS anchor_tx_bytes, txns.txid AS anchor_txid, - txns.txn_id AS anchor_tx_primary_key, transfer_time_unix, - keys.raw_key AS internal_key_bytes, keys.key_family AS internal_key_fam, - keys.key_index AS internal_key_index, id AS transfer_id, - transfer_time_unix + utxos.taro_root, utxos.tapscript_sibling, + utxos.utxo_id AS new_anchor_utxo_id, txns.raw_tx AS anchor_tx_bytes, + txns.txid AS anchor_txid, txns.txn_id AS anchor_tx_primary_key, + txns.chain_fees, transfer_time_unix, keys.raw_key AS internal_key_bytes, + keys.key_family AS internal_key_fam, keys.key_index AS internal_key_index, + id AS transfer_id, transfer_time_unix FROM asset_transfers JOIN internal_keys keys ON asset_transfers.new_internal_key = keys.key_id @@ -356,6 +356,7 @@ type QueryAssetTransfersRow struct { AnchorTxBytes []byte AnchorTxid []byte AnchorTxPrimaryKey int32 + ChainFees int64 TransferTimeUnix time.Time InternalKeyBytes []byte InternalKeyFam int32 @@ -382,6 +383,7 @@ func (q *Queries) QueryAssetTransfers(ctx context.Context, arg QueryAssetTransfe &i.AnchorTxBytes, &i.AnchorTxid, &i.AnchorTxPrimaryKey, + &i.ChainFees, &i.TransferTimeUnix, &i.InternalKeyBytes, &i.InternalKeyFam, diff --git a/tarofreighter/chain_porter.go b/tarofreighter/chain_porter.go index 27fe0ff01..6ab3bd975 100644 --- a/tarofreighter/chain_porter.go +++ b/tarofreighter/chain_porter.go @@ -934,6 +934,12 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) { return nil, err } + chainFees, err := tarogarden.GetTxFee(currentPkg.SendPkt) + if err != nil { + return nil, fmt.Errorf("unable to get on-chain fees "+ + "for psbt: %w", err) + } + // Before we broadcast, we'll write to disk that we have a // pending outbound parcel. If we crash before this point, // we'll start all over. Otherwise, we'll come back to this @@ -965,6 +971,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) { TapscriptSibling: currentPkg.InputAsset.TapscriptSibling, // TODO(bhandras): use clock.Clock instead. TransferTime: time.Now(), + ChainFees: chainFees, } // Don't allow shutdown while we're attempting to store proofs. diff --git a/tarofreighter/interface.go b/tarofreighter/interface.go index 725ca1db9..7871b3106 100644 --- a/tarofreighter/interface.go +++ b/tarofreighter/interface.go @@ -156,6 +156,10 @@ type OutboundParcelDelta struct { // TransferTime holds the timestamp of the outbound spend. TransferTime time.Time + + // ChainFees is the amount in sats paid in on-chain fees for the + // anchor transaction. + ChainFees int64 } // AssetConfirmEvent is used to mark a batched spend as confirmed on disk. diff --git a/tarofreighter/parcel.go b/tarofreighter/parcel.go index 41cf8b2f0..9118b8353 100644 --- a/tarofreighter/parcel.go +++ b/tarofreighter/parcel.go @@ -631,6 +631,6 @@ func (s *sendPackage) deliverResponse(respChan chan<- *PendingParcel) { }, }, }, - TotalFees: 0, + TotalFees: btcutil.Amount(s.OutboundPkg.ChainFees), } } diff --git a/tarogarden/caretaker.go b/tarogarden/caretaker.go index 48ce21e86..c3bed03ad 100644 --- a/tarogarden/caretaker.go +++ b/tarogarden/caretaker.go @@ -503,6 +503,14 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error) } b.cfg.Batch.GenesisPacket.Pkt = signedPkt + // Populate how much this tx paid in on-chain fees. + chainFees, err := GetTxFee(signedPkt) + if err != nil { + return 0, fmt.Errorf("unable to get on-chain fees "+ + "for psbt: %w", err) + } + b.cfg.Batch.GenesisPacket.ChainFees = chainFees + log.Infof("BatchCaretaker(%x): GenesisPacket finalized: %v", b.batchKey[:], spew.Sdump(signedPkt)) @@ -739,3 +747,18 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error) return 0, fmt.Errorf("unknown state: %v", currentState) } } + +// GetTxFee returns the value of the on-chain fees paid by a finalized PSBT. +func GetTxFee(pkt *psbt.Packet) (int64, error) { + inputValue, err := psbt.SumUtxoInputValues(pkt) + if err != nil { + return 0, fmt.Errorf("unable to sum input values: %v", err) + } + + outputValue := int64(0) + for _, out := range pkt.UnsignedTx.TxOut { + outputValue += out.Value + } + + return inputValue - outputValue, nil +} diff --git a/tarogarden/interface.go b/tarogarden/interface.go index 78e2f4230..8f4274fdc 100644 --- a/tarogarden/interface.go +++ b/tarogarden/interface.go @@ -271,6 +271,10 @@ type FundedPsbt struct { // Taro commitment (the non-change output). ChangeOutputIndex uint32 + // ChainFees is the amount in sats paid in on-chain fees for this + // transaction. + ChainFees int64 + // LockedUTXOs is the set of UTXOs that were locked to create the PSBT // packet. LockedUTXOs []wire.OutPoint