diff --git a/docs/api/docs.go b/docs/api/docs.go index 2a763b8..417a2c3 100644 --- a/docs/api/docs.go +++ b/docs/api/docs.go @@ -1431,6 +1431,9 @@ const docTemplate = `{ }, "reserved": { "type": "number" + }, + "vested": { + "type": "number" } } }, diff --git a/docs/api/swagger.json b/docs/api/swagger.json index 5e04e6d..5a091bb 100644 --- a/docs/api/swagger.json +++ b/docs/api/swagger.json @@ -1420,6 +1420,9 @@ }, "reserved": { "type": "number" + }, + "vested": { + "type": "number" } } }, @@ -3240,4 +3243,4 @@ } } } -} \ No newline at end of file +} diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index c405c11..0e4dcfe 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -11,6 +11,8 @@ definitions: type: integer reserved: type: number + vested: + type: number type: object github_com_itering_subscan_plugins_balance_model.Transfer: properties: diff --git a/docs/heima_evm_interaction_records.md b/docs/heima_evm_interaction_records.md new file mode 100644 index 0000000..fa69d2b --- /dev/null +++ b/docs/heima_evm_interaction_records.md @@ -0,0 +1,94 @@ +# Heima EVM 智能合约交互记录调查 + +调查日期:2026-05-25 + +## 结论 + +当前状态:部分支持。 + +Heima 线上 API 已经索引 EVM 合约、普通 EVM 交易和交易 receipt logs。现有产品也已经有合约详情页、地址详情页、交易列表页和交易详情页,所以“某个合约有哪些直接 EVM 交易交互”可以查看。 + +但这还不是完整的智能合约交互记录能力。当前前端没有 raw event/log 页签、没有 internal transaction/trace 入口,也没有在交易详情里展示 ABI 解码后的 method call、event name 或参数。对于未验证合约,线上 API 返回的 `abi`、`method_identifiers`、`event_identifiers` 也为空,因此页面只能展示交易哈希、from/to、value、input data 等低层字段。 + +## 最佳线上证据 + +线上页面入口: + +- 合约页:`https://test-explorer.heima.network/contract/0xeb9c31afbe1bc3cfbb218f554148b456095def9b` +- 交易页:`https://test-explorer.heima.network/tx/0x2e2f768ca1ffc8a5d53fc6dcd7e4af36af02c29d762ecc0f1ecbf2825d70bb6d` + +线上 API 样本: + +- `POST https://explorer-api.heima.network/api/scan/metadata` 返回 `enable_evm: true`、`total_evm_contract: "22"`、`total_transaction: "2106"`。 +- `POST https://explorer-api.heima.network/api/plugin/evm/contracts` 返回合约 `0xeb9c31afbe1bc3cfbb218f554148b456095def9b`,`transaction_count: 46`。 +- `POST https://explorer-api.heima.network/api/plugin/evm/transactions` 加 `address: "0xeb9c31afbe1bc3cfbb218f554148b456095def9b"` 返回多条真实交互交易,例如 `0x2e2f768ca1ffc8a5d53fc6dcd7e4af36af02c29d762ecc0f1ecbf2825d70bb6d`。 +- `POST https://explorer-api.heima.network/api/plugin/evm/transaction` 查询该 hash 返回 `to_address: "0xeb9c31afbe1bc3cfbb218f554148b456095def9b"`、`input_data: "0x28d3a294..."`、`success: true`、`extrinsic_index: "9649159-2"`。 +- `GET https://explorer-api.heima.network/api/plugin/evm/etherscan?module=logs&action=getLogs&address=0xeb9c31afbe1bc3cfbb218f554148b456095def9b&page=1&offset=10` 返回多条 receipt log,说明数据库/API 层有 raw event/log 数据。 + +## 产品和代码层面 + +已存在入口: + +- `ui-react/src/pages/contract/[id].tsx`:合约详情页,包含 `Contract`、`Transactions`、`ERC-20 Transfers`、`ERC-721 Transfers` 页签。 +- `ui-react/src/pages/address/[id].tsx`:地址详情页,包含 token、交易和 transfer 页签。 +- `ui-react/src/pages/tx/[id].tsx`:EVM 交易详情页,展示 hash、from、to、value、result、nonce、input data、fee 和 signature。 +- `ui-react/src/components/tx/txTable.tsx`:EVM 交易表,按地址或区块过滤。 + +已存在 API/索引能力: + +- `plugins/evm/http/http.go` 注册了 `transactions`、`transaction`、`contracts`、`contract`、`token/transfer`、`etherscan` 等 EVM API。 +- `plugins/evm/dao/transaction.go` 在扫描 EVM block transaction 时调用 `eth_getTransactionReceipt`,写入 `evm_transactions` 和 `evm_transaction_receipts`。 +- `plugins/evm/dao/api.go` 的 `API_GetLogs` 从 `evm_transaction_receipts` 输出 Etherscan-compatible logs。 +- `plugins/evm/dao/event.go` 会对部分 receipt event 做 token/proxy 后处理,例如 ERC-20/ERC-721 Transfer 和 proxy Upgraded。 + +## 缺失层级和上游原因 + +前端层: + +- 缺少合约详情页的 raw `Logs / Events` 页签。 +- 缺少交易详情页的 receipt logs、decoded method call 和 decoded event 参数展示。 +- 缺少 internal transaction/trace 页签或入口。 + +API 层: + +- 已有 Etherscan-compatible `logs/getLogs` raw log API,但没有被当前 React 页面消费。 +- `txlistinternal` 请求当前返回 HTTP 200 且 body 为空,未形成可展示的 internal transaction 数据契约。 +- `API_Transactions` 的 `FunctionName` 仍是空字符串,当前 API 没有把 method selector 映射成可读函数名输出给页面。 + +数据库层: + +- 已有 `evm_transactions` 和 `evm_transaction_receipts`,能够支撑直接交易列表和 raw event/log。 +- 未看到 internal transaction/trace 专用表;当前 schema 不能支撑合约内部调用树展示。 +- 示例合约 `0xeb9c31afbe1bc3cfbb218f554148b456095def9b` 未验证,线上 `abi`、`method_identifiers`、`event_identifiers` 均为空,限制了 method/event 解码。 + +扫描解析层: + +- 扫描器已经解析 block transaction 和 receipt logs。 +- 扫描器没有显式采集 `debug_traceTransaction`、`trace_*` 或等价 internal call 数据。 +- receipt log 后处理目前聚焦 token/proxy 特定事件,不是通用 ABI event 解码流水线。 + +## 最小补齐路径 + +前端: + +1. 在合约详情页增加 `Logs / Events` 页签,消费现有 `etherscan?module=logs&action=getLogs&address=...`。 +2. 在交易详情页增加 receipt logs 区块,并展示 topic、data、log index、contract address。 +3. 对已验证合约增加 method/event 名称和参数展示;未验证合约保留 raw input/topic/data。 + +API: + +1. 提供 UI-native 的 `POST /api/plugin/evm/logs` 或在前端规范化消费现有 Etherscan GET logs。 +2. 在 `transactions` 和 `transaction` 返回值中补充 method selector、已知 method name 和 decode 状态。 +3. 明确定义 `internal transactions` API;在没有 trace 数据前返回结构化空结果,而不是空 body。 + +数据库: + +1. 复用 `evm_transaction_receipts` 支撑 raw logs。 +2. 增加 method/event ABI 映射缓存与按合约、topic、selector 的查询索引。 +3. 如需 internal tx,新增 trace/internal call 表,至少记录 tx hash、trace path、from、to、value、input、output、call type、status/error。 + +扫描解析: + +1. 继续保存 `eth_getTransactionReceipt` logs。 +2. 对已验证合约按 ABI 解码 input 和 logs。 +3. 如链节点支持 trace/debug RPC,新增 internal call 采集任务;如不支持,需要先确认 Heima 节点侧 RPC 能力。 diff --git a/plugins/balance/dao/account.go b/plugins/balance/dao/account.go index b217703..a535a2e 100644 --- a/plugins/balance/dao/account.go +++ b/plugins/balance/dao/account.go @@ -7,7 +7,6 @@ import ( bModel "github.com/itering/subscan/plugins/balance/model" "github.com/itering/subscan/util/address" "github.com/itering/substrate-api-rpc/rpc" - "github.com/shopspring/decimal" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -70,24 +69,49 @@ func RefreshAccount(ctx context.Context, s *Storage, accountId string) error { if q.RowsAffected == 1 { _, _ = s.Pool.HINCRBY(ctx, model.MetadataCacheKey(), "total_account", 1) } - return AfterAccountCreate(ctx, db, &account) + currentBlock, _ := s.Dao.GetCurrentBlockNum(ctx) + return AfterAccountCreate(ctx, db, &account, currentBlock) } -func AfterAccountCreate(ctx context.Context, db *gorm.DB, account *bModel.Account) error { +func AfterAccountCreate(ctx context.Context, db *gorm.DB, account *bModel.Account, currentBlock uint64) error { accountDataRaw, err := rpc.ReadStorage(nil, "system", "account", "", account.Address) if err != nil { return err } accountData := new(bModel.AccountData) accountDataRaw.ToAny(accountData) + locks := ReadBalanceLocks(account.Address) + vesting := ReadVesting(account.Address) + lockSummary := bModel.AccountLockSummary(accountData, locks) return db.WithContext(ctx).Model(account).Where("address = ?", account.Address).UpdateColumns(map[string]interface{}{ "nonce": accountData.Nonce, "balance": accountData.Data.Free.Add(accountData.Data.Reserved), - "locked": decimal.Max(accountData.Data.MiscFrozen, accountData.Data.FeeFrozen), + "locked": lockSummary.Locked, "reserved": accountData.Data.Reserved, + "vested": bModel.SummarizeVesting(vesting, currentBlock), }).Error } +func ReadBalanceLocks(accountId string) []bModel.BalanceLock { + locksRaw, err := rpc.ReadStorage(nil, "balances", "locks", "", accountId) + if err != nil { + return nil + } + var locks []bModel.BalanceLock + locksRaw.ToAny(&locks) + return locks +} + +func ReadVesting(accountId string) []bModel.VestingInfo { + vestingRaw, err := rpc.ReadStorage(nil, "vesting", "vesting", "", accountId) + if err != nil { + return nil + } + var vesting []bModel.VestingInfo + vestingRaw.ToAny(&vesting) + return vesting +} + func (s *Storage) AddOrUpdateItem(c context.Context, item interface{}, keys []string, updates ...string) *gorm.DB { var keyFields []clause.Column for _, key := range keys { diff --git a/plugins/balance/dao/script.go b/plugins/balance/dao/script.go index 0d17003..bd7b39d 100644 --- a/plugins/balance/dao/script.go +++ b/plugins/balance/dao/script.go @@ -9,7 +9,6 @@ import ( "github.com/itering/subscan/util" "github.com/itering/subscan/util/address" "github.com/panjf2000/ants/v2" - "github.com/shopspring/decimal" "gorm.io/gorm" "log" "sync" @@ -17,6 +16,9 @@ import ( func InitAccount(sg *Storage) { ctx := context.Background() + locksByAddress := readAllBalanceLocks(ctx) + vestingByAddress := readAllVesting(ctx) + currentBlock, _ := sg.Dao.GetCurrentBlockNum(ctx) wg := new(sync.WaitGroup) bp, _ := ants.NewPoolWithFunc(10, func(i interface{}) { wg.Add(1) @@ -24,13 +26,16 @@ func InitAccount(sg *Storage) { params := i.([]interface{}) addr := params[0].(string) info := params[1].(*bModel.AccountData) + lockSummary := bModel.AccountLockSummary(info, locksByAddress[addr]) + vested := bModel.SummarizeVesting(vestingByAddress[addr], currentBlock) sg.AddOrUpdateItem(ctx, &bModel.Account{ Address: addr, Nonce: info.Nonce, Balance: info.Data.Free.Add(info.Data.Reserved), - Locked: decimal.Max(info.Data.MiscFrozen, info.Data.FeeFrozen), + Locked: lockSummary.Locked, Reserved: info.Data.Reserved, - }, []string{"address"}, "nonce", "balance", "locked", "reserved") + Vested: vested, + }, []string{"address"}, "nonce", "balance", "locked", "reserved", "vested") }) defer bp.Release() @@ -51,6 +56,50 @@ func InitAccount(sg *Storage) { wg.Wait() } +func readAllBalanceLocks(ctx context.Context) map[string][]bModel.BalanceLock { + result := map[string][]bModel.BalanceLock{} + _ = substrate.BatchReadKeysPaged(ctx, "Balances", "Locks", "", func(keys []string, scaleType string) error { + r, _ := substrate.BatchStorageByKey(ctx, keys, scaleType, "") + for key, v := range r { + val, err := substrate.ParseStorageKey(key) + if err != nil || len(val) == 0 { + continue + } + addr := address.Format(val[0].ToString()) + if addr == "" { + continue + } + var locks []bModel.BalanceLock + v.ToAny(&locks) + result[addr] = locks + } + return nil + }) + return result +} + +func readAllVesting(ctx context.Context) map[string][]bModel.VestingInfo { + result := map[string][]bModel.VestingInfo{} + _ = substrate.BatchReadKeysPaged(ctx, "Vesting", "Vesting", "", func(keys []string, scaleType string) error { + r, _ := substrate.BatchStorageByKey(ctx, keys, scaleType, "") + for key, v := range r { + val, err := substrate.ParseStorageKey(key) + if err != nil || len(val) == 0 { + continue + } + addr := address.Format(val[0].ToString()) + if addr == "" { + continue + } + var vesting []bModel.VestingInfo + v.ToAny(&vesting) + result[addr] = vesting + } + return nil + }) + return result +} + func RefreshAllAccount(_ *Storage) { } diff --git a/plugins/balance/model/model.go b/plugins/balance/model/model.go index 7b86e43..5809908 100644 --- a/plugins/balance/model/model.go +++ b/plugins/balance/model/model.go @@ -1,8 +1,6 @@ package model -import ( - "github.com/shopspring/decimal" -) +import "github.com/shopspring/decimal" type Account struct { ID uint `gorm:"primary_key" json:"-"` @@ -11,6 +9,7 @@ type Account struct { Balance decimal.Decimal `json:"balance" gorm:"type:decimal(65,0);index:balance;index:balance_address,priority:1"` Locked decimal.Decimal `json:"locked" gorm:"type:decimal(65,0);"` Reserved decimal.Decimal `json:"reserved" gorm:"type:decimal(65,0);"` + Vested decimal.Decimal `json:"vested" gorm:"type:decimal(65,0);"` } func (a *Account) TableName() string { @@ -25,9 +24,69 @@ type AccountData struct { Reserved decimal.Decimal `json:"reserved"` MiscFrozen decimal.Decimal `json:"miscFrozen"` FeeFrozen decimal.Decimal `json:"feeFrozen"` + Frozen decimal.Decimal `json:"frozen"` } `json:"data"` } +type BalanceLock struct { + ID string `json:"id"` + Amount decimal.Decimal `json:"amount"` +} + +type LockSummary struct { + Locked decimal.Decimal +} + +func (a AccountData) LockedBalance() decimal.Decimal { + return decimal.Max(a.Data.Frozen, decimal.Max(a.Data.MiscFrozen, a.Data.FeeFrozen)) +} + +func SummarizeLocks(locks []BalanceLock) LockSummary { + var summary LockSummary + for _, lock := range locks { + if lock.Amount.GreaterThan(summary.Locked) { + summary.Locked = lock.Amount + } + } + return summary +} + +func AccountLockSummary(accountData *AccountData, locks []BalanceLock) LockSummary { + summary := SummarizeLocks(locks) + if accountData == nil { + return summary + } + if dataLocked := accountData.LockedBalance(); dataLocked.GreaterThan(summary.Locked) { + summary.Locked = dataLocked + } + return summary +} + +type VestingInfo struct { + Locked decimal.Decimal `json:"locked"` + PerBlock decimal.Decimal `json:"perBlock"` + StartingBlock uint64 `json:"startingBlock"` +} + +func (v VestingInfo) VestedAt(blockNum uint64) decimal.Decimal { + if blockNum <= v.StartingBlock { + return decimal.Zero + } + vested := v.PerBlock.Mul(decimal.NewFromUint64(blockNum - v.StartingBlock)) + if vested.GreaterThan(v.Locked) { + return v.Locked + } + return vested +} + +func SummarizeVesting(vesting []VestingInfo, blockNum uint64) decimal.Decimal { + var vested decimal.Decimal + for _, schedule := range vesting { + vested = vested.Add(schedule.VestedAt(blockNum)) + } + return vested +} + type Transfer struct { Id uint `json:"id" gorm:"primary_key;autoIncrement:false"` BlockNum uint `json:"blockNum" gorm:"size:32"` diff --git a/plugins/balance/model/model_test.go b/plugins/balance/model/model_test.go new file mode 100644 index 0000000..059d001 --- /dev/null +++ b/plugins/balance/model/model_test.go @@ -0,0 +1,57 @@ +package model + +import ( + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" +) + +func TestAccountDataLockedBalanceSupportsLegacyAndCurrentFields(t *testing.T) { + accountData := AccountData{} + accountData.Data.MiscFrozen = decimal.NewFromInt(3) + accountData.Data.FeeFrozen = decimal.NewFromInt(5) + accountData.Data.Frozen = decimal.NewFromInt(7) + + assert.Equal(t, "7", accountData.LockedBalance().String()) +} + +func TestAccountLockSummaryUsesMaxLockedValue(t *testing.T) { + accountData := AccountData{} + accountData.Data.Frozen = decimal.NewFromInt(10) + + summary := AccountLockSummary(&accountData, []BalanceLock{ + {ID: "0x7374616b696e6720", Amount: decimal.NewFromInt(12)}, + }) + + assert.Equal(t, "12", summary.Locked.String()) +} + +func TestSummarizeVestingCalculatesVestedAmount(t *testing.T) { + summary := SummarizeVesting([]VestingInfo{ + { + Locked: decimal.NewFromInt(100), + PerBlock: decimal.NewFromInt(3), + StartingBlock: 10, + }, + { + Locked: decimal.NewFromInt(50), + PerBlock: decimal.NewFromInt(10), + StartingBlock: 12, + }, + }, 16) + + assert.Equal(t, "58", summary.String()) +} + +func TestVestingInfoVestedAtCapsAtLocked(t *testing.T) { + schedule := VestingInfo{ + Locked: decimal.NewFromInt(100), + PerBlock: decimal.NewFromInt(3), + StartingBlock: 10, + } + + assert.Equal(t, "0", schedule.VestedAt(10).String()) + assert.Equal(t, "15", schedule.VestedAt(15).String()) + assert.Equal(t, "100", schedule.VestedAt(100).String()) +} diff --git a/plugins/evm/dao/api.go b/plugins/evm/dao/api.go index 659cae0..703f03f 100644 --- a/plugins/evm/dao/api.go +++ b/plugins/evm/dao/api.go @@ -491,7 +491,12 @@ func (a AccountsJson) Cursor() string { func (a *ApiSrv) AccountsCursor(ctx context.Context, address string, limit int, before, after *string) ([]AccountsJson, map[string]interface{}) { var list []AccountsJson fetch := limit + 1 - q := sg.db.WithContext(ctx).Select("evm_account,balance").Model(&Account{}).Joins("join balance_accounts on evm_accounts.address=balance_accounts.address") + q := sg.db.WithContext(ctx). + Select("evm_accounts.evm_account,balance"). + 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") if address != "" { q = q.Where("evm_account = ?", address) } diff --git a/plugins/evm/dao/api_cursor_test.go b/plugins/evm/dao/api_cursor_test.go index 6236dbd..2cf6869 100644 --- a/plugins/evm/dao/api_cursor_test.go +++ b/plugins/evm/dao/api_cursor_test.go @@ -12,12 +12,21 @@ import ( "gorm.io/gorm" ) -func TestAccountsCursorFiltersByEvmAccount(t *testing.T) { +func setupAccountsCursorTest(t *testing.T) *gorm.DB { + t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) - require.NoError(t, db.AutoMigrate(&Account{}, &balanceModel.Account{})) + require.NoError(t, db.AutoMigrate(&Account{}, &Contract{}, &balanceModel.Account{})) + originalSg := sg sg = &Storage{db: db} + t.Cleanup(func() { sg = originalSg }) + + return db +} + +func TestAccountsCursorFiltersByEvmAccount(t *testing.T) { + db := setupAccountsCursorTest(t) ctx := context.Background() target := "0x63c4545ac01c77cc74044f25b8edea3880224577" @@ -37,11 +46,7 @@ func TestAccountsCursorFiltersByEvmAccount(t *testing.T) { } func TestAccountsCursorBeforeUsesBeforeCursor(t *testing.T) { - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - require.NoError(t, err) - require.NoError(t, db.AutoMigrate(&Account{}, &balanceModel.Account{})) - - sg = &Storage{db: db} + db := setupAccountsCursorTest(t) ctx := context.Background() accounts := []struct { @@ -69,3 +74,29 @@ func TestAccountsCursorBeforeUsesBeforeCursor(t *testing.T) { assert.Equal(t, false, page["has_previous_page"]) assert.Equal(t, true, page["has_next_page"]) } + +func TestAccountsCursorExcludesSmartContracts(t *testing.T) { + db := setupAccountsCursorTest(t) + + ctx := context.Background() + eoa := "0x0000000000000000000000000000000000000001" + contract := "0x0000000000000000000000000000000000000002" + + require.NoError(t, db.Create(&Account{Address: "substrate-eoa", EvmAccount: eoa}).Error) + require.NoError(t, db.Create(&Account{Address: "substrate-contract", EvmAccount: contract}).Error) + require.NoError(t, db.Create(&balanceModel.Account{Address: "substrate-eoa", Balance: decimal.NewFromInt(10)}).Error) + 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) + + 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) + assert.Empty(t, list) + assert.Nil(t, page["start_cursor"]) + assert.Nil(t, page["end_cursor"]) +} diff --git a/plugins/evm/http/accounts_e2e_test.go b/plugins/evm/http/accounts_e2e_test.go new file mode 100644 index 0000000..3663a94 --- /dev/null +++ b/plugins/evm/http/accounts_e2e_test.go @@ -0,0 +1,67 @@ +package http + +import ( + "bytes" + "encoding/json" + nethttp "net/http" + "net/http/httptest" + "strings" + "testing" + + balanceModel "github.com/itering/subscan/plugins/balance/model" + "github.com/itering/subscan/plugins/evm/dao" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestAccountsRouteExcludesSmartContracts(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, db.AutoMigrate(&dao.Account{}, &dao.Contract{}, &balanceModel.Account{})) + + dao.Init(db, nil) + originalSrv := srv + srv = &dao.ApiSrv{} + t.Cleanup(func() { srv = originalSrv }) + + eoa := "0x0000000000000000000000000000000000000001" + contract := "0x0000000000000000000000000000000000000002" + + require.NoError(t, db.Create(&dao.Account{Address: "substrate-eoa", EvmAccount: eoa}).Error) + require.NoError(t, db.Create(&dao.Account{Address: "substrate-contract", EvmAccount: contract}).Error) + require.NoError(t, db.Create(&balanceModel.Account{Address: "substrate-eoa", Balance: decimal.NewFromInt(10)}).Error) + 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(&dao.Contract{Address: contract}).Error) + + request := httptest.NewRequest( + nethttp.MethodPost, + "/api/plugin/evm/accounts", + strings.NewReader(`{"row":10}`), + ) + recorder := httptest.NewRecorder() + handler := nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) { + _ = accountsHandle(w, r) + }) + + handler.ServeHTTP(recorder, request) + require.Equal(t, nethttp.StatusOK, recorder.Code) + + var response struct { + Code int `json:"code"` + Data struct { + List []dao.AccountsJson `json:"list"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response)) + require.Zero(t, response.Code) + require.Len(t, response.Data.List, 1) + assert.Equal(t, eoa, response.Data.List[0].EvmAccount) + assert.NotEqual(t, contract, response.Data.List[0].EvmAccount) + + 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()) +} diff --git a/ui-react/src/pages/sub/account/[id].tsx b/ui-react/src/pages/sub/account/[id].tsx index 56fd159..17bb35d 100644 --- a/ui-react/src/pages/sub/account/[id].tsx +++ b/ui-react/src/pages/sub/account/[id].tsx @@ -37,7 +37,7 @@ export default function Page() { } else { return BIG_ZERO } - }, [accountData?.reserved, accountData?.locked, accountData?.balance, metadata?.enabledNewTransferableFormulas]) + }, [accountData, metadata?.enabledNewTransferableFormulas]) return ( @@ -69,6 +69,11 @@ export default function Page() {
{getBalanceAmount(new BigNumber(accountData.locked), token?.decimals).toFormat()}
+
+
Vested
+
{getBalanceAmount(new BigNumber(accountData.vested || 0), token?.decimals).toFormat()}
+
+
Reserved
{getBalanceAmount(new BigNumber(accountData.reserved), token?.decimals).toFormat()}
diff --git a/ui-react/src/utils/api.ts b/ui-react/src/utils/api.ts index 42dcdac..29c5d06 100644 --- a/ui-react/src/utils/api.ts +++ b/ui-react/src/utils/api.ts @@ -256,6 +256,7 @@ export type accountType = { locked: string nonce: string reserved: string + vested?: string } type getAccountParams = {