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
38 changes: 34 additions & 4 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,27 @@ on:
description: 'Environment name (dev/prod) for AWS credentials'
required: false
type: string
default: ''
default: 'dev'
workflow_dispatch:
inputs:
image_tag:
description: 'Tag for the Docker image (defaults to git SHA)'
required: false
type: string
default: ''
set_latest:
description: 'Also tag as latest'
required: false
type: boolean
default: false
environment:
description: 'Environment name (dev/prod) for AWS credentials'
required: false
type: choice
options:
- dev
- prod
default: 'dev'

permissions:
contents: read
Expand Down Expand Up @@ -155,8 +174,19 @@ jobs:
platform: linux/arm64
arch_suffix: arm64
runs-on: ${{ matrix.runs_on }}
environment: ${{ inputs.environment != '' && inputs.environment || '' }}
environment: ${{ inputs.environment || 'dev' }}
steps:
- name: Debug - Environment and Secrets Check
run: |
echo "=== Environment Debug ==="
echo "Input environment: '${{ inputs.environment }}'"
echo "Resolved environment: '${{ inputs.environment || 'dev' }}'"
echo "GitHub environment: ${{ github.environment }}"
echo ""
echo "=== Secrets Check ==="
echo "AWS_ROLE_TO_ASSUME is set: ${{ secrets.AWS_ROLE_TO_ASSUME != '' }}"
echo "AWS_REGION is set: ${{ secrets.AWS_REGION != '' }}"

- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

Expand Down Expand Up @@ -204,7 +234,7 @@ jobs:
merge-manifest:
needs: [build, determine-tag]
runs-on: ubuntu-latest
environment: ${{ inputs.environment != '' && inputs.environment || '' }}
environment: ${{ inputs.environment || 'dev' }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
Expand Down Expand Up @@ -317,7 +347,7 @@ jobs:
needs: [merge-manifest]
if: ${{ inputs.set_latest == true && github.event_name != 'workflow_dispatch' }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment != '' && inputs.environment || '' }}
environment: ${{ inputs.environment || 'dev' }}
env:
ECS_CLUSTER: ${{ secrets.ECS_CLUSTER }}
ECS_SERVICE: ${{ secrets.ECS_SERVICE }}
Expand Down
30 changes: 0 additions & 30 deletions src/services/b32.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/services/b32/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './schemas'
export * from './utils'

76 changes: 76 additions & 0 deletions src/services/b32/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { z } from 'zod'

/**
* Schema for ABI input parameter
*/
export const AbiInputSchema = z.object({
name: z.string().describe('Parameter name'),
type: z.string().describe('Parameter type (e.g., address, uint256)'),
indexed: z.boolean().optional().describe('Whether the parameter is indexed (for events)'),
internalType: z.string().optional().describe('Internal type'),
components: z.array(z.any()).optional().describe('Components for tuple types'),
})

/**
* Schema for ABI output parameter
*/
export const AbiOutputSchema = z.object({
name: z.string().describe('Output name'),
type: z.string().describe('Output type'),
internalType: z.string().optional().describe('Internal type'),
components: z.array(z.any()).optional().describe('Components for tuple types'),
})

/**
* Schema for a single ABI item (event, function, etc.)
*/
export const AbiItemSchema = z
.object({
type: z.enum(['event', 'function', 'constructor', 'fallback', 'receive', 'error']).describe('ABI item type'),
name: z.string().optional().describe('Name of the event/function'),
inputs: z.array(AbiInputSchema).optional().describe('Input parameters'),
outputs: z.array(AbiOutputSchema).optional().describe('Output parameters (for functions)'),
stateMutability: z.enum(['pure', 'view', 'nonpayable', 'payable']).optional().describe('State mutability'),
anonymous: z.boolean().optional().describe('Whether the event is anonymous'),
})
.passthrough()

/**
* Schema for a complete ABI (array of ABI items)
*/
export const AbiSchema = z.array(AbiItemSchema).describe('Contract ABI')

