diff --git a/tools/preconf-rpc/notifier/notifier.go b/tools/preconf-rpc/notifier/notifier.go index 7c58f7b93..66ce1a6a6 100644 --- a/tools/preconf-rpc/notifier/notifier.go +++ b/tools/preconf-rpc/notifier/notifier.go @@ -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 @@ -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{} { @@ -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) diff --git a/x/util/util.go b/x/util/util.go index 725a10d38..a3a6f0ce3 100644 --- a/x/util/util.go +++ b/x/util/util.go @@ -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 { @@ -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"} +}