refactor: rename SolaceScanScout to Solace and update related configurations

- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
defiQUG
2026-04-10 12:52:17 -07:00
parent 6eef6b07f6
commit 0972178cc5
160 changed files with 13274 additions and 1061 deletions

View File

@@ -1,7 +1,52 @@
import { getGruCatalogPosture } from '@/services/api/gruCatalog'
export type DirectSearchTarget =
| { kind: 'address'; href: string; label: string }
| { kind: 'transaction'; href: string; label: string }
| { kind: 'block'; href: string; label: string }
| { kind: 'token'; href: string; label: string }
export interface SearchTokenHint {
chainId?: number
symbol?: string
address?: string
name?: string
tags?: string[]
}
export interface RawExplorerSearchItem {
type?: string | null
address?: string | null
block_number?: number | string | null
transaction_hash?: string | null
priority?: number | null
name?: string | null
symbol?: string | null
token_type?: string | null
}
export interface ExplorerSearchResult {
type: string
chain_id: number
data: {
hash?: string
address?: string
number?: number
}
score: number
href?: string
label: string
name?: string
symbol?: string
token_type?: string
is_curated_token?: boolean
is_gru_token?: boolean
is_x402_ready?: boolean
is_wrapped_transport?: boolean
currency_code?: string
match_reason?: string
matched_tags?: string[]
}
const addressPattern = /^0x[a-f0-9]{40}$/i
const transactionHashPattern = /^0x[a-f0-9]{64}$/i
@@ -39,3 +84,226 @@ export function inferDirectSearchTarget(query: string): DirectSearchTarget | nul
return null
}
export function inferTokenSearchTarget(query: string, tokens: SearchTokenHint[] = []): DirectSearchTarget | null {
const trimmed = query.trim()
if (!trimmed) {
return null
}
const lower = trimmed.toLowerCase()
const matched = tokens.find((token) => {
if (token.chainId !== 138) return false
return token.address?.toLowerCase() === lower || token.symbol?.toLowerCase() === lower
})
if (!matched?.address) {
return null
}
return {
kind: 'token',
href: `/tokens/${matched.address}`,
label: `Open token${matched.symbol ? ` (${matched.symbol})` : ''}`,
}
}
function normalizeNumber(value: string | number | null | undefined): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed
}
}
return undefined
}
function getTypeWeight(type: string): number {
switch (type) {
case 'token':
return 40
case 'transaction':
return 30
case 'address':
return 20
case 'block':
return 10
default:
return 0
}
}
function getExactnessBoost(query: string, item: RawExplorerSearchItem): number {
const trimmed = query.trim().toLowerCase()
if (!trimmed) {
return 0
}
const candidates = [
item.address?.toLowerCase(),
item.transaction_hash?.toLowerCase(),
item.symbol?.toLowerCase(),
item.name?.toLowerCase(),
normalizeNumber(item.block_number)?.toString(),
].filter((value): value is string => Boolean(value))
return candidates.includes(trimmed) ? 1000 : 0
}
function getCuratedMatchReason(query: string, token?: SearchTokenHint): string | undefined {
if (!token) return undefined
const trimmed = query.trim().toLowerCase()
if (!trimmed) return undefined
if (token.address?.toLowerCase() === trimmed) return 'exact curated token address'
if (token.symbol?.toLowerCase() === trimmed) return 'exact curated token symbol'
if (token.name?.toLowerCase() === trimmed) return 'exact curated token name'
if (token.symbol?.toLowerCase().includes(trimmed)) return 'symbol match'
if (token.name?.toLowerCase().includes(trimmed)) return 'name match'
if (token.tags?.some((tag) => tag.toLowerCase().includes(trimmed))) return 'tag match'
return undefined
}
function getHref(type: string, item: RawExplorerSearchItem, curatedToken: SearchTokenHint | undefined): string | undefined {
if ((type === 'token' || curatedToken) && item.address) {
return `/tokens/${item.address}`
}
if (type === 'address' && item.address) {
return `/addresses/${item.address}`
}
if (type === 'transaction' && item.transaction_hash) {
return `/transactions/${item.transaction_hash}`
}
const blockNumber = normalizeNumber(item.block_number)
if (type === 'block' && blockNumber != null) {
return `/blocks/${blockNumber}`
}
return undefined
}
function getLabel(type: string, item: RawExplorerSearchItem, curatedToken: SearchTokenHint | undefined): string {
if ((type === 'token' || curatedToken) && item.symbol) {
return `Token${item.symbol ? ` · ${item.symbol}` : ''}`
}
if ((type === 'token' || curatedToken) && item.name) {
return 'Token'
}
switch (type) {
case 'transaction':
return 'Transaction'
case 'block':
return 'Block'
case 'address':
return 'Address'
default:
return 'Search Result'
}
}
function getDeduplicationKey(type: string, item: RawExplorerSearchItem): string {
if ((type === 'token' || type === 'address') && item.address) {
return `entity:${item.address.toLowerCase()}`
}
if (type === 'transaction' && item.transaction_hash) {
return `tx:${item.transaction_hash.toLowerCase()}`
}
const blockNumber = normalizeNumber(item.block_number)
if (type === 'block' && blockNumber != null) {
return `block:${blockNumber}`
}
return `${type}:${item.address || item.transaction_hash || item.block_number || item.name || item.symbol || 'unknown'}`
}
export function normalizeExplorerSearchResults(
query: string,
items: RawExplorerSearchItem[] = [],
tokens: SearchTokenHint[] = [],
): ExplorerSearchResult[] {
const curatedLookup = new Map<string, SearchTokenHint>()
for (const token of tokens) {
if (token.chainId !== 138 || !token.address) continue
curatedLookup.set(token.address.toLowerCase(), token)
}
const deduped = new Map<string, ExplorerSearchResult & { _ranking: number }>()
for (const item of items) {
const type = item.type || 'unknown'
const blockNumber = normalizeNumber(item.block_number)
const curatedToken = item.address ? curatedLookup.get(item.address.toLowerCase()) : undefined
const normalizedType = type === 'address' && curatedToken ? 'token' : type
const gruPosture =
normalizedType === 'token' || normalizedType === 'address'
? getGruCatalogPosture({
symbol: item.symbol || curatedToken?.symbol,
address: item.address,
tags: curatedToken?.tags,
})
: null
const ranking =
getExactnessBoost(query, item) +
(item.priority ?? 0) * 10 +
getTypeWeight(normalizedType) +
(curatedToken ? 15 : 0) +
(gruPosture?.isGru ? 8 : 0) +
(gruPosture?.isX402Ready ? 5 : 0)
const result: ExplorerSearchResult & { _ranking: number } = {
type: normalizedType,
chain_id: 138,
data: {
hash: item.transaction_hash || undefined,
address: item.address || undefined,
number: blockNumber,
},
score: item.priority ?? 0,
href: getHref(normalizedType, item, curatedToken),
label: getLabel(normalizedType, item, curatedToken),
name: item.name || curatedToken?.name || undefined,
symbol: item.symbol || curatedToken?.symbol || undefined,
token_type: item.token_type || undefined,
is_curated_token: Boolean(curatedToken),
is_gru_token: gruPosture?.isGru || false,
is_x402_ready: gruPosture?.isX402Ready || false,
is_wrapped_transport: gruPosture?.isWrappedTransport || false,
currency_code: gruPosture?.currencyCode,
match_reason:
getCuratedMatchReason(query, curatedToken) ||
(getExactnessBoost(query, item) > 0 ? 'exact match' : undefined),
matched_tags: curatedToken?.tags?.filter((tag) => tag.toLowerCase().includes(query.trim().toLowerCase())) || [],
_ranking: ranking,
}
const key = getDeduplicationKey(normalizedType, item)
const existing = deduped.get(key)
if (!existing || result._ranking > existing._ranking) {
deduped.set(key, result)
}
}
return Array.from(deduped.values())
.sort((left, right) => right._ranking - left._ranking)
.map(({ _ranking, ...result }) => result)
}
export function suggestCuratedTokens(query: string, tokens: SearchTokenHint[] = []): SearchTokenHint[] {
const trimmed = query.trim().toLowerCase()
if (!trimmed) return []
return tokens
.filter((token) => token.chainId === 138)
.filter((token) =>
token.symbol?.toLowerCase().includes(trimmed) ||
token.name?.toLowerCase().includes(trimmed) ||
token.tags?.some((tag) => tag.toLowerCase().includes(trimmed)),
)
.sort((left, right) => {
const leftExact = left.symbol?.toLowerCase() === trimmed || left.name?.toLowerCase() === trimmed
const rightExact = right.symbol?.toLowerCase() === trimmed || right.name?.toLowerCase() === trimmed
if (leftExact !== rightExact) return leftExact ? -1 : 1
return (left.symbol || left.name || '').localeCompare(right.symbol || right.name || '')
})
.slice(0, 5)
}