feat(wallet/activity): Added API for tx and multiTx details (#3939)
This commit is contained in:
parent
8d8bd4fc92
commit
dd3e408a4e
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue