Skip to content

Commit a5c11e3

Browse files
committed
feat: implement directional normalization for transactions
- Introduced a new `DirectionalNormalizer` interface to handle transaction normalization based on direction for both Stellar and XRP indexers. - Added `normalizeDirectionalMetadata` function to process transaction metadata according to the specified direction. - Updated `StellarIndexer` and `XRPIndexer` to implement the normalization logic. - Enhanced tests to validate the correct handling of source-side routing for path payments in both Stellar and XRP transactions. - Added new test cases to ensure proper functionality of the directional normalization feature.
1 parent 34319a1 commit a5c11e3

11 files changed

Lines changed: 796 additions & 12 deletions

File tree

internal/indexer/directional.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package indexer
2+
3+
import (
4+
"github.com/fystack/multichain-indexer/pkg/common/constant"
5+
"github.com/fystack/multichain-indexer/pkg/common/types"
6+
)
7+
8+
const (
9+
metadataKeySourceAmount = "source_amount"
10+
metadataKeySourceAsset = "source_asset_address"
11+
metadataKeySourceTxType = "source_tx_type"
12+
)
13+
14+
func normalizeDirectionalMetadata(tx types.Transaction, direction string) types.Transaction {
15+
if direction != types.DirectionOut {
16+
clearDirectionalMetadata(&tx)
17+
return tx
18+
}
19+
20+
sourceType := tx.GetMetadataString(metadataKeySourceTxType)
21+
if sourceType != "" {
22+
tx.Type = constant.TxType(sourceType)
23+
if tx.Type == constant.TxTypeNativeTransfer {
24+
tx.AssetAddress = ""
25+
}
26+
}
27+
28+
if sourceAmount := tx.GetMetadataString(metadataKeySourceAmount); sourceAmount != "" {
29+
tx.Amount = sourceAmount
30+
}
31+
32+
switch tx.GetMetadataString(metadataKeySourceTxType) {
33+
case string(constant.TxTypeNativeTransfer):
34+
tx.AssetAddress = ""
35+
case string(constant.TxTypeTokenTransfer):
36+
tx.AssetAddress = tx.GetMetadataString(metadataKeySourceAsset)
37+
}
38+
39+
clearDirectionalMetadata(&tx)
40+
return tx
41+
}
42+
43+
func clearDirectionalMetadata(tx *types.Transaction) {
44+
if tx == nil || tx.Metadata == nil {
45+
return
46+
}
47+
48+
delete(tx.Metadata, metadataKeySourceAmount)
49+
delete(tx.Metadata, metadataKeySourceAsset)
50+
delete(tx.Metadata, metadataKeySourceTxType)
51+
if len(tx.Metadata) == 0 {
52+
tx.Metadata = nil
53+
}
54+
}

internal/indexer/indexer.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@ type Indexer interface {
1919
GetBlocksByNumbers(ctx context.Context, blockNumbers []uint64) ([]BlockResult, error)
2020
IsHealthy() bool
2121
}
22+
23+
// DirectionalNormalizer is an optional hook for chains whose outgoing wallet
24+
// view differs from the incoming payload of the same routed transfer
25+
// (for example, path/cross-asset payments). It must not change from/to-based
26+
// routing decisions; it only shapes the emitted transaction payload.
27+
type DirectionalNormalizer interface {
28+
NormalizeForDirection(tx types.Transaction, direction string) types.Transaction
29+
}

