Skip to content
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
86 changes: 44 additions & 42 deletions tools/preconf-rpc/notifier/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
"github.com/primev/mev-commit/tools/preconf-rpc/sender"
"github.com/primev/mev-commit/x/util"
)

// Message represents a notification message structure
Expand Down Expand Up @@ -194,6 +195,33 @@ func (n *Notifier) SendBidderFundedNotification(
return n.SendMessage(ctx, message)
}

func buildStatus(t txnInfo) string {
switch t.txn.Status {
case sender.TxStatusPreConfirmed:
return "⚡ Pre-Confirmed"
case sender.TxStatusConfirmed:
return "✅ Confirmed"
case sender.TxStatusFailed:
return fmt.Sprintf("❌ Failed (Error: %s)", t.txn.Details)
default:
return "❓ Unknown"
}
}

func buildType(t txnInfo) string {
switch t.txn.Type {
case sender.TxTypeRegular:
classification := util.ClassifyTxOnly(t.txn.Transaction, t.txn.Sender)
return fmt.Sprintf("💸 %s (%s)", classification.Kind, classification.Details)
case sender.TxTypeDeposit:
return "🏦 Deposit"
case sender.TxTypeInstantBridge:
return "🌉 Instant Bridge"
default:
return "❓ Unknown"
}
}

