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

@@ -0,0 +1,406 @@
import { getExplorerApiBase, fetchBlockscoutJson } from './blockscout'
import { keccak_256 } from 'js-sha3'
export interface ContractMethodParam {
name: string
type: string
}
export interface ContractMethod {
name: string
signature: string
stateMutability: string
inputs: ContractMethodParam[]
outputs: ContractMethodParam[]
}
export interface ContractMethodExecutionResult {
value: string
}
export interface ContractProfile {
has_custom_methods_read: boolean
has_custom_methods_write: boolean
proxy_type?: string
is_self_destructed?: boolean
implementations: string[]
creation_bytecode?: string
deployed_bytecode?: string
source_verified: boolean
abi_available: boolean
contract_name?: string
compiler_version?: string
optimization_enabled?: boolean
optimization_runs?: number
evm_version?: string
license_type?: string
constructor_arguments?: string
abi?: string
source_code_preview?: string
source_status_text?: string
read_methods: ContractMethod[]
write_methods: ContractMethod[]
}
function truncateHex(value?: string | null, maxLength = 66): string | undefined {
if (!value) return undefined
if (value.length <= maxLength) return value
return `${value.slice(0, maxLength)}...`
}
function truncateText(value?: string | null, maxLength = 400): string | undefined {
if (!value) return undefined
if (value.length <= maxLength) return value
return `${value.slice(0, maxLength)}...`
}
interface ContractCompatibilityAbiResponse {
status?: string | null
message?: string | null
result?: string | null
}
interface ContractCompatibilitySourceRecord {
Address?: string
ContractName?: string
CompilerVersion?: string
OptimizationUsed?: string | number
Runs?: string | number
EVMVersion?: string
LicenseType?: string
ConstructorArguments?: string
SourceCode?: string
ABI?: string
}
interface ContractCompatibilitySourceResponse {
status?: string | null
message?: string | null
result?: ContractCompatibilitySourceRecord[] | null
}
interface ABIEntry {
type?: string
name?: string
stateMutability?: string
constant?: boolean
inputs?: Array<{ name?: string; type?: string }>
outputs?: Array<{ name?: string; type?: string }>
}
async function fetchCompatJson<T>(params: URLSearchParams): Promise<T> {
const response = await fetch(`${getExplorerApiBase()}/api?${params.toString()}`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json() as Promise<T>
}
function normalizeBooleanFlag(value: string | number | null | undefined): boolean | undefined {
if (value == null || value === '') return undefined
if (typeof value === 'number') return value === 1
return value === '1' || value.toLowerCase() === 'true'
}
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 parseABI(abiString?: string): ContractMethod[] {
if (!abiString) return []
try {
const parsed = JSON.parse(abiString) as ABIEntry[]
if (!Array.isArray(parsed)) return []
return parsed
.filter((entry) => entry.type === 'function' && entry.name)
.map((entry) => {
const inputs = Array.isArray(entry.inputs)
? entry.inputs.map((input) => ({
name: input.name || '',
type: input.type || 'unknown',
}))
: []
const outputs = Array.isArray(entry.outputs)
? entry.outputs.map((output) => ({
name: output.name || '',
type: output.type || 'unknown',
}))
: []
return {
name: entry.name || 'unknown',
signature: `${entry.name || 'unknown'}(${inputs.map((input) => input.type).join(',')})`,
stateMutability:
entry.stateMutability ||
(entry.constant || outputs.length > 0 ? 'view' : 'nonpayable'),
inputs,
outputs,
}
})
} catch {
return []
}
}
function isReadMethod(method: ContractMethod): boolean {
return method.stateMutability === 'view' || method.stateMutability === 'pure'
}
function isSupportedInputType(type: string): boolean {
return ['address', 'bool', 'string', 'uint256', 'uint8', 'bytes32'].includes(type)
}
function isSupportedOutputType(type: string): boolean {
return ['address', 'bool', 'string', 'uint256', 'uint8', 'bytes32', 'bytes'].includes(type)
}
function supportsSimpleReadCall(method: ContractMethod): boolean {
return (
method.outputs.length === 1 &&
method.inputs.every((input) => isSupportedInputType(input.type)) &&
method.outputs.every((output) => isSupportedOutputType(output.type))
)
}
function supportsSimpleWriteCall(method: ContractMethod): boolean {
return !isReadMethod(method) && method.inputs.every((input) => isSupportedInputType(input.type))
}
function getPublicRpcUrl(): string {
return process.env.NEXT_PUBLIC_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org'
}
function decodeDynamicString(wordData: string, offset: number): string {
const lengthHex = wordData.slice(offset, offset + 64)
const length = parseInt(lengthHex || '0', 16)
const start = offset + 64
const end = start + length * 2
const contentHex = wordData.slice(start, end)
if (!contentHex) return ''
const bytes = contentHex.match(/.{1,2}/g) || []
return bytes
.map((byte) => String.fromCharCode(parseInt(byte, 16)))
.join('')
.replace(/\u0000+$/g, '')
}
function validateAndEncodeInput(type: string, value: string): { head: string; tail?: string } {
const trimmed = value.trim()
switch (type) {
case 'address': {
if (!/^0x[a-fA-F0-9]{40}$/.test(trimmed)) {
throw new Error('Address inputs must be full 0x-prefixed addresses.')
}
return { head: trimmed.slice(2).toLowerCase().padStart(64, '0') }
}
case 'bool':
if (!['true', 'false', '1', '0'].includes(trimmed.toLowerCase())) {
throw new Error('Boolean inputs must be true/false or 1/0.')
}
return { head: (trimmed === 'true' || trimmed === '1' ? '1' : '0').padStart(64, '0') }
case 'uint256':
case 'uint8': {
if (!/^\d+$/.test(trimmed)) {
throw new Error('Unsigned integer inputs must be non-negative decimal numbers.')
}
return { head: BigInt(trimmed).toString(16).padStart(64, '0') }
}
case 'bytes32': {
if (!/^0x[a-fA-F0-9]{64}$/.test(trimmed)) {
throw new Error('bytes32 inputs must be 32-byte 0x-prefixed hex values.')
}
return { head: trimmed.slice(2).toLowerCase() }
}
case 'string': {
const contentHex = Array.from(new TextEncoder().encode(trimmed))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
const paddedContent = contentHex.padEnd(Math.ceil(contentHex.length / 64) * 64 || 64, '0')
const lengthHex = contentHex.length / 2
return {
head: '',
tail:
BigInt(lengthHex).toString(16).padStart(64, '0') +
paddedContent,
}
}
default:
throw new Error(`Unsupported input type ${type}`)
}
}
export function encodeMethodCalldata(method: ContractMethod, values: string[]): string {
if (values.length !== method.inputs.length) {
throw new Error('Method input count does not match the provided values.')
}
const selector = keccak_256(method.signature).slice(0, 8)
const encodedInputs = method.inputs.map((input, index) => validateAndEncodeInput(input.type, values[index] || ''))
let dynamicOffsetWords = method.inputs.length * 32
const heads = encodedInputs.map((encoded) => {
if (encoded.tail != null) {
const head = BigInt(dynamicOffsetWords).toString(16).padStart(64, '0')
dynamicOffsetWords += encoded.tail.length / 2
return head
}
return encoded.head
})
const tails = encodedInputs
.filter((encoded) => encoded.tail != null)
.map((encoded) => encoded.tail || '')
.join('')
return `0x${selector}${heads.join('')}${tails}`
}
function decodeSimpleOutput(outputType: string, data: string): string {
const normalized = data.replace(/^0x/i, '')
if (!normalized) return 'No data returned'
switch (outputType) {
case 'address':
return `0x${normalized.slice(24, 64)}`
case 'bool':
return BigInt(`0x${normalized.slice(0, 64)}`) === 0n ? 'false' : 'true'
case 'string': {
const offset = Number(BigInt(`0x${normalized.slice(0, 64)}`) * 2n)
return decodeDynamicString(normalized, offset)
}
case 'bytes32':
return `0x${normalized.slice(0, 64)}`
case 'bytes': {
const offset = Number(BigInt(`0x${normalized.slice(0, 64)}`) * 2n)
const length = parseInt(normalized.slice(offset + 64, offset + 128) || '0', 16)
const start = offset + 128
return `0x${normalized.slice(start, start + length * 2)}`
}
default:
if (outputType.startsWith('uint') || outputType.startsWith('int')) {
return BigInt(`0x${normalized.slice(0, 64)}`).toString()
}
return `0x${normalized}`
}
}
export async function callSimpleReadMethod(address: string, method: ContractMethod, values: string[] = []): Promise<string> {
if (!supportsSimpleReadCall(method)) {
throw new Error('Only simple read methods with supported input and output types are supported in this explorer surface.')
}
const data = encodeMethodCalldata(method, values)
const response = await fetch(getPublicRpcUrl(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_call',
params: [
{
to: address,
data,
},
'latest',
],
}),
})
if (!response.ok) {
throw new Error(`RPC HTTP ${response.status}`)
}
const payload = (await response.json()) as { result?: string; error?: { message?: string } }
if (payload.error?.message) {
throw new Error(payload.error.message)
}
const result = payload.result || '0x'
return decodeSimpleOutput(method.outputs[0]?.type || 'bytes', result)
}
export const contractsApi = {
getProfileSafe: async (address: string): Promise<{ ok: boolean; data: ContractProfile | null }> => {
try {
const [raw, abiResponse, sourceResponse] = await Promise.all([
fetchBlockscoutJson<{
has_custom_methods_read?: boolean
has_custom_methods_write?: boolean
proxy_type?: string | null
is_self_destructed?: boolean | null
implementations?: Array<{ address?: string | null } | string>
creation_bytecode?: string | null
deployed_bytecode?: string | null
}>(`/api/v2/smart-contracts/${address}`),
fetchCompatJson<ContractCompatibilityAbiResponse>(
new URLSearchParams({
module: 'contract',
action: 'getabi',
address,
}),
).catch(() => null),
fetchCompatJson<ContractCompatibilitySourceResponse>(
new URLSearchParams({
module: 'contract',
action: 'getsourcecode',
address,
}),
).catch(() => null),
])
const sourceRecord = Array.isArray(sourceResponse?.result) ? sourceResponse?.result[0] : undefined
const abiString =
abiResponse?.status === '1' && abiResponse.result && abiResponse.result !== 'Contract source code not verified'
? abiResponse.result
: sourceRecord?.ABI && sourceRecord.ABI !== 'Contract source code not verified'
? sourceRecord.ABI
: undefined
const sourceCode = sourceRecord?.SourceCode
const parsedMethods = parseABI(abiString)
const sourceVerified = Boolean(
abiString ||
(sourceCode && sourceCode.trim().length > 0) ||
(sourceRecord?.ContractName && sourceRecord.ContractName.trim().length > 0),
)
const sourceStatusText = abiResponse?.message || sourceResponse?.message || (sourceVerified ? 'Verified source available' : 'Contract source code not verified')
return {
ok: true,
data: {
has_custom_methods_read: !!raw.has_custom_methods_read,
has_custom_methods_write: !!raw.has_custom_methods_write,
proxy_type: raw.proxy_type || undefined,
is_self_destructed: raw.is_self_destructed ?? undefined,
implementations: Array.isArray(raw.implementations)
? raw.implementations
.map((entry) => typeof entry === 'string' ? entry : entry.address || '')
.filter(Boolean)
: [],
creation_bytecode: truncateHex(raw.creation_bytecode),
deployed_bytecode: truncateHex(raw.deployed_bytecode),
source_verified: sourceVerified,
abi_available: Boolean(abiString),
contract_name: sourceRecord?.ContractName || undefined,
compiler_version: sourceRecord?.CompilerVersion || undefined,
optimization_enabled: normalizeBooleanFlag(sourceRecord?.OptimizationUsed),
optimization_runs: normalizeNumber(sourceRecord?.Runs),
evm_version: sourceRecord?.EVMVersion || undefined,
license_type: sourceRecord?.LicenseType || undefined,
constructor_arguments: truncateHex(sourceRecord?.ConstructorArguments, 90),
abi: truncateText(abiString, 1200),
source_code_preview: truncateText(sourceCode, 1200),
source_status_text: sourceStatusText || undefined,
read_methods: parsedMethods.filter(isReadMethod),
write_methods: parsedMethods.filter((method) => !isReadMethod(method)),
},
}
} catch {
return { ok: false, data: null }
}
},
supportsSimpleReadCall,
supportsSimpleWriteCall,
}