internal/indexer/stellar.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ func NewStellarIndexer(
5353
func (s *StellarIndexer) GetName() string { return strings.ToUpper(s.chainName) }
5454
func (s *StellarIndexer) GetNetworkType() enum.NetworkType { return enum.NetworkTypeStellar }
5555
func (s *StellarIndexer) GetNetworkInternalCode() string { return s.config.InternalCode }
56+
func (s *StellarIndexer) NormalizeForDirection(tx types.Transaction, direction string) types.Transaction {
57+
return normalizeDirectionalMetadata(tx, direction)
58+
}
5659

5760
func (s *StellarIndexer) isMonitoredTransfer(from, to string) bool {
5861
if s.pubkeyStore == nil {
@@ -490,6 +493,13 @@ func (s *StellarIndexer) convertPayment(
490493
if memoType != "" {
491494
tx.SetMetadata(types.MetadataKeyMemoType, memoType)
492495
}
496+
if sourceType, sourceAssetAddress, sourceAmount, ok := stellarSourcePaymentDetails(payment); ok {
497+
tx.SetMetadata(metadataKeySourceTxType, string(sourceType))
498+
tx.SetMetadata(metadataKeySourceAmount, sourceAmount)
499+
if sourceType == constant.TxTypeTokenTransfer {
500+
tx.SetMetadata(metadataKeySourceAsset, sourceAssetAddress)
501+
}
502+
}
493503

494504
return tx, true
495505
}
@@ -1021,6 +1031,28 @@ func isNativeStellarPayment(payment stellar.Payment) bool {
10211031
return strings.EqualFold(strings.TrimSpace(payment.AssetType), "native")
10221032
}
10231033

1034+
func stellarSourcePaymentDetails(payment stellar.Payment) (constant.TxType, string, string, bool) {
1035+
switch strings.ToLower(strings.TrimSpace(payment.Type)) {
1036+
case "path_payment_strict_receive", "path_payment_strict_recieve", "path_payment_strict_send":
1037+
default:
1038+
return "", "", "", false
1039+
}
1040+
1041+
amount := strings.TrimSpace(payment.SourceAmount)
1042+
if amount == "" {
1043+
return "", "", "", false
1044+
}
1045+
if strings.EqualFold(strings.TrimSpace(payment.SourceAssetType), "native") {
1046+
return constant.TxTypeNativeTransfer, "", amount, true
1047+
}
1048+
1049+
assetAddress := formatStellarAsset(payment.SourceAssetIssuer, payment.SourceAssetCode)
1050+
if assetAddress == "" {
1051+
return "", "", "", false
1052+
}
1053+
return constant.TxTypeTokenTransfer, assetAddress, amount, true
1054+
}
1055+
10241056
func stellarTransferFields(payment stellar.Payment) (string, string, string) {
10251057
switch strings.ToLower(strings.TrimSpace(payment.Type)) {
10261058
case "create_account":

internal/indexer/stellar_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,54 @@ func TestStellarGetBlock_ParsesNativeAndIssuedPaymentsWithMemo(t *testing.T) {
405405
assert.Equal(t, "0.00003", pathTx.TxFee.String())
406406
}
407407

408+
func TestStellarConvertPayment_PathPaymentStoresSourceSideRouting(t *testing.T) {
409+
t.Parallel()
410+
411+
idx := NewStellarIndexer(
412+
"stellar_mainnet",
413+
config.ChainConfig{NetworkId: "stellar_mainnet"},
414+
nil,
415+
nil,
416+
mockStellarPubkeyStore{addresses: map[string]struct{}{"GDEST": {}}},
417+
)
418+
419+
tx, ok := idx.convertPayment(
420+
stellar.Payment{
421+
Type: "path_payment_strict_send",
422+
TransactionHash: "tx-path",
423+
TransactionSuccessful: true,
424+
From: "GSOURCE",
425+
To: "GDEST",
426+
Amount: "5.0000000",
427+
AssetType: "credit_alphanum4",
428+
AssetCode: "USDC",
429+
AssetIssuer: "GISSUER",
430+
SourceAmount: "10.0000000",
431+
SourceAssetType: "native",
432+
},
433+
nil,
434+
21,
435+
"ledger-hash",
436+
"payment-1",
437+
123,
438+
)
439+
require.True(t, ok)
440+
assert.Equal(t, constant.TxTypeTokenTransfer, tx.Type)
441+
assert.Equal(t, "GISSUER:USDC", tx.AssetAddress)
442+
assert.Equal(t, "5.0000000", tx.Amount)
443+
assert.Equal(t, string(constant.TxTypeNativeTransfer), tx.GetMetadataString(metadataKeySourceTxType))
444+
assert.Equal(t, "10.0000000", tx.GetMetadataString(metadataKeySourceAmount))
445+
assert.Equal(t, "", tx.GetMetadataString(metadataKeySourceAsset))
446+
447+
outTx := idx.NormalizeForDirection(tx, types.DirectionOut)
448+
assert.Equal(t, constant.TxTypeNativeTransfer, outTx.Type)
449+
assert.Equal(t, "", outTx.AssetAddress)
450+
assert.Equal(t, "10.0000000", outTx.Amount)
451+
assert.Equal(t, "", outTx.GetMetadataString(metadataKeySourceTxType))
452+
assert.Equal(t, "", outTx.GetMetadataString(metadataKeySourceAmount))
453+
assert.Equal(t, "", outTx.GetMetadataString(metadataKeySourceAsset))
454+
}
455+
408456
func TestStellarGetBlock_FetchesTransactionDetailsLazily(t *testing.T) {
409457
t.Parallel()
410458

@@ -1019,6 +1067,19 @@ func TestStellarMainnetFetchAndParseTransactions(t *testing.T) {
10191067
}
10201068

10211069
testCases := []stellarRealTxCase{
1070+
{
1071+
name: "native payment",
1072+
kind: "payment",
1073+
txHash: "81dba977244285fd3a6e9192ff2a594fe99d9bd6ec892533fe80c5f25c77c1f0",
1074+
transferIndex: "265365546521460737",
1075+
wantType: constant.TxTypeNativeTransfer,
1076+
wantFrom: "GBC6NRTTQLRCABQHIR5J4R4YDJWFWRAO4ZRQIM2SVI5GSIZ2HZ42RINW",
1077+
wantTo: "GCZNOTQRRETQLBQH2MPWYMCLQBYMXKZI7XXYHS7F5RJHH7VMATQ57TQZ",
1078+
wantAmount: "120.0621870",
1079+
verify: func(t *testing.T, tx types.Transaction, _ *StellarIndexer, _ *stellar.Payment, _ *stellar.Operation, _ []stellar.Effect) {
1080+
assert.Equal(t, "", tx.AssetAddress)
1081+
},
1082+
},
10221083
{
10231084
name: "create account",
10241085
kind: "payment",

0 commit comments

Comments
 (0)