Skip to content

Commit 6bae85d

Browse files
committed
Implement missing logic and enhance NFT detection
1 parent d83e067 commit 6bae85d

File tree

7 files changed

+223
-19
lines changed

7 files changed

+223
-19
lines changed

integration_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"testing"
7+
"time"
8+
9+
"github.com/Jetlum/WalletAlertService/mock"
10+
"github.com/Jetlum/WalletAlertService/models"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestIntegration(t *testing.T) {
16+
if testing.Short() {
17+
t.Skip("Skipping integration test in short mode")
18+
}
19+
20+
// Setup test context with timeout
21+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
22+
defer cancel()
23+
24+
// Create test event
25+
event := &models.Event{
26+
TxHash: "0xtest",
27+
FromAddress: "0xfrom",
28+
ToAddress: "0xto",
29+
Value: "1000000000000000000", // 1 ETH
30+
EventType: "LARGE_TRANSFER",
31+
}
32+
33+
// Setup mocks
34+
mockEventRepo := &mock.MockEventRepository{
35+
CreateFunc: func(e *models.Event) error {
36+
assert.Equal(t, event.TxHash, e.TxHash)
37+
return nil
38+
},
39+
}
40+
41+
userPref := models.UserPreference{
42+
UserID: "test@example.com",
43+
WalletAddress: "0xto",
44+
MinEtherValue: "500000000000000000", // 0.5 ETH
45+
EmailNotification: true,
46+
}
47+
48+
mockUserPrefRepo := &mock.MockUserPreferenceRepository{
49+
GetMatchingPreferencesFunc: func(e *models.Event) ([]models.UserPreference, error) {
50+
assert.Equal(t, event.ToAddress, e.ToAddress)
51+
return []models.UserPreference{userPref}, nil
52+
},
53+
}
54+
55+
// Setup mock email notification
56+
emailSent := false
57+
mockEmailNotification := &mock.MockEmailNotification{
58+
SendFunc: func(e *models.Event, up *models.UserPreference) error {
59+
emailSent = true
60+
return nil
61+
},
62+
}
63+
64+
// Create mock block and header
65+
mockHeader := &types.Header{
66+
Number: big.NewInt(1),
67+
}
68+
69+
// Test full workflow
70+
done := make(chan bool)
71+
go func() {
72+
processBlock(
73+
nil,
74+
mockHeader,
75+
mock.NewMockNFTDetector(),
76+
mockEmailNotification,
77+
mockEventRepo,
78+
mockUserPrefRepo,
79+
)
80+
done <- true
81+
}()
82+
83+
// Wait for completion or timeout
84+
select {
85+
case <-ctx.Done():
86+
t.Fatal("Test timed out")
87+
case <-done:
88+
assert.True(t, emailSent, "Email notification should have been sent")
89+
}
90+
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func main() {
8989
func processBlock(
9090
client *ethclient.Client,
9191
header *types.Header,
92-
nftDetector *nfts.NFTDetector,
92+
nftDetector nfts.INFTDetector,
9393
emailNotification services.EmailNotifier,
9494
eventRepo repository.EventRepositoryInterface,
9595
userPrefRepo repository.UserPreferenceRepositoryInterface,

mock/nft_mock.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package mock
2+
3+
import (
4+
nfts "github.com/Jetlum/WalletAlertService/nft"
5+
"github.com/ethereum/go-ethereum/core/types"
6+
)
7+
8+
type MockNFTDetector struct {
9+
IsNFTTransactionFunc func(tx *types.Transaction) bool
10+
}
11+
12+
func NewMockNFTDetector() nfts.INFTDetector {
13+
return &MockNFTDetector{
14+
IsNFTTransactionFunc: func(tx *types.Transaction) bool {
15+
return false
16+
},
17+
}
18+
}
19+
20+
func (m *MockNFTDetector) IsNFTTransaction(tx *types.Transaction) bool {
21+
if m.IsNFTTransactionFunc != nil {
22+
return m.IsNFTTransactionFunc(tx)
23+
}
24+
return false
25+
}

models/validation.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package models
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
)
9+
10+
func (up *UserPreference) Validate() error {
11+
if up.UserID == "" {
12+
return fmt.Errorf("user ID is required")
13+
}
14+
15+
if up.WalletAddress == "" {
16+
return fmt.Errorf("wallet address is required")
17+
}
18+
19+
if !common.IsHexAddress(up.WalletAddress) {
20+
return fmt.Errorf("invalid wallet address format")
21+
}
22+
23+
if up.MinEtherValue != "" {
24+
value, ok := new(big.Int).SetString(up.MinEtherValue, 10)
25+
if !ok {
26+
return fmt.Errorf("invalid minimum ether value")
27+
}
28+
if value.Cmp(big.NewInt(0)) < 0 {
29+
return fmt.Errorf("minimum ether value cannot be negative")
30+
}
31+
}
32+
33+
return nil
34+
}

nft/nftdetector.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,46 @@
11
package nfts
22

33
import (
4+
"sync"
5+
46
"github.com/ethereum/go-ethereum/common"
57
"github.com/ethereum/go-ethereum/core/types"
68
)
79

10+
// INFTDetector defines the interface for NFT detection
11+
type INFTDetector interface {
12+
IsNFTTransaction(tx *types.Transaction) bool
13+
}
14+
15+
// NFTDetector implements the INFTDetector interface
816
type NFTDetector struct {
9-
nftContracts map[common.Address]bool
17+
nftContracts sync.Map
1018
}
1119

12-
func NewNFTDetector() *NFTDetector {
13-
return &NFTDetector{
14-
nftContracts: map[common.Address]bool{
15-
common.HexToAddress("0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"): true, // BAYC
16-
common.HexToAddress("0x23581767a106ae21c074b2276D25e5C3e136a68b"): true, // Moonbirds
17-
},
20+
func NewNFTDetector() INFTDetector {
21+
detector := &NFTDetector{}
22+
23+
// Initialize known contracts
24+
knownContracts := map[string]bool{
25+
"0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D": true, // BAYC
26+
"0x23581767a106ae21c074b2276D25e5C3e136a68b": true, // Moonbirds
27+
}
28+
29+
for addr := range knownContracts {
30+
detector.nftContracts.Store(common.HexToAddress(addr), true)
1831
}
32+
33+
return detector
1934
}
2035

2136
func (d *NFTDetector) IsNFTTransaction(tx *types.Transaction) bool {
2237
if tx.To() == nil {
2338
return false
2439
}
25-
_, exists := d.nftContracts[*tx.To()]
26-
return exists
40+
41+
if isContract, ok := d.nftContracts.Load(*tx.To()); ok {
42+
return isContract.(bool)
43+
}
44+
45+
return false
2746
}

repository/user_preference.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package repository
22

33
import (
4+
"fmt"
5+
"math/big"
6+
47
"github.com/Jetlum/WalletAlertService/models"
58
"gorm.io/gorm"
69
)
@@ -15,6 +18,26 @@ func NewUserPreferenceRepository(db *gorm.DB) *UserPreferenceRepository {
1518

1619
func (r *UserPreferenceRepository) GetMatchingPreferences(event *models.Event) ([]models.UserPreference, error) {
1720
var preferences []models.UserPreference
18-
err := r.db.Where("wallet_address = ?", event.ToAddress).Find(&preferences).Error
19-
return preferences, err
21+
22+
eventValue, ok := new(big.Int).SetString(event.Value, 10)
23+
if !ok {
24+
return nil, fmt.Errorf("invalid event value: %s", event.Value)
25+
}
26+
27+
// Base query
28+
query := r.db.Where("wallet_address = ?", event.ToAddress)
29+
30+
// Add filters based on event type
31+
switch event.EventType {
32+
case "LARGE_TRANSFER":
33+
query = query.Where("min_ether_value <= ?", eventValue.String())
34+
case "NFT_TRANSFER":
35+
query = query.Where("track_nfts = ?", true)
36+
}
37+
38+
if err := query.Find(&preferences).Error; err != nil {
39+
return nil, fmt.Errorf("failed to fetch preferences: %w", err)
40+
}
41+
42+
return preferences, nil
2043
}

services/email_notifier.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package services
22

33
import (
4+
"fmt"
5+
46
"github.com/Jetlum/WalletAlertService/models"
57
"github.com/sendgrid/sendgrid-go"
68
"github.com/sendgrid/sendgrid-go/helpers/mail"
@@ -12,18 +14,29 @@ type EmailNotifier interface {
1214

1315
type EmailNotification struct {
1416
APIKey string
17+
client *sendgrid.Client
1518
}
1619

17-
var _ EmailNotifier = (*EmailNotification)(nil)
18-
1920
func (en *EmailNotification) Send(event *models.Event, userPref *models.UserPreference) error {
20-
from := mail.NewEmail("Wallet Alert Service", "no-reply@example.com")
21+
if userPref.UserID == "" {
22+
return fmt.Errorf("invalid user email")
23+
}
24+
25+
from := mail.NewEmail("Wallet Alert Service", "alerts@walletalert.service")
2126
to := mail.NewEmail("", userPref.UserID)
22-
subject := "Wallet Activity Alert"
27+
subject := fmt.Sprintf("Alert: %s Event Detected", event.EventType)
2328
content := formatEventMessage(event)
29+
2430
message := mail.NewSingleEmail(from, subject, to, content, content)
2531

26-
client := sendgrid.NewSendClient(en.APIKey)
27-
_, err := client.Send(message)
28-
return err
32+
response, err := en.client.Send(message)
33+
if err != nil {
34+
return fmt.Errorf("failed to send email: %w", err)
35+
}
36+
37+
if response.StatusCode >= 400 {
38+
return fmt.Errorf("email API error: status %d", response.StatusCode)
39+
}
40+
41+
return nil
2942
}

0 commit comments

Comments
 (0)