Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/api/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,9 @@ const docTemplate = `{
"type": "string",
"minLength": 0
},
"include_contracts": {
"type": "boolean"
},
"row": {
"type": "integer",
"maximum": 100,
Expand Down
3 changes: 3 additions & 0 deletions docs/api/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2520,6 +2520,9 @@
"type": "string",
"minLength": 0
},
"include_contracts": {
"type": "boolean"
},
"row": {
"type": "integer",
"maximum": 100,
Expand Down
2 changes: 2 additions & 0 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,8 @@ definitions:
before:
minLength: 0
type: string
include_contracts:
type: boolean
row:
maximum: 100
minimum: 1
Expand Down
86 changes: 78 additions & 8 deletions plugins/evm/dao/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"context"
"fmt"
"github.com/itering/subscan/model"
"github.com/itering/subscan/pkg/go-web3/complex/types"
"github.com/itering/subscan/pkg/go-web3/dto"
balanceModel "github.com/itering/subscan/plugins/balance/model"
"github.com/itering/subscan/share/web3"
"github.com/itering/subscan/util"
"github.com/shopspring/decimal"
"strings"
Expand All @@ -25,7 +28,7 @@ type ISrv interface {
BlockByNum(ctx context.Context, blockNum uint) *EvmBlock
BlockByHash(ctx context.Context, hash string) *EvmBlock
TransactionsCursor(ctx context.Context, limit int, before, after *uint, opts ...model.Option) ([]TransactionSampleJson, map[string]interface{})
AccountsCursor(ctx context.Context, address string, limit int, before, after *string) ([]AccountsJson, map[string]interface{})
AccountsCursor(ctx context.Context, address string, includeContracts bool, limit int, before, after *string) ([]AccountsJson, map[string]interface{})
ContractsCursor(ctx context.Context, limit int, before, after *string) ([]ContractsJson, map[string]interface{})

AccountTokens(ctx context.Context, address, category string) []AccountTokenJson
Expand Down Expand Up @@ -116,11 +119,11 @@ func transactionReceiptsToEtherscanLogs(ctx context.Context, list []TransactionR
return
}

func (a *ApiSrv) API_GetAccounts(ctx context.Context, h160 []string) (map[string]balanceModel.Account, error) {
func (a *ApiSrv) API_GetAccounts(ctx context.Context, h160s []string) (map[string]balanceModel.Account, error) {
var addresses []string
var addr2H160 = make(map[string]string)

for _, v := range h160 {
for _, v := range h160s {
addr := h160ToAccountIdByNetwork(ctx, v, util.NetworkNode)
if addr == "" {
return nil, fmt.Errorf("address %s not a valid address", v)
Expand All @@ -136,6 +139,14 @@ func (a *ApiSrv) API_GetAccounts(ctx context.Context, h160 []string) (map[string
for _, v := range accounts {
accountMap[addr2H160[v.Address]] = v
}
for _, h160 := range h160s {
if balance, ok := latestEvmNativeBalance(ctx, h160); ok {
account := accountMap[h160]
account.Address = h160ToAccountIdByNetwork(ctx, h160, util.NetworkNode)
account.Balance = balance
accountMap[h160] = account
}
}
return accountMap, nil
}

Expand Down Expand Up @@ -488,15 +499,24 @@ func (a AccountsJson) Cursor() string {
return util.Base64Encode(fmt.Sprintf("%s_%s", a.Balance.String(), a.EvmAccount))
}

func (a *ApiSrv) AccountsCursor(ctx context.Context, address string, limit int, before, after *string) ([]AccountsJson, map[string]interface{}) {
func (a *ApiSrv) AccountsCursor(ctx context.Context, address string, includeContracts bool, limit int, before, after *string) ([]AccountsJson, map[string]interface{}) {
var list []AccountsJson
fetch := limit + 1
singleAddressWithContracts := includeContracts && address != ""
selectClause := "evm_accounts.evm_account,balance"
balanceJoin := "join balance_accounts on evm_accounts.address=balance_accounts.address"
if singleAddressWithContracts {
selectClause = "evm_accounts.evm_account,COALESCE(balance_accounts.balance,0) as balance"
balanceJoin = "left join balance_accounts on evm_accounts.address=balance_accounts.address"
}
q := sg.db.WithContext(ctx).
Select("evm_accounts.evm_account,balance").
Select(selectClause).
Model(&Account{}).
Joins("join balance_accounts on evm_accounts.address=balance_accounts.address").
Joins("left join evm_contracts on evm_contracts.address=evm_accounts.evm_account").
Where("evm_contracts.address IS NULL")
Joins(balanceJoin)
if !includeContracts {
q = q.Joins("left join evm_contracts on evm_contracts.address=evm_accounts.evm_account").
Where("evm_contracts.address IS NULL")
}
if address != "" {
q = q.Where("evm_account = ?", address)
}
Expand All @@ -508,6 +528,15 @@ func (a *ApiSrv) AccountsCursor(ctx context.Context, address string, limit int,
q = q.Order("balance desc").Order("balance_accounts.address desc")
}
q.Limit(fetch).Scan(&list)
if singleAddressWithContracts {
if balance, ok := latestEvmContractDisplayBalance(ctx, address); ok {
if len(list) == 0 {
list = append(list, AccountsJson{EvmAccount: address, Balance: balance})
} else {
list[0].Balance = balance
}
}
}
var hasPrev, hasNext bool
if before != nil && *before != "" {
hasPrev = len(list) > limit
Expand Down Expand Up @@ -535,6 +564,47 @@ func (a *ApiSrv) AccountsCursor(ctx context.Context, address string, limit int,
return list, map[string]interface{}{"start_cursor": start, "end_cursor": end, "has_previous_page": hasPrev, "has_next_page": hasNext}
}

func latestEvmContractDisplayBalance(ctx context.Context, address string) (decimal.Decimal, bool) {
nativeBalance, nativeOK := latestEvmNativeBalance(ctx, address)
depositBalance, depositOK := latestEvmContractDepositBalance(ctx, address)
if depositOK {
if nativeOK {
return nativeBalance.Add(depositBalance), true
}
return depositBalance, true
}
return nativeBalance, nativeOK
}

func latestEvmNativeBalance(ctx context.Context, address string) (decimal.Decimal, bool) {
if web3.RPC == nil || web3.RPC.Eth == nil {
return decimal.Zero, false
}
balance, err := web3.RPC.Eth.GetBalance(ctx, address, "latest")
if err != nil || balance == nil {
return decimal.Zero, false
}
return decimal.NewFromBigInt(balance, 0), true
}

func latestEvmContractDepositBalance(ctx context.Context, address string) (decimal.Decimal, bool) {
if web3.RPC == nil || web3.RPC.Eth == nil {
return decimal.Zero, false
}
result, err := web3.RPC.Eth.Call(ctx, &dto.TransactionParameters{
To: address,
Data: types.ComplexString("0xc399ec88"), // getDeposit()
})
if err != nil || result == nil {
return decimal.Zero, false
}
balance, err := result.ToBigInt()
if err != nil || balance == nil {
return decimal.Zero, false
}
return decimal.NewFromBigInt(balance, 0), true
}

type ContractsJson struct {
ContractName string `json:"contract_name"`
Address string `json:"address"`
Expand Down
31 changes: 27 additions & 4 deletions plugins/evm/dao/api_cursor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestAccountsCursorFiltersByEvmAccount(t *testing.T) {
require.NoError(t, db.Create(&balanceModel.Account{Address: "target-account", Balance: decimal.NewFromInt(5)}).Error)
require.NoError(t, db.Create(&balanceModel.Account{Address: "other-account", Balance: decimal.NewFromInt(10)}).Error)

list, page := (&ApiSrv{}).AccountsCursor(ctx, target, 10, nil, nil)
list, page := (&ApiSrv{}).AccountsCursor(ctx, target, false, 10, nil, nil)

require.Len(t, list, 1)
assert.Equal(t, target, list[0].EvmAccount)
Expand Down Expand Up @@ -67,7 +67,7 @@ func TestAccountsCursorBeforeUsesBeforeCursor(t *testing.T) {
EvmAccount: "0x0000000000000000000000000000000000000002",
Balance: decimal.NewFromInt(20),
}.Cursor()
list, page := (&ApiSrv{}).AccountsCursor(ctx, "", 10, &cursor, nil)
list, page := (&ApiSrv{}).AccountsCursor(ctx, "", false, 10, &cursor, nil)

require.Len(t, list, 1)
assert.Equal(t, accounts[0].account, list[0].EvmAccount)
Expand All @@ -88,15 +88,38 @@ func TestAccountsCursorExcludesSmartContracts(t *testing.T) {
require.NoError(t, db.Create(&balanceModel.Account{Address: "substrate-contract", Balance: decimal.NewFromInt(20)}).Error)
require.NoError(t, db.Session(&gorm.Session{SkipHooks: true}).Create(&Contract{Address: contract}).Error)

list, page := (&ApiSrv{}).AccountsCursor(ctx, "", 10, nil, nil)
list, page := (&ApiSrv{}).AccountsCursor(ctx, "", false, 10, nil, nil)

require.Len(t, list, 1)
assert.Equal(t, eoa, list[0].EvmAccount)
assert.Equal(t, decimal.NewFromInt(10), list[0].Balance)
assert.Equal(t, false, page["has_next_page"])

list, page = (&ApiSrv{}).AccountsCursor(ctx, contract, 10, nil, nil)
list, page = (&ApiSrv{}).AccountsCursor(ctx, contract, false, 10, nil, nil)
assert.Empty(t, list)
assert.Nil(t, page["start_cursor"])
assert.Nil(t, page["end_cursor"])

list, page = (&ApiSrv{}).AccountsCursor(ctx, contract, true, 10, nil, nil)
require.Len(t, list, 1)
assert.Equal(t, contract, list[0].EvmAccount)
assert.Equal(t, decimal.NewFromInt(20), list[0].Balance)
assert.Equal(t, false, page["has_next_page"])
}

func TestAccountsCursorIncludesContractWithoutIndexedBalance(t *testing.T) {
db := setupAccountsCursorTest(t)

ctx := context.Background()
contract := "0x0000000000000000000000000000000000000003"

require.NoError(t, db.Create(&Account{Address: "substrate-contract", EvmAccount: contract}).Error)
require.NoError(t, db.Session(&gorm.Session{SkipHooks: true}).Create(&Contract{Address: contract}).Error)

list, page := (&ApiSrv{}).AccountsCursor(ctx, contract, true, 10, nil, nil)

require.Len(t, list, 1)
assert.Equal(t, contract, list[0].EvmAccount)
assert.True(t, list[0].Balance.IsZero())
assert.Equal(t, false, page["has_next_page"])
}
15 changes: 15 additions & 0 deletions plugins/evm/http/accounts_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ func TestAccountsRouteExcludesSmartContracts(t *testing.T) {
assert.Equal(t, eoa, response.Data.List[0].EvmAccount)
assert.NotEqual(t, contract, response.Data.List[0].EvmAccount)

request = httptest.NewRequest(
nethttp.MethodPost,
"/api/plugin/evm/accounts",
strings.NewReader(`{"address":"`+contract+`","row":10,"include_contracts":true}`),
)
recorder = httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
require.Equal(t, nethttp.StatusOK, recorder.Code)

require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
require.Zero(t, response.Code)
require.Len(t, response.Data.List, 1)
assert.Equal(t, contract, response.Data.List[0].EvmAccount)
assert.Equal(t, decimal.NewFromInt(20), response.Data.List[0].Balance)

var pretty bytes.Buffer
require.NoError(t, json.Indent(&pretty, recorder.Body.Bytes(), "", " "))
t.Logf("POST /api/plugin/evm/accounts response with seeded evm_accounts and evm_contracts:\n%s", pretty.String())
Expand Down
2 changes: 1 addition & 1 deletion plugins/evm/http/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (m MockServer) TransactionsCursor(ctx context.Context, limit int, before, a
return nil, nil
}

func (m MockServer) AccountsCursor(ctx context.Context, address string, limit int, before, after *string) ([]dao.AccountsJson, map[string]interface{}) {
func (m MockServer) AccountsCursor(ctx context.Context, address string, includeContracts bool, limit int, before, after *string) ([]dao.AccountsJson, map[string]interface{}) {
return nil, nil
}

Expand Down
11 changes: 6 additions & 5 deletions plugins/evm/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,11 @@ func transactionsHandle(w http.ResponseWriter, r *http.Request) error {
}

type EvmAccountParams struct {
Limit int `json:"row" validate:"min=1,max=100"`
Before *string `json:"before" validate:"omitempty,min=0"`
After *string `json:"after" validate:"omitempty,min=0"`
Address string `json:"address" validate:"omitempty,eth_addr"`
Limit int `json:"row" validate:"min=1,max=100"`
Before *string `json:"before" validate:"omitempty,min=0"`
After *string `json:"after" validate:"omitempty,min=0"`
Address string `json:"address" validate:"omitempty,eth_addr"`
IncludeContracts bool `json:"include_contracts"`
}

// @Summary Evm accounts list
Expand All @@ -310,7 +311,7 @@ func accountsHandle(w http.ResponseWriter, r *http.Request) error {
toJson(w, 10001, nil, err)
return nil
}
list, page := srv.AccountsCursor(r.Context(), p.Address, p.Limit, p.Before, p.After)
list, page := srv.AccountsCursor(r.Context(), p.Address, p.IncludeContracts, p.Limit, p.Before, p.After)
toJson(w, 0, map[string]interface{}{"list": list, "pagination": page}, nil)
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions ui-react/src/components/cursorPagination/cursorPagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { BareProps } from '@/types/page'
import { themeType } from '@/utils/text'

interface PaginationInfo {
start_cursor: number
end_cursor: number
start_cursor: number | string
end_cursor: number | string
has_next_page: boolean
has_previous_page: boolean
}
Expand Down
6 changes: 3 additions & 3 deletions ui-react/src/components/pvmAccount/accountTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'
import { BareProps } from '@/types/page'
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, getKeyValue, Spinner } from '@heroui/react'
import { getBalanceAmount, getThemeColor } from '@/utils/text'
import { getExtrinsicListParams, unwrap, usePVMAccounts } from '@/utils/api'
import { getPVMAccountListParams, unwrap, usePVMAccounts } from '@/utils/api'
import { PAGE_SIZE } from '@/utils/const'
import { useData } from '@/context'
import BigNumber from 'bignumber.js'
Expand All @@ -12,13 +12,13 @@ import { CursorPagination } from '../cursorPagination'
import { env } from 'next-runtime-env'

interface Props extends BareProps {
args?: getExtrinsicListParams
args?: getPVMAccountListParams
}

const Component: React.FC<Props> = ({ children, className, args }) => {
const { metadata, token } = useData()
const [page, setPage] = React.useState(1)
const [cursor, setCursor] = React.useState<{ after?: number; before?: number }>({})
const [cursor, setCursor] = React.useState<{ after?: string; before?: string }>({})
const rowsPerPage = PAGE_SIZE
const NEXT_PUBLIC_API_HOST = env('NEXT_PUBLIC_API_HOST') || ''
const { data, isLoading } = usePVMAccounts(NEXT_PUBLIC_API_HOST, {
Expand Down
1 change: 1 addition & 0 deletions ui-react/src/pages/contract/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function Page() {

const { data: accountsData, isLoading } = usePVMAccounts(NEXT_PUBLIC_API_HOST, {
address: id,
include_contracts: true,
row: 10,
page: 0,
})
Expand Down
9 changes: 5 additions & 4 deletions ui-react/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,8 @@ export type pvmAccountListType = {
list: pvmAccountType[] | null
count: number
pagination: {
start_cursor: number,
end_cursor: number,
start_cursor: string,
end_cursor: string,
has_next_page: boolean,
has_previous_page: boolean
}
Expand All @@ -460,9 +460,10 @@ export type pvmAccountListType = {
export type getPVMAccountListParams = {
page?: number
row?: number
after?: number
before?: number
after?: string
before?: string
address?: string
include_contracts?: boolean
}

export const usePVMAccounts = (host: string, data: getPVMAccountListParams) => {
Expand Down
Loading