Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
95
backend/mempool/fee/oracle.go
Normal file
95
backend/mempool/fee/oracle.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package fee
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Oracle provides gas price estimates
|
||||
type Oracle struct {
|
||||
db *pgxpool.Pool
|
||||
chainID int
|
||||
}
|
||||
|
||||
// NewOracle creates a new fee oracle
|
||||
func NewOracle(db *pgxpool.Pool, chainID int) *Oracle {
|
||||
return &Oracle{
|
||||
db: db,
|
||||
chainID: chainID,
|
||||
}
|
||||
}
|
||||
|
||||
// FeeEstimate represents gas price estimates
|
||||
type FeeEstimate struct {
|
||||
Slow string `json:"slow"`
|
||||
Standard string `json:"standard"`
|
||||
Fast string `json:"fast"`
|
||||
Urgent string `json:"urgent"`
|
||||
}
|
||||
|
||||
// GetFeeEstimates gets current fee estimates
|
||||
func (o *Oracle) GetFeeEstimates(ctx context.Context) (*FeeEstimate, error) {
|
||||
// Get recent gas prices from last 100 blocks
|
||||
query := `
|
||||
SELECT avg_gas_price
|
||||
FROM gas_price_history
|
||||
WHERE chain_id = $1
|
||||
ORDER BY time DESC
|
||||
LIMIT 100
|
||||
`
|
||||
|
||||
rows, err := o.db.Query(ctx, query, o.chainID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query gas prices: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var prices []int64
|
||||
for rows.Next() {
|
||||
var price int64
|
||||
if err := rows.Scan(&price); err == nil {
|
||||
prices = append(prices, price)
|
||||
}
|
||||
}
|
||||
|
||||
if len(prices) == 0 {
|
||||
// Return default estimates if no data
|
||||
return &FeeEstimate{
|
||||
Slow: "20000000000",
|
||||
Standard: "30000000000",
|
||||
Fast: "50000000000",
|
||||
Urgent: "100000000000",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Calculate percentiles
|
||||
p25 := percentile(prices, 0.25)
|
||||
p50 := percentile(prices, 0.50)
|
||||
p75 := percentile(prices, 0.75)
|
||||
p95 := percentile(prices, 0.95)
|
||||
|
||||
return &FeeEstimate{
|
||||
Slow: big.NewInt(p25).String(),
|
||||
Standard: big.NewInt(p50).String(),
|
||||
Fast: big.NewInt(p75).String(),
|
||||
Urgent: big.NewInt(p95).String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// percentile calculates percentile of sorted slice
|
||||
func percentile(data []int64, p float64) int64 {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
index := int(float64(len(data)) * p)
|
||||
if index >= len(data) {
|
||||
index = len(data) - 1
|
||||
}
|
||||
|
||||
return data[index]
|
||||
}
|
||||
|
||||
94
backend/mempool/tracker.go
Normal file
94
backend/mempool/tracker.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package mempool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Tracker tracks pending transactions in the mempool
|
||||
type Tracker struct {
|
||||
db *pgxpool.Pool
|
||||
client *ethclient.Client
|
||||
chainID int
|
||||
}
|
||||
|
||||
// NewTracker creates a new mempool tracker
|
||||
func NewTracker(db *pgxpool.Pool, client *ethclient.Client, chainID int) *Tracker {
|
||||
return &Tracker{
|
||||
db: db,
|
||||
client: client,
|
||||
chainID: chainID,
|
||||
}
|
||||
}
|
||||
|
||||
// TrackPendingTransaction tracks a pending transaction
|
||||
func (t *Tracker) TrackPendingTransaction(ctx context.Context, tx *types.Transaction) error {
|
||||
from, _ := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
|
||||
|
||||
var toAddress sql.NullString
|
||||
if tx.To() != nil {
|
||||
toAddress.String = tx.To().Hex()
|
||||
toAddress.Valid = true
|
||||
}
|
||||
|
||||
var maxFeePerGas, maxPriorityFeePerGas sql.NullInt64
|
||||
if tx.Type() == types.DynamicFeeTxType {
|
||||
if tx.GasFeeCap() != nil {
|
||||
maxFeePerGas.Int64 = tx.GasFeeCap().Int64()
|
||||
maxFeePerGas.Valid = true
|
||||
}
|
||||
if tx.GasTipCap() != nil {
|
||||
maxPriorityFeePerGas.Int64 = tx.GasTipCap().Int64()
|
||||
maxPriorityFeePerGas.Valid = true
|
||||
}
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO mempool_transactions (
|
||||
time, chain_id, hash, from_address, to_address, value,
|
||||
gas_price, max_fee_per_gas, max_priority_fee_per_gas,
|
||||
gas_limit, nonce, input_data_length, first_seen, status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
ON CONFLICT (time, chain_id, hash) DO UPDATE SET
|
||||
status = $14,
|
||||
updated_at = NOW()
|
||||
`
|
||||
|
||||
_, err := t.db.Exec(ctx, query,
|
||||
time.Now(),
|
||||
t.chainID,
|
||||
tx.Hash().Hex(),
|
||||
from.Hex(),
|
||||
toAddress,
|
||||
tx.Value().String(),
|
||||
tx.GasPrice().Int64(),
|
||||
maxFeePerGas,
|
||||
maxPriorityFeePerGas,
|
||||
tx.Gas(),
|
||||
tx.Nonce(),
|
||||
len(tx.Data()),
|
||||
time.Now(),
|
||||
"pending",
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateTransactionStatus updates transaction status when confirmed
|
||||
func (t *Tracker) UpdateTransactionStatus(ctx context.Context, txHash common.Hash, blockNumber int64, status string) error {
|
||||
query := `
|
||||
UPDATE mempool_transactions
|
||||
SET status = $1, confirmed_block_number = $2, confirmed_at = NOW()
|
||||
WHERE chain_id = $3 AND hash = $4
|
||||
`
|
||||
|
||||
_, err := t.db.Exec(ctx, query, status, blockNumber, t.chainID, txHash.Hex())
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user