From 12f5c8890a9ae335f817e246e5b61f868d291849 Mon Sep 17 00:00:00 2001 From: Onah Prosper Date: Thu, 19 Feb 2026 15:01:36 +0000 Subject: [PATCH] feat: add unit tests for UpdateReceiveAddressStatus function - Introduced a new test file `indexer_test.go` to validate the behavior of the `UpdateReceiveAddressStatus` function. - Implemented tests to ensure correct handling of various token transfer scenarios, including dust transfers, valid transfers, and ignored transfers based on address and duplicate transaction hashes. - Enhanced the `UpdateReceiveAddressStatus` function to skip dust transfers, preventing address poisoning attacks. - Updated the `ProcessPaymentOrderFromBlockchain` function to set the order percent to 100 when processing payment orders. --- services/common/indexer.go | 5 + services/common/indexer_test.go | 232 ++++++++++++++++++++++++++++++++ services/common/order.go | 1 + 3 files changed, 238 insertions(+) create mode 100644 services/common/indexer_test.go diff --git a/services/common/indexer.go b/services/common/indexer.go index dbeafacc..8a6d4f12 100644 --- a/services/common/indexer.go +++ b/services/common/indexer.go @@ -243,6 +243,11 @@ func UpdateReceiveAddressStatus( return false, nil } + // Skip dust transfers to prevent address poisoning attacks + if event.Value.LessThanOrEqual(decimal.NewFromFloat(0.1)) { + return false, nil + } + // This is a transfer to the receive address to create an order on-chain // Compare the transferred value with the expected order amount + fees fees := paymentOrder.NetworkFee.Add(paymentOrder.SenderFee) diff --git a/services/common/indexer_test.go b/services/common/indexer_test.go new file mode 100644 index 00000000..8c1ea829 --- /dev/null +++ b/services/common/indexer_test.go @@ -0,0 +1,232 @@ +package common + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" + "github.com/paycrest/aggregator/ent" + "github.com/paycrest/aggregator/ent/enttest" + "github.com/paycrest/aggregator/ent/paymentorder" + db "github.com/paycrest/aggregator/storage" + "github.com/paycrest/aggregator/types" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" +) + +func setupTestDB(t *testing.T) (*ent.Client, func()) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + if err := client.Schema.Create(context.Background()); err != nil { + t.Fatalf("failed to create schema: %v", err) + } + db.Client = client + return client, func() { _ = client.Close() } +} + +func createTestOrder(t *testing.T, client *ent.Client, amount float64, receiveAddress string) *ent.PaymentOrder { + ctx := context.Background() + + network, err := client.Network.Create(). + SetIdentifier("polygon"). + SetChainID(137). + SetRPCEndpoint("https://polygon-rpc.com"). + SetGatewayContractAddress("0xGateway"). + SetIsTestnet(false). + SetBlockTime(decimal.NewFromFloat(2.0)). + SetFee(decimal.NewFromFloat(0.1)). + Save(ctx) + assert.NoError(t, err) + + token, err := client.Token.Create(). + SetSymbol("USDT"). + SetContractAddress("0xTokenContract"). + SetDecimals(6). + SetBaseCurrency("USD"). + SetIsEnabled(true). + SetNetwork(network). + Save(ctx) + assert.NoError(t, err) + + order, err := client.PaymentOrder.Create(). + SetAmount(decimal.NewFromFloat(amount)). + SetAmountInUsd(decimal.NewFromFloat(amount)). + SetAmountPaid(decimal.Zero). + SetAmountReturned(decimal.Zero). + SetPercentSettled(decimal.Zero). + SetNetworkFee(network.Fee). + SetSenderFee(decimal.Zero). + SetRate(decimal.NewFromFloat(1.0)). + SetToken(token). + SetReceiveAddress(receiveAddress). + SetReceiveAddressSalt([]byte("salt")). + SetReceiveAddressExpiry(time.Now().Add(time.Hour)). + SetReturnAddress("0xReturnAddr"). + SetInstitution("ABNGNGLA"). + SetAccountIdentifier("1234567890"). + SetAccountName("Test Account"). + SetStatus(paymentorder.StatusInitiated). + Save(ctx) + assert.NoError(t, err) + + order, err = client.PaymentOrder.Query(). + Where(paymentorder.IDEQ(order.ID)). + WithToken(func(tq *ent.TokenQuery) { tq.WithNetwork() }). + Only(ctx) + assert.NoError(t, err) + + return order +} + +func noopCreateOrder(_ context.Context, _ uuid.UUID) error { return nil } +func noopGetProviderRate(_ context.Context, _ *ent.ProviderProfile, _ string, _ string) (decimal.Decimal, error) { + return decimal.Zero, nil +} + +func TestUpdateReceiveAddressStatus_DustTransferIgnored(t *testing.T) { + _, cleanup := setupTestDB(t) + defer cleanup() + + order := createTestOrder(t, db.Client, 5.0, "0xReceiveAddr") + + dustEvent := &types.TokenTransferEvent{ + BlockNumber: 100, + TxHash: "0xDustTxHash", + From: "0xAttacker", + To: "0xReceiveAddr", + Value: decimal.NewFromFloat(0.005), + } + + done, err := UpdateReceiveAddressStatus( + context.Background(), order, dustEvent, noopCreateOrder, noopGetProviderRate, + ) + assert.NoError(t, err) + assert.False(t, done) + + // Order must still be initiated with original amount + updated, err := db.Client.PaymentOrder.Get(context.Background(), order.ID) + assert.NoError(t, err) + assert.Equal(t, paymentorder.StatusInitiated, updated.Status) + assert.True(t, updated.Amount.Equal(decimal.NewFromFloat(5.0)), "order amount should not be mutated by dust") + assert.True(t, updated.AmountPaid.Equal(decimal.Zero), "amount paid should remain zero") +} + +func TestUpdateReceiveAddressStatus_BoundaryDustIgnored(t *testing.T) { + _, cleanup := setupTestDB(t) + defer cleanup() + + order := createTestOrder(t, db.Client, 5.0, "0xReceiveAddr2") + + boundaryEvent := &types.TokenTransferEvent{ + BlockNumber: 101, + TxHash: "0xBoundaryTxHash", + From: "0xAttacker", + To: "0xReceiveAddr2", + Value: decimal.NewFromFloat(0.1), + } + + done, err := UpdateReceiveAddressStatus( + context.Background(), order, boundaryEvent, noopCreateOrder, noopGetProviderRate, + ) + assert.NoError(t, err) + assert.False(t, done) + + updated, err := db.Client.PaymentOrder.Get(context.Background(), order.ID) + assert.NoError(t, err) + assert.Equal(t, paymentorder.StatusInitiated, updated.Status) +} + +func TestUpdateReceiveAddressStatus_ValidTransferProcessed(t *testing.T) { + _, cleanup := setupTestDB(t) + defer cleanup() + + order := createTestOrder(t, db.Client, 5.0, "0xReceiveAddr3") + + // order amount (5.0) + network fee (0.1) + sender fee (0) = 5.1 + validEvent := &types.TokenTransferEvent{ + BlockNumber: 102, + TxHash: "0xValidTxHash", + From: "0xSender", + To: "0xReceiveAddr3", + Value: decimal.NewFromFloat(5.1), + } + + done, _ := UpdateReceiveAddressStatus( + context.Background(), order, validEvent, noopCreateOrder, noopGetProviderRate, + ) + // done==true confirms the transfer passed the dust check and was processed + assert.True(t, done) + + updated, err := db.Client.PaymentOrder.Get(context.Background(), order.ID) + assert.NoError(t, err) + // Order should have moved past initiated (deposited or pending depending on tx driver) + assert.NotEqual(t, paymentorder.StatusInitiated, updated.Status, "order should no longer be initiated") + assert.True(t, updated.AmountPaid.GreaterThan(decimal.Zero), "amount paid should be updated") +} + +func TestUpdateReceiveAddressStatus_WrongAddressIgnored(t *testing.T) { + _, cleanup := setupTestDB(t) + defer cleanup() + + order := createTestOrder(t, db.Client, 5.0, "0xReceiveAddr4") + + wrongAddrEvent := &types.TokenTransferEvent{ + BlockNumber: 103, + TxHash: "0xWrongAddrTxHash", + From: "0xSender", + To: "0xSomeOtherAddress", + Value: decimal.NewFromFloat(5.1), + } + + done, err := UpdateReceiveAddressStatus( + context.Background(), order, wrongAddrEvent, noopCreateOrder, noopGetProviderRate, + ) + assert.NoError(t, err) + assert.False(t, done) + + updated, err := db.Client.PaymentOrder.Get(context.Background(), order.ID) + assert.NoError(t, err) + assert.Equal(t, paymentorder.StatusInitiated, updated.Status) +} + +func TestUpdateReceiveAddressStatus_DuplicateTxHashIgnored(t *testing.T) { + _, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + order := createTestOrder(t, db.Client, 5.0, "0xReceiveAddr5") + + // Pre-set a txHash on another order to simulate a duplicate + _, err := db.Client.PaymentOrder.Create(). + SetAmount(decimal.NewFromFloat(1.0)). + SetAmountInUsd(decimal.NewFromFloat(1.0)). + SetAmountPaid(decimal.Zero). + SetAmountReturned(decimal.Zero). + SetPercentSettled(decimal.Zero). + SetNetworkFee(decimal.Zero). + SetSenderFee(decimal.Zero). + SetRate(decimal.NewFromFloat(1.0)). + SetToken(order.Edges.Token). + SetInstitution("ABNGNGLA"). + SetAccountIdentifier("9999999999"). + SetAccountName("Dupe Account"). + SetStatus(paymentorder.StatusDeposited). + SetTxHash("0xAlreadyIndexed"). + Save(ctx) + assert.NoError(t, err) + + dupeEvent := &types.TokenTransferEvent{ + BlockNumber: 104, + TxHash: "0xAlreadyIndexed", + From: "0xSender", + To: "0xReceiveAddr5", + Value: decimal.NewFromFloat(5.1), + } + + done, err := UpdateReceiveAddressStatus( + context.Background(), order, dupeEvent, noopCreateOrder, noopGetProviderRate, + ) + assert.NoError(t, err) + assert.False(t, done) +} diff --git a/services/common/order.go b/services/common/order.go index e710ca56..2e8c528e 100644 --- a/services/common/order.go +++ b/services/common/order.go @@ -99,6 +99,7 @@ func ProcessPaymentOrderFromBlockchain( Update(). Where(paymentorder.IDEQ(existingOrderWithMessageHash.ID)). SetGatewayID(event.OrderId). + SetOrderPercent(decimal.NewFromInt(100)). SetTxHash(event.TxHash). SetBlockNumber(int64(event.BlockNumber)). SetStatus(paymentorder.StatusPending).