/**
* Schema for B32 signature lookup response
*/
export const B32SignatureLookupResultSchema = z.object({
signature: z.string().describe('The signature hash that was looked up'),
found: z.boolean().describe('Whether an ABI was found for the signature'),
abi: AbiSchema.optional().describe('The ABI if found'),
matchingItems: z
.array(
z.object({
type: z.string().describe('Type of ABI item (event, function, etc.)'),
name: z.string().optional().describe('Name of the item'),
signature: z.string().optional().describe('Human-readable signature'),
}),
)
.optional()
.describe('Summary of matching ABI items'),
})

/**
* Schema for hex signature (4 bytes for functions, 32 bytes for events)
*/
export const SignatureHashSchema = z
.string()
.regex(/^0x[a-fA-F0-9]+$/, 'Signature must be a 0x-prefixed hex string')
.describe('Signature hash (0x-prefixed)')

// Type exports
export type AbiInput = z.infer<typeof AbiInputSchema>
export type AbiOutput = z.infer<typeof AbiOutputSchema>
export type AbiItem = z.infer<typeof AbiItemSchema>
export type Abi = z.infer<typeof AbiSchema>
export type B32SignatureLookupResult = z.infer<typeof B32SignatureLookupResultSchema>

108 changes: 108 additions & 0 deletions src/services/b32/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { Abi as ViemAbi } from 'viem'
import { logger } from '@/utils/logger'
import type { Abi } from './schemas'

const B32_URL = 'https://b32.vecha.in'

/**
* Fetches ABI from B32 service by event/function signature hash
* B32 is a signature database that maps 4-byte function selectors and
* 32-byte event topic hashes to their corresponding ABI definitions.
*
* @param signature - The signature hash (topic[0] for events, 4-byte selector for functions)
* @returns The ABI if found, null otherwise
*/
export async function fetchAbiBySignature(signature: string): Promise<ViemAbi | null> {
try {
logger.debug(`Fetching ABI for signature: ${signature}`)

const response = await fetch(`${B32_URL}/q/${signature}.json`)

if (!response.ok) {
if (response.status === 404) {
logger.debug(`No ABI found for signature: ${signature}`)
return null
}
logger.warn(`Failed to fetch ABI: ${response.status} ${response.statusText}`)
return null
}

const abi = (await response.json()) as ViemAbi
logger.debug(`Successfully fetched ABI for signature: ${signature}`)

return abi
} catch (error) {
logger.warn(`Error fetching ABI for signature ${signature}:`, error)
return null
}
}

/**
* Fetches ABI from B32 and returns parsed result with metadata
* @param signature - The signature hash to look up
* @returns Structured result with ABI and matching items summary
*/
export async function lookupSignature(signature: string): Promise<{
found: boolean
abi: Abi | null
matchingItems: Array<{ type: string; name?: string; signature?: string }>
}> {
const abi = await fetchAbiBySignature(signature)

if (!abi) {
return {
found: false,
abi: null,
matchingItems: [],
}
}

// Extract summary of matching items
const matchingItems = abi.map((item) => {
const result: { type: string; name?: string; signature?: string } = {
type: item.type,
}

if ('name' in item && item.name) {
result.name = item.name
}

// Build human-readable signature for events and functions
if ((item.type === 'event' || item.type === 'function') && 'name' in item && 'inputs' in item) {
const inputs = item.inputs || []
const params = inputs.map((input) => input.type).join(', ')
result.signature = `${item.name}(${params})`
}

return result
})

return {
found: true,
abi: abi as Abi,
matchingItems,
}
}

/**
* Check if a signature hash is a valid format
* @param signature - The signature hash to validate
* @returns true if valid format
*/
export function isValidSignatureHash(signature: string): boolean {
// Must be 0x-prefixed hex string
if (!signature.startsWith('0x')) {
return false
}

// Remove 0x prefix and check if valid hex
const hex = signature.slice(2)
if (!/^[a-fA-F0-9]+$/.test(hex)) {
return false
}

// Function selectors are 4 bytes (8 hex chars), event topics are 32 bytes (64 hex chars)
// But B32 accepts various lengths
return hex.length >= 8
}

3 changes: 3 additions & 0 deletions src/services/sourcify/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './schemas'
export * from './utils'

Loading