diff --git a/internal/server/http/scan.go b/internal/server/http/scan.go
index f066bce..8cf8c2e 100644
--- a/internal/server/http/scan.go
+++ b/internal/server/http/scan.go
@@ -1,14 +1,18 @@
package http
import (
+ "context"
"errors"
- "github.com/itering/subscan/model"
- "github.com/itering/subscan/share/token"
- "github.com/itering/subscan/util/address"
+ "strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
+ "github.com/itering/subscan/model"
+ "github.com/itering/subscan/plugins"
+ evmDao "github.com/itering/subscan/plugins/evm/dao"
+ "github.com/itering/subscan/share/token"
"github.com/itering/subscan/util"
+ "github.com/itering/subscan/util/address"
)
// @Summary Current network metadata
@@ -285,11 +289,11 @@ func logsHandle(c *gin.Context) {
}
type checkSearchParams struct {
- Hash string `json:"hash" binding:"len=66"`
+ Hash string `json:"hash" binding:"required"`
}
-// checkSearchHashHandle handler check hash type, block or extrinsic or evm tx hash
-// @Summary Check hash type
+// checkSearchHashHandle handler check search type, block/extrinsic/evm hash or account address
+// @Summary Check search type
// @Tags hash
// @Accept json
// @Produce json
@@ -304,19 +308,62 @@ func checkSearchHashHandle(c *gin.Context) {
}
ctx := c.Request.Context()
+ search := strings.TrimSpace(p.Hash)
- if data := svc.GetBlockByHash(ctx, p.Hash); data != nil {
+ switch address.DetectSearchType(search) {
+ case address.SearchTypeEvmAddress:
+ toJson(c, map[string]string{"hash_type": evmAddressSearchType(ctx, search)}, nil)
+ return
+ case address.SearchTypeSubstrateAddress:
+ toJson(c, map[string]string{"hash_type": address.SearchTypeSubstrateAddress}, nil)
+ return
+ case address.SearchTypeHash:
+ default:
+ toJson(c, nil, util.ParamsError)
+ return
+ }
+
+ if data := svc.GetBlockByHash(ctx, search); data != nil {
toJson(c, map[string]string{"hash_type": "block"}, nil)
return
}
- if data := svc.GetExtrinsicByHash(ctx, p.Hash); data != nil {
+ if data := svc.GetExtrinsicByHash(ctx, search); data != nil {
toJson(c, map[string]string{"hash_type": "extrinsic"}, nil)
return
}
- // todo evm tx hash
+ if hashType := evmHashSearchType(ctx, search); hashType != "" {
+ toJson(c, map[string]string{"hash_type": hashType}, nil)
+ return
+ }
toJson(c, nil, util.RecordNotFound)
}
+func evmAddressSearchType(ctx context.Context, search string) string {
+ if evmPluginEnabled() && evmDao.ContractExists(ctx, address.Format(search)) {
+ return "evm_contract"
+ }
+ return address.SearchTypeEvmAddress
+}
+
+func evmHashSearchType(ctx context.Context, search string) string {
+ if !evmPluginEnabled() {
+ return ""
+ }
+ srv := evmDao.ApiSrv{}
+ if srv.BlockByHash(ctx, search) != nil {
+ return "evm_block"
+ }
+ if srv.GetTransactionByHash(ctx, search) != nil {
+ return "evm_transaction"
+ }
+ return ""
+}
+
+func evmPluginEnabled() bool {
+ evm, ok := plugins.RegisteredPlugins["evm"]
+ return ok && evm.Enable()
+}
+
// @Summary Get runtime list
// @Description runtimeListHandler get runtime list
// @Tags runtime
diff --git a/plugins/evm/dao/contract.go b/plugins/evm/dao/contract.go
index 0792646..9b3f9a1 100644
--- a/plugins/evm/dao/contract.go
+++ b/plugins/evm/dao/contract.go
@@ -312,6 +312,11 @@ func backfillContractFromRuntimeCode(ctx context.Context, contractAddress string
return contract
}
+func ContractExists(ctx context.Context, address string) bool {
+ var contract Contract
+ return sg.db.WithContext(ctx).Select("address").Where("address = ?", address).Take(&contract).Error == nil
+}
+
func findEventIdentifiers(_ context.Context, abiRaw []byte) []byte {
var abiValue abi.ABI
_ = abiValue.UnmarshalJSON(abiRaw)
diff --git a/ui-react/src/__tests__/components/Navbar.test.tsx b/ui-react/src/__tests__/components/Navbar.test.tsx
index 41d1051..0ef924b 100644
--- a/ui-react/src/__tests__/components/Navbar.test.tsx
+++ b/ui-react/src/__tests__/components/Navbar.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, fireEvent } from '@testing-library/react'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { Navbar } from '@/components/navbar'
import { useRouter } from 'next/router'
import '@testing-library/jest-dom'
@@ -15,6 +15,7 @@ jest.mock('@/utils/api', () => ({
...jest.requireActual('@/utils/api'),
useMetadata: jest.fn(),
useToken: jest.fn(),
+ checkSearchHash: jest.fn(),
}))
describe('Navbar', () => {
@@ -34,9 +35,10 @@ describe('Navbar', () => {
const mockToken = { TST: { price: '100', change: '1' } }
beforeEach(() => {
- (useRouter as jest.Mock).mockReturnValue(mockRouter)
+ ;(useRouter as jest.Mock).mockReturnValue(mockRouter)
;(api.useMetadata as jest.Mock).mockReturnValue({ data: { data: mockMetadata } })
;(api.useToken as jest.Mock).mockReturnValue({ data: { data: mockToken } })
+ ;(api.checkSearchHash as jest.Mock).mockResolvedValue({ code: 0, data: { hash_type: 'block' } })
})
afterEach(() => {
@@ -47,7 +49,7 @@ describe('Navbar', () => {
return render(
- ,
+
)
}
@@ -56,13 +58,25 @@ describe('Navbar', () => {
expect(screen.getByText('Heima Explorer')).toBeInTheDocument()
})
- it('handles search with enter key', () => {
+ it('handles search with enter key', async () => {
renderNavbar()
const searchInput = screen.getByPlaceholderText('Search')
fireEvent.change(searchInput, { target: { value: '123456' } })
fireEvent.keyDown(searchInput, { key: 'Enter' })
- expect(mockRouter.push).toHaveBeenCalledWith('/sub/block/123456')
+ await waitFor(() => expect(mockRouter.push).toHaveBeenCalledWith('/sub/block/123456'))
+ })
+
+ it('auto-detects a substrate account without using the selected block type', async () => {
+ renderNavbar()
+ const searchInput = screen.getByPlaceholderText('Search')
+ const address = '47BHMeKG1Q36gU6WP9ZGiqFhEPF5BhfyTVn9NSaemMd9e9uP'
+
+ fireEvent.change(searchInput, { target: { value: address } })
+ fireEvent.keyDown(searchInput, { key: 'Enter' })
+
+ await waitFor(() => expect(mockRouter.push).toHaveBeenCalledWith(`/sub/account/${address}`))
+ expect(api.checkSearchHash).not.toHaveBeenCalled()
})
})
diff --git a/ui-react/src/components/navbar/navbar.tsx b/ui-react/src/components/navbar/navbar.tsx
index 193718b..a6e7d95 100644
--- a/ui-react/src/components/navbar/navbar.tsx
+++ b/ui-react/src/components/navbar/navbar.tsx
@@ -22,10 +22,16 @@ import { useData } from '@/context'
import Image from 'next/image'
import _ from 'lodash'
import { env } from 'next-runtime-env'
+import { checkSearchHash } from '@/utils/api'
interface Props extends BareProps {
value: string
}
+
+type SearchType = 'auto' | 'sub_block' | 'sub_extrinsic' | 'sub_event' | 'sub_account' | 'pvm_block' | 'pvm_tx' | 'pvm_contract' | 'pvm_account'
+
+const evmAddressRegex = /^(0x)?[0-9a-fA-F]{40}$/
+const substrateAccountRegex = /^[1-9A-HJ-NP-Za-km-z]{32,60}$/
const ChevronDown = ({ fill, size, ...props }: { fill?: string; size?: number | string } & React.SVGProps) => {
return (