feat: Calculate finalize status and filter by it (#3969)

This commit is contained in:
Cuteivist 2023-09-20 10:30:31 +02:00 committed by GitHub
parent d3c4ba315a
commit d29c6c5b6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 37 deletions

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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()
},
}
}