func (n *Notifier) StartTransactionNotifier(
ctx context.Context,
) <-chan struct{} {
Expand All @@ -220,52 +248,26 @@ func (n *Notifier) StartTransactionNotifier(
n.queuedTxns = nil
n.queuedMu.Unlock()
// create markdown table with the txn info
text := ""
for _, txnInfo := range txnsToNotify {
status := ""
switch txnInfo.txn.Status {
case sender.TxStatusPreConfirmed:
status = "⚡ Pre-Confirmed"
case sender.TxStatusConfirmed:
status = "✅ Confirmed"
case sender.TxStatusFailed:
status = "❌ Failed"
status = fmt.Sprintf("%s (Error: %s)", status, txnInfo.txn.Details)
default:
status = "❓ Unknown"
}
txType := ""
switch txnInfo.txn.Type {
case sender.TxTypeRegular:
txType = "💸 ETH Transaction"
case sender.TxTypeDeposit:
txType = "🏦 Deposit"
case sender.TxTypeInstantBridge:
txType = "🌉 Instant Bridge"
default:
txType = "❓ Unknown"
}
text += fmt.Sprintf(
"- Txn: %s | Sender: %s | Attempts: %d | Duration: %s | Type: %s | Status: %s\n",
txnInfo.txn.Hash().Hex()[:8],
txnInfo.txn.Sender.Hex()[:8],
txnInfo.noOfAttempts,
txnInfo.timeTaken,
txType,
status,
fields := make([]Field, 0, len(txnsToNotify)*2)
for _, t := range txnsToNotify {
fields = append(fields,
Field{Title: "Txn", Value: fmt.Sprintf("`%s`", t.txn.Hash().Hex()), Short: false},
Field{Title: "Status", Value: buildStatus(t), Short: true},
Field{Title: "Sender", Value: fmt.Sprintf("`%s`", t.txn.Sender.Hex()[:10]), Short: true},
Field{Title: "Type", Value: buildType(t), Short: true},
Field{Title: "Attempts", Value: fmt.Sprintf("%d", t.noOfAttempts), Short: true},
Field{Title: "Duration", Value: t.timeTaken.String(), Short: true},
)
}
message := Message{
Text: "🚀 Transaction Report",
Attachments: []Attachment{
{
Color: "#FFA500",
Title: "The following transactions were completed in the last 15 mins",
Text: text,
Footer: "Preconf RPC Monitor",
TS: time.Now().Unix(),
},
},
Attachments: []Attachment{{
Color: "#2D9CDB",
Title: "Last 15 minutes",
Fields: fields,
MarkdownIn: []string{"text", "fields"},
TS: time.Now().Unix(),
}},
}
if err := n.SendMessage(ctx, message); err != nil {
n.logger.Error("Failed to send 15 minute transaction notification", "error", err)
Expand Down
89 changes: 89 additions & 0 deletions x/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"os"
"path/filepath"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

func PadKeyTo32Bytes(key *big.Int) []byte {
Expand Down Expand Up @@ -89,3 +92,89 @@ func ResolveFilePath(path string) (string, error) {

return path, nil
}

type Kind string

const (
KindETHSend Kind = "ETH Send"
KindSelfTransfer Kind = "Self ETH Transfer"
KindContractCreation Kind = "Contract Creation"
KindContractCall Kind = "Contract Call"
KindERC20Transfer Kind = "ERC20 Transfer"
KindERC20Approve Kind = "ERC20/721 Approval"
KindERC20Permit Kind = "ERC20 Permit (EIP-2612)"
KindERC721Transfer Kind = "ERC721 Transfer"
KindERC721ApproveAll Kind = "ERC721/1155 SetApprovalForAll"
KindERC1155Transfer Kind = "ERC1155 TransferSingle"
KindERC1155Batch Kind = "ERC1155 TransferBatch"
KindUnknown Kind = "Unknown"
)

type Classification struct {
Kind Kind
Details string // small hint (e.g. function name)
}

// Common selectors (first 4 bytes of calldata)
var (
selERC20Transfer = [4]byte{0xa9, 0x05, 0x9c, 0xbb} // transfer(address,uint256)
selERC20Approve = [4]byte{0x09, 0x5e, 0xa7, 0xb3} // approve(address,uint256) (same as ERC721 approve(address,uint256))
selERC20TransferFrom = [4]byte{0x23, 0xb8, 0x72, 0xdd} // transferFrom(address,address,uint256)
selPermit2612 = [4]byte{0xd5, 0x05, 0xac, 0xcf} // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
sel721SafeTx3 = [4]byte{0x42, 0x84, 0x2e, 0x0e} // safeTransferFrom(address,address,uint256)
sel721SafeTx4 = [4]byte{0xb8, 0x8d, 0x4f, 0xde} // safeTransferFrom(address,address,uint256,bytes)
selSetApprovalForAll = [4]byte{0xa2, 0x2c, 0xb4, 0x65} // setApprovalForAll(address,bool)
sel1155SafeTx = [4]byte{0xf2, 0x42, 0x43, 0x2a} // safeTransferFrom(address,address,uint256,uint256,bytes)
sel1155SafeBatchTx = [4]byte{0x2e, 0xb2, 0xc2, 0xd6} // safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)
)

// ClassifyTxOnly uses only tx envelope fields (no receipt/logs).
// `from` is required because go-ethereum’s Transaction doesn’t carry it.
func ClassifyTxOnly(tx *types.Transaction, from common.Address) Classification {
to := tx.To()
data := tx.Data()
val := tx.Value()

// Creation vs call vs plain ETH send
if to == nil {
return Classification{Kind: KindContractCreation, Details: "contract deploy"}
}
if len(data) == 0 {
if val != nil && val.Sign() > 0 {
if from == *to {
return Classification{Kind: KindSelfTransfer, Details: fmt.Sprintf("%s wei", val.String())}
}
return Classification{Kind: KindETHSend, Details: fmt.Sprintf("to %s, %s wei", to.Hex(), val.String())}
}
// empty data + zero value ⇒ likely “noop” call (rare) or account-base tx
return Classification{Kind: KindContractCall, Details: "empty calldata"}
}

// Function selector heuristics
if len(data) >= 4 {
var sel [4]byte
copy(sel[:], data[:4])
switch sel {
case selERC20Transfer:
return Classification{Kind: KindERC20Transfer, Details: "transfer(address,uint256)"}
case selERC20Approve:
return Classification{Kind: KindERC20Approve, Details: "approve(...)"} // could be ERC721 approve; indistinguishable without logs/ABI
case selERC20TransferFrom:
// Could be ERC20 transferFrom; ERC721 transferFrom has same selector (0x23b872dd).
return Classification{Kind: KindERC721Transfer, Details: "transferFrom(...)"} // label generically as transferFrom
case selPermit2612:
return Classification{Kind: KindERC20Permit, Details: "permit(...) (EIP-2612)"}
case sel721SafeTx3, sel721SafeTx4:
return Classification{Kind: KindERC721Transfer, Details: "safeTransferFrom"}
case selSetApprovalForAll:
return Classification{Kind: KindERC721ApproveAll, Details: "setApprovalForAll"}
case sel1155SafeTx:
return Classification{Kind: KindERC1155Transfer, Details: "ERC1155 safeTransferFrom"}
case sel1155SafeBatchTx:
return Classification{Kind: KindERC1155Batch, Details: "ERC1155 safeBatchTransferFrom"}
}
}

// Fallback
return Classification{Kind: KindContractCall, Details: "call with calldata"}
}
Loading