feat: Calculate finalize status and filter by it (#3969)
This commit is contained in:
parent
d3c4ba315a
commit
d29c6c5b6f
|
@ -324,6 +324,8 @@ type FilterDependencies struct {
|
|||
tokenSymbol func(token Token) string
|
||||
// use the chainID and symbol to look up token.TokenType and token.Address. Return nil if not found
|
||||
tokenFromSymbol func(chainID *common.ChainID, symbol string) *Token
|
||||
// use to get current timestamp
|
||||
currentTimestamp func() int64
|
||||
}
|
||||
|
||||
// getActivityEntries queries the transfers, pending_transactions, and multi_transactions tables based on filter parameters and arguments
|
||||
|
@ -379,6 +381,11 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
|
|||
networks = joinItems(chainIDs, nil)
|
||||
}
|
||||
|
||||
layer2Chains := []uint64{common.OptimismMainnet, common.OptimismGoerli, common.ArbitrumMainnet, common.ArbitrumGoerli}
|
||||
layer2Networks := joinItems(layer2Chains, func(chainID uint64) string {
|
||||
return fmt.Sprintf("%d", chainID)
|
||||
})
|
||||
|
||||
startFilterDisabled := !(filter.Period.StartTimestamp > 0)
|
||||
endFilterDisabled := !(filter.Period.EndTimestamp > 0)
|
||||
filterActivityTypeAll := len(filter.Types) == 0
|
||||
|
@ -408,7 +415,7 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
|
|||
})
|
||||
|
||||
queryString := fmt.Sprintf(queryFormatString, involvedAddresses, toAddresses, assetsTokenCodes, assetsERC20, networks,
|
||||
joinedMTTypes)
|
||||
layer2Networks, joinedMTTypes)
|
||||
|
||||
// The duplicated temporary table UNION with CTE acts as an optimization
|
||||
// As soon as we use filter_addresses CTE or filter_addresses_table temp table
|
||||
|
@ -426,10 +433,13 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
|
|||
fromTrType, toTrType,
|
||||
allAddresses, filterAllToAddresses,
|
||||
includeAllStatuses, filterStatusCompleted, filterStatusFailed, filterStatusFinalized, filterStatusPending,
|
||||
FailedAS, CompleteAS, PendingAS,
|
||||
FailedAS, CompleteAS, FinalizedAS, PendingAS,
|
||||
includeAllTokenTypeAssets,
|
||||
includeAllNetworks,
|
||||
transactions.Pending,
|
||||
deps.currentTimestamp(),
|
||||
648000, // 7.5 days in seconds for layer 2 finalization. 0.5 day is buffer to not create false positive.
|
||||
960, // A block on layer 1 is every 12s, finalization require 64 blocks. A buffer of 16 blocks is added to not create false positives.
|
||||
limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"database/sql"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/testutils"
|
||||
|
@ -18,6 +19,8 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var mockupTime = time.Unix(946724400, 0) // 2000-01-01 12:00:00
|
||||
|
||||
func tokenFromSymbol(chainID *common.ChainID, symbol string) *Token {
|
||||
for i, t := range transfer.TestTokens {
|
||||
if (chainID == nil || t.ChainID == uint64(*chainID)) && t.Symbol == symbol {
|
||||
|
@ -70,6 +73,9 @@ func setupTestActivityDBStorageChoice(tb testing.TB, inMemory bool) (deps Filter
|
|||
},
|
||||
// tokenFromSymbol nil chainID accepts first symbol found
|
||||
tokenFromSymbol: tokenFromSymbol,
|
||||
currentTimestamp: func() int64 {
|
||||
return mockupTime.Unix()
|
||||
},
|
||||
}
|
||||
|
||||
return deps, func() {
|
||||
|
@ -197,7 +203,7 @@ func TestGetActivityEntriesAll(t *testing.T) {
|
|||
id: td.tr1.MultiTransactionID,
|
||||
timestamp: td.tr1.Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(td.tr1.Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -235,7 +241,7 @@ func TestGetActivityEntriesAll(t *testing.T) {
|
|||
id: td.multiTx1ID,
|
||||
timestamp: td.multiTx1.Timestamp,
|
||||
activityType: SendAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1.FromAmount)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(td.multiTx1.ToAmount)),
|
||||
tokenOut: tokenFromSymbol(nil, td.multiTx1.FromToken),
|
||||
|
@ -327,7 +333,7 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[5].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[5].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -346,7 +352,7 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
|
|||
id: td.multiTx1ID,
|
||||
timestamp: td.multiTx1.Timestamp,
|
||||
activityType: SendAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1.FromAmount)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(td.multiTx1.ToAmount)),
|
||||
tokenOut: tokenFromSymbol(nil, td.multiTx1.FromToken),
|
||||
|
@ -373,7 +379,7 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[2].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[2].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -392,7 +398,7 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
|
|||
id: td.multiTx1ID,
|
||||
timestamp: td.multiTx1.Timestamp,
|
||||
activityType: SendAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1.FromAmount)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(td.multiTx1.ToAmount)),
|
||||
tokenOut: tokenFromSymbol(nil, td.multiTx1.FromToken),
|
||||
|
@ -418,7 +424,7 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[2].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[2].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -437,7 +443,7 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: td.tr1.Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(td.tr1.Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -483,7 +489,7 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[8].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[8].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -502,7 +508,7 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[6].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[6].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -527,7 +533,7 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[6].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[6].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -546,7 +552,7 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[4].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[4].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -571,7 +577,7 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[2].Timestamp,
|
||||
activityType: ReceiveAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(0)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(trs[2].Value)),
|
||||
tokenOut: nil,
|
||||
|
@ -749,7 +755,7 @@ func TestGetActivityEntriesFilterByAddresses(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[4].Timestamp,
|
||||
activityType: SendAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(trs[4].Value)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(0)),
|
||||
tokenOut: TTrToToken(t, &trs[4].TestTransaction),
|
||||
|
@ -768,7 +774,7 @@ func TestGetActivityEntriesFilterByAddresses(t *testing.T) {
|
|||
id: 0,
|
||||
timestamp: trs[1].Timestamp,
|
||||
activityType: SendAT,
|
||||
activityStatus: CompleteAS,
|
||||
activityStatus: FinalizedAS,
|
||||
amountOut: (*hexutil.Big)(big.NewInt(trs[1].Value)),
|
||||
amountIn: (*hexutil.Big)(big.NewInt(0)),
|
||||
tokenOut: TTrToToken(t, &trs[1].TestTransaction),
|
||||
|
@ -805,9 +811,9 @@ func TestGetActivityEntriesFilterByStatus(t *testing.T) {
|
|||
deps, close := setupTestActivityDB(t)
|
||||
defer close()
|
||||
|
||||
// Adds 4 extractable transactions: 1 T, 1 T pending, 1 MT pending, 1 MT with 2xT success
|
||||
// Adds 4 extractable transactions: 1 T, 1 T pending, 1 MT pending, 1 MT with 2xT finalized
|
||||
td, fromTds, toTds := fillTestData(t, deps.db)
|
||||
// Add 7 extractable transactions: 1 pending, 1 Tr failed, 1 MT failed, 4 success
|
||||
// Add 7 extractable transactions: 1 pending, 1 Tr failed, 1 MT failed, 4 finalized
|
||||
trs, fromTrs, toTrs := transfer.GenerateTestTransfers(t, deps.db, td.nextIndex, 7)
|
||||
multiTx := transfer.GenerateTestSendMultiTransaction(trs[6])
|
||||
failedMTID := transfer.InsertTestMultiTransaction(t, deps.db, &multiTx)
|
||||
|
@ -817,6 +823,10 @@ func TestGetActivityEntriesFilterByStatus(t *testing.T) {
|
|||
transfer.InsertTestPendingTransaction(t, deps.db, &trs[i])
|
||||
} else {
|
||||
trs[i].Success = i != 3 && i != 6
|
||||
if trs[i].Success && (i == 2 || i == 5) {
|
||||
// Finalize status depends on timestamp
|
||||
trs[i].Timestamp = mockupTime.Unix() - 10
|
||||
}
|
||||
transfer.InsertTestTransfer(t, deps.db, trs[i].To, &trs[i])
|
||||
}
|
||||
}
|
||||
|
@ -845,13 +855,12 @@ func TestGetActivityEntriesFilterByStatus(t *testing.T) {
|
|||
filter.Statuses = []Status{CompleteAS}
|
||||
entries, err = getActivityEntries(context.Background(), deps, allAddresses, true, []common.ChainID{}, filter, 0, 15)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, len(entries))
|
||||
require.Equal(t, 2, len(entries))
|
||||
|
||||
// Finalized is treated as Complete, would need dynamic blockchain status to track the Finalized level
|
||||
filter.Statuses = []Status{FinalizedAS}
|
||||
entries, err = getActivityEntries(context.Background(), deps, allAddresses, true, []common.ChainID{}, filter, 0, 15)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, len(entries))
|
||||
require.Equal(t, 4, len(entries))
|
||||
|
||||
// Combined filter
|
||||
filter.Statuses = []Status{FailedAS, PendingAS}
|
||||
|
|
|
@ -34,11 +34,15 @@ WITH filter_conditions AS (
|
|||
? AS filterStatusFinalized,
|
||||
? AS filterStatusPending,
|
||||
? AS statusFailed,
|
||||
? AS statusSuccess,
|
||||
? AS statusCompleted,
|
||||
? AS statusFinalized,
|
||||
? AS statusPending,
|
||||
? AS includeAllTokenTypeAssets,
|
||||
? AS includeAllNetworks,
|
||||
? AS pendingStatus,
|
||||
? AS nowTimestamp,
|
||||
? AS layer2FinalisationDuration,
|
||||
? AS layer1FinalisationDuration,
|
||||
'0000000000000000000000000000000000000000' AS zeroAddress
|
||||
),
|
||||
-- This UNION between CTE and TEMP TABLE acts as an optimization. As soon as we drop one or use them interchangeably the performance drops significantly.
|
||||
|
@ -139,6 +143,9 @@ pending_network_ids AS (
|
|||
AND pending_transactions.network_id IN filter_networks
|
||||
GROUP BY
|
||||
pending_transactions.multi_transaction_id
|
||||
),
|
||||
layer2_networks(network_id) AS (
|
||||
VALUES %s
|
||||
)
|
||||
SELECT
|
||||
transfers.hash AS transfer_hash,
|
||||
|
@ -166,7 +173,16 @@ SELECT
|
|||
NULL AS mt_from_amount,
|
||||
NULL AS mt_to_amount,
|
||||
CASE
|
||||
WHEN transfers.status IS 1 THEN statusSuccess
|
||||
WHEN transfers.status IS 1 THEN
|
||||
CASE
|
||||
WHEN transfers.timestamp > 0 AND filter_conditions.nowTimestamp >= transfers.timestamp +
|
||||
(CASE
|
||||
WHEN transfers.network_id in layer2_networks THEN layer2FinalisationDuration
|
||||
ELSE layer1FinalisationDuration
|
||||
END)
|
||||
THEN statusFinalized
|
||||
ELSE statusCompleted
|
||||
END
|
||||
ELSE statusFailed
|
||||
END AS agg_status,
|
||||
1 AS agg_count,
|
||||
|
@ -282,15 +298,16 @@ WHERE
|
|||
AND (
|
||||
filterAllActivityStatus
|
||||
OR (
|
||||
(
|
||||
filterStatusCompleted
|
||||
OR filterStatusFinalized
|
||||
)
|
||||
AND transfers.status = 1
|
||||
filterStatusCompleted
|
||||
AND agg_status = statusCompleted
|
||||
)
|
||||
OR (
|
||||
filterStatusFinalized
|
||||
AND agg_status = statusFinalized
|
||||
)
|
||||
OR (
|
||||
filterStatusFailed
|
||||
AND transfers.status = 0
|
||||
AND agg_status = statusFailed
|
||||
)
|
||||
)
|
||||
UNION
|
||||
|
@ -401,8 +418,17 @@ SELECT
|
|||
multi_transactions.from_amount AS mt_from_amount,
|
||||
multi_transactions.to_amount AS mt_to_amount,
|
||||
CASE
|
||||
WHEN tr_status.min_status = 1
|
||||
AND COALESCE(pending_status.count, 0) = 0 THEN statusSuccess
|
||||
WHEN tr_status.min_status = 1 AND COALESCE(pending_status.count, 0) = 0 THEN
|
||||
CASE
|
||||
WHEN multi_transactions.timestamp > 0 AND filter_conditions.nowTimestamp >= multi_transactions.timestamp +
|
||||
(CASE
|
||||
WHEN multi_transactions.from_network_id in layer2_networks
|
||||
OR multi_transactions.to_network_id in layer2_networks THEN layer2FinalisationDuration
|
||||
ELSE layer1FinalisationDuration
|
||||
END)
|
||||
THEN statusFinalized
|
||||
ELSE statusCompleted
|
||||
END
|
||||
WHEN tr_status.min_status = 0 THEN statusFailed
|
||||
ELSE statusPending
|
||||
END AS agg_status,
|
||||
|
@ -475,11 +501,12 @@ WHERE
|
|||
AND (
|
||||
filterAllActivityStatus
|
||||
OR (
|
||||
(
|
||||
filterStatusCompleted
|
||||
OR filterStatusFinalized
|
||||
)
|
||||
AND agg_status = statusSuccess
|
||||
filterStatusCompleted
|
||||
AND agg_status = statusCompleted
|
||||
)
|
||||
OR (
|
||||
filterStatusFinalized
|
||||
AND agg_status = statusFinalized
|
||||
)
|
||||
OR (
|
||||
filterStatusFailed
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
|
@ -269,6 +270,9 @@ func (s *Service) getDeps() FilterDependencies {
|
|||
Address: t.Address,
|
||||
}
|
||||
},
|
||||
currentTimestamp: func() int64 {
|
||||
return time.Now().Unix()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue