feat(wallet/activity): Added API for tx and multiTx details (#3939)

This commit is contained in:
Cuteivist 2023-08-24 14:23:40 +02:00 committed by GitHub
parent 8d8bd4fc92
commit dd3e408a4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 306 additions and 50 deletions

View File

@ -48,61 +48,58 @@ const (
) )
type Entry struct { type Entry struct {
payloadType PayloadType payloadType PayloadType
transaction *transfer.TransactionIdentity transaction *transfer.TransactionIdentity
id transfer.MultiTransactionIDType id transfer.MultiTransactionIDType
timestamp int64 timestamp int64
activityType Type activityType Type
activityStatus Status activityStatus Status
amountOut *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT amountOut *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT
amountIn *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT amountIn *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT
tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT
tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT
sender *eth.Address sender *eth.Address
recipient *eth.Address recipient *eth.Address
chainIDOut *common.ChainID chainIDOut *common.ChainID
chainIDIn *common.ChainID chainIDIn *common.ChainID
transferType *TransferType transferType *TransferType
contractAddress *eth.Address
} }
type jsonSerializationTemplate struct { type jsonSerializationTemplate struct {
PayloadType PayloadType `json:"payloadType"` PayloadType PayloadType `json:"payloadType"`
Transaction *transfer.TransactionIdentity `json:"transaction"` Transaction *transfer.TransactionIdentity `json:"transaction"`
ID transfer.MultiTransactionIDType `json:"id"` ID transfer.MultiTransactionIDType `json:"id"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
ActivityType Type `json:"activityType"` ActivityType Type `json:"activityType"`
ActivityStatus Status `json:"activityStatus"` ActivityStatus Status `json:"activityStatus"`
AmountOut *hexutil.Big `json:"amountOut"` AmountOut *hexutil.Big `json:"amountOut"`
AmountIn *hexutil.Big `json:"amountIn"` AmountIn *hexutil.Big `json:"amountIn"`
TokenOut *Token `json:"tokenOut,omitempty"` TokenOut *Token `json:"tokenOut,omitempty"`
TokenIn *Token `json:"tokenIn,omitempty"` TokenIn *Token `json:"tokenIn,omitempty"`
Sender *eth.Address `json:"sender,omitempty"` Sender *eth.Address `json:"sender,omitempty"`
Recipient *eth.Address `json:"recipient,omitempty"` Recipient *eth.Address `json:"recipient,omitempty"`
ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"` ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"`
ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"` ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"`
TransferType *TransferType `json:"transferType,omitempty"` TransferType *TransferType `json:"transferType,omitempty"`
ContractAddress *eth.Address `json:"contractAddress,omitempty"`
} }
func (e *Entry) MarshalJSON() ([]byte, error) { func (e *Entry) MarshalJSON() ([]byte, error) {
return json.Marshal(jsonSerializationTemplate{ return json.Marshal(jsonSerializationTemplate{
PayloadType: e.payloadType, PayloadType: e.payloadType,
Transaction: e.transaction, Transaction: e.transaction,
ID: e.id, ID: e.id,
Timestamp: e.timestamp, Timestamp: e.timestamp,
ActivityType: e.activityType, ActivityType: e.activityType,
ActivityStatus: e.activityStatus, ActivityStatus: e.activityStatus,
AmountOut: e.amountOut, AmountOut: e.amountOut,
AmountIn: e.amountIn, AmountIn: e.amountIn,
TokenOut: e.tokenOut, TokenOut: e.tokenOut,
TokenIn: e.tokenIn, TokenIn: e.tokenIn,
Sender: e.sender, Sender: e.sender,
Recipient: e.recipient, Recipient: e.recipient,
ChainIDOut: e.chainIDOut, ChainIDOut: e.chainIDOut,
ChainIDIn: e.chainIDIn, ChainIDIn: e.chainIDIn,
TransferType: e.transferType, TransferType: e.transferType,
ContractAddress: e.contractAddress,
}) })
} }
@ -128,7 +125,6 @@ func (e *Entry) UnmarshalJSON(data []byte) error {
e.chainIDOut = aux.ChainIDOut e.chainIDOut = aux.ChainIDOut
e.chainIDIn = aux.ChainIDIn e.chainIDIn = aux.ChainIDIn
e.transferType = aux.TransferType e.transferType = aux.TransferType
e.contractAddress = aux.ContractAddress
return nil return nil
} }
@ -847,7 +843,6 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
entry.chainIDOut = outChainID entry.chainIDOut = outChainID
entry.chainIDIn = inChainID entry.chainIDIn = inChainID
entry.transferType = transferType entry.transferType = transferType
entry.contractAddress = contractAddress
entries = append(entries, entry) entries = append(entries, entry)
} }

View File

@ -1115,6 +1115,46 @@ func TestGetActivityEntriesNullAddresses(t *testing.T) {
require.Equal(t, 3, len(activities)) require.Equal(t, 3, len(activities))
} }
func TestGetTxDetails(t *testing.T) {
deps, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions 2 transactions (ETH/Goerli, ETH/Optimism), one MT USDC to DAI and another MT USDC to SNT
td, _, _ := fillTestData(t, deps.db)
_, err := getTxDetails(context.Background(), deps.db, "")
require.EqualError(t, err, "invalid tx id")
details, err := getTxDetails(context.Background(), deps.db, td.tr1.Hash.String())
require.NoError(t, err)
require.Equal(t, td.tr1.Hash.String(), details.ID)
require.Equal(t, 0, details.MultiTxID)
require.Equal(t, td.tr1.Nonce, details.Nonce)
require.Equal(t, td.tr1.BlkNumber, details.BlockNumber)
require.Equal(t, td.tr1.Contract, *details.Contract)
}
func TestGetMultiTxDetails(t *testing.T) {
deps, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions 2 transactions (ETH/Goerli, ETH/Optimism), one MT USDC to DAI and another MT USDC to SNT
td, _, _ := fillTestData(t, deps.db)
_, err := getMultiTxDetails(context.Background(), deps.db, 0)
require.EqualError(t, err, "invalid tx id")
details, err := getMultiTxDetails(context.Background(), deps.db, int(td.multiTx1.MultiTransactionID))
require.NoError(t, err)
require.Equal(t, "", details.ID)
require.Equal(t, int(td.multiTx1.MultiTransactionID), details.MultiTxID)
require.Equal(t, td.multiTx1Tr2.Nonce, details.Nonce)
require.Equal(t, td.multiTx1Tr2.BlkNumber, details.BlockNumber)
require.Equal(t, td.multiTx1Tr1.Contract, *details.Contract)
}
func setupBenchmark(b *testing.B, inMemory bool, resultCount int) (deps FilterDependencies, close func(), accounts []eth.Address) { func setupBenchmark(b *testing.B, inMemory bool, resultCount int) (deps FilterDependencies, close func(), accounts []eth.Address) {
deps, close = setupTestActivityDBStorageChoice(b, inMemory) deps, close = setupTestActivityDBStorageChoice(b, inMemory)

View File

@ -0,0 +1,195 @@
package activity
import (
"context"
"database/sql"
"encoding/hex"
"errors"
eth "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/sqlite"
)
type ProtocolType = int
const (
ProtocolHop ProtocolType = iota + 1
ProtocolUniswap
)
type EntryDetails struct {
ID string `json:"id"`
MultiTxID int `json:"multiTxId"`
Nonce uint64 `json:"nonce"`
BlockNumber int64 `json:"blockNumber"`
Input string `json:"input"`
ProtocolType *ProtocolType `json:"protocolType,omitempty"`
Hash *eth.Hash `json:"hash,omitempty"`
Contract *eth.Address `json:"contractAddress,omitempty"`
MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"`
GasLimit hexutil.Uint64 `json:"gasLimit"`
}
func protocolTypeFromDBType(dbType string) (protocolType *ProtocolType) {
protocolType = new(ProtocolType)
switch common.Type(dbType) {
case common.UniswapV2Swap:
fallthrough
case common.UniswapV3Swap:
*protocolType = ProtocolUniswap
case common.HopBridgeFrom:
fallthrough
case common.HopBridgeTo:
*protocolType = ProtocolHop
default:
return nil
}
return protocolType
}
func getMultiTxDetails(ctx context.Context, db *sql.DB, multiTxID int) (*EntryDetails, error) {
if multiTxID <= 0 {
return nil, errors.New("invalid tx id")
}
rows, err := db.QueryContext(ctx, `
SELECT
tx_hash,
blk_number,
type,
account_nonce,
tx,
contract_address
FROM
transfers
WHERE
multi_transaction_id = ?;`, multiTxID)
if err != nil {
return nil, err
}
defer rows.Close()
var maxFeePerGas *hexutil.Big
var gasLimit hexutil.Uint64
var input string
var protocolType *ProtocolType
var transferHash *eth.Hash
var contractAddress *eth.Address
var blockNumber int64
var nonce uint64
for rows.Next() {
var contractTypeDB sql.NullString
var transferHashDB, contractAddressDB sql.RawBytes
var blockNumberDB int64
var nonceDB uint64
tx := &types.Transaction{}
nullableTx := sqlite.JSONBlob{Data: tx}
err := rows.Scan(&transferHashDB, &blockNumberDB, &contractTypeDB, &nonceDB, &nullableTx, &contractAddressDB)
if err != nil {
return nil, err
}
if len(transferHashDB) > 0 {
transferHash = new(eth.Hash)
*transferHash = eth.BytesToHash(transferHashDB)
}
if contractTypeDB.Valid && protocolType == nil {
protocolType = protocolTypeFromDBType(contractTypeDB.String)
}
if blockNumberDB > 0 {
blockNumber = blockNumberDB
}
if nonceDB > 0 {
nonce = nonceDB
}
if len(input) == 0 && nullableTx.Valid {
if len(input) == 0 {
input = "0x" + hex.EncodeToString(tx.Data())
}
if maxFeePerGas == nil {
maxFeePerGas = (*hexutil.Big)(tx.GasFeeCap())
gasLimit = hexutil.Uint64(tx.Gas())
}
}
if contractAddress == nil && len(contractAddressDB) > 0 {
contractAddress = new(eth.Address)
*contractAddress = eth.BytesToAddress(contractAddressDB)
}
}
if err = rows.Err(); err != nil {
return nil, err
}
return &EntryDetails{
MultiTxID: multiTxID,
Nonce: nonce,
BlockNumber: blockNumber,
Hash: transferHash,
ProtocolType: protocolType,
Input: input,
Contract: contractAddress,
MaxFeePerGas: maxFeePerGas,
GasLimit: gasLimit,
}, nil
}
func getTxDetails(ctx context.Context, db *sql.DB, id string) (*EntryDetails, error) {
if len(id) == 0 {
return nil, errors.New("invalid tx id")
}
rows, err := db.QueryContext(ctx, `
SELECT
tx_hash,
blk_number,
account_nonce,
tx,
contract_address
FROM
transfers
WHERE
hash = ?;`, eth.HexToHash(id))
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, errors.New("Entry not found")
}
tx := &types.Transaction{}
nullableTx := sqlite.JSONBlob{Data: tx}
var transferHashDB, contractAddressDB sql.RawBytes
var blockNumber int64
var nonce uint64
err = rows.Scan(&transferHashDB, &blockNumber, &nonce, &nullableTx, &contractAddressDB)
if err != nil {
return nil, err
}
details := &EntryDetails{
ID: id,
Nonce: nonce,
BlockNumber: blockNumber,
}
if len(transferHashDB) > 0 {
details.Hash = new(eth.Hash)
*details.Hash = eth.BytesToHash(transferHashDB)
}
if len(contractAddressDB) > 0 {
details.Contract = new(eth.Address)
*details.Contract = eth.BytesToAddress(contractAddressDB)
}
if nullableTx.Valid {
details.Input = "0x" + hex.EncodeToString(tx.Data())
details.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap())
details.GasLimit = hexutil.Uint64(tx.Gas())
}
return details, nil
}

View File

@ -101,6 +101,14 @@ func (s *Service) FilterActivityAsync(requestID int32, addresses []common.Addres
}) })
} }
func (s *Service) GetMultiTxDetails(ctx context.Context, multiTxID int) (*EntryDetails, error) {
return getMultiTxDetails(ctx, s.db, multiTxID)
}
func (s *Service) GetTxDetails(ctx context.Context, id string) (*EntryDetails, error) {
return getTxDetails(ctx, s.db, id)
}
type GetRecipientsResponse struct { type GetRecipientsResponse struct {
Addresses []common.Address `json:"addresses"` Addresses []common.Address `json:"addresses"`
Offset int `json:"offset"` Offset int `json:"offset"`

View File

@ -547,6 +547,18 @@ func (api *API) FilterActivityAsync(requestID int32, addresses []common.Address,
return nil return nil
} }
func (api *API) GetMultiTxDetails(ctx context.Context, multiTxID int) (*activity.EntryDetails, error) {
log.Debug("wallet.api.GetMultiTxDetails", "multiTxID", multiTxID)
return api.s.activity.GetMultiTxDetails(ctx, multiTxID)
}
func (api *API) GetTxDetails(ctx context.Context, id string) (*activity.EntryDetails, error) {
log.Debug("wallet.api.GetTxDetails", "id", id)
return api.s.activity.GetTxDetails(ctx, id)
}
func (api *API) GetRecipientsAsync(requestID int32, offset int, limit int) (ignored bool, err error) { func (api *API) GetRecipientsAsync(requestID int32, offset int, limit int) (ignored bool, err error) {
log.Debug("wallet.api.GetRecipientsAsync", "offset", offset, "limit", limit) log.Debug("wallet.api.GetRecipientsAsync", "offset", offset, "limit", limit)

View File

@ -23,6 +23,8 @@ type TestTransaction struct {
Timestamp int64 Timestamp int64
BlkNumber int64 BlkNumber int64
Success bool Success bool
Nonce uint64
Contract eth_common.Address
MultiTransactionID MultiTransactionIDType MultiTransactionID MultiTransactionIDType
} }
@ -69,6 +71,8 @@ func generateTestTransaction(seed int) TestTransaction {
Timestamp: int64(seed), Timestamp: int64(seed),
BlkNumber: int64(seed), BlkNumber: int64(seed),
Success: true, Success: true,
Nonce: uint64(seed),
Contract: eth_common.HexToAddress(fmt.Sprintf("0x2%d", seed)),
MultiTransactionID: NoMultiTransactionID, MultiTransactionID: NoMultiTransactionID,
} }
} }
@ -281,7 +285,9 @@ func InsertTestTransferWithOptions(tb testing.TB, db *sql.DB, address eth_common
txValue: big.NewInt(tr.Value), txValue: big.NewInt(tr.Value),
txFrom: txFrom, txFrom: txFrom,
txTo: txTo, txTo: txTo,
txNonce: &tr.Nonce,
tokenAddress: &opt.TokenAddress, tokenAddress: &opt.TokenAddress,
contractAddress: &tr.Contract,
} }
err = updateOrInsertTransfersDBFields(tx, []transferDBFields{transfer}) err = updateOrInsertTransfersDBFields(tx, []transferDBFields{transfer})
require.NoError(tb, err) require.NoError(tb, err)