Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d417333
Add ROFL-8004 constants
lubej Nov 20, 2025
5859ae2
Add ROFL-8004 utils
lubej Nov 20, 2025
c1a370a
Add disabled prop to InputFormField textarea
lubej Nov 20, 2025
c8b95d6
Add ERC-8004 step in create flow
lubej Nov 20, 2025
d9f0abd
Add ROFL-8004 to compose.yaml in case configuration is set
lubej Nov 20, 2025
5d73aa6
Add json language to CodeDisplay
lubej Nov 20, 2025
f671365
Add ERC-8004 contract interactions
lubej Nov 20, 2025
8f70a90
Add useERC8000TokenURI
lubej Nov 20, 2025
a210ca8
Display ERC-8004 metadata in AppDetails
lubej Nov 20, 2025
1fd1fe1
Add changelog
lubej Nov 20, 2025
80ef70f
Add additional fields to ERC8004 form
lubej Nov 21, 2025
dd9fb70
Add ERC-8004 step to custom build
lubej Nov 21, 2025
5a21b15
Prefill ERC8004Form with metadata
lubej Nov 21, 2025
ac6e0eb
Update correct step indexes
lubej Nov 21, 2025
c5a216b
Fix wrong merge in CustomInputsStep
lubej Nov 21, 2025
783d2fa
Fix environment mapping
lubej Nov 21, 2025
e09f899
Add additional variables rofl-8004 env
lubej Nov 21, 2025
7e8287b
Add reserved service name validation tp custom build
lubej Nov 21, 2025
f43c811
Remove 0x validation from ERC8004 signing key
lubej Nov 21, 2025
423872a
Remove additional environment variables
lubej Nov 21, 2025
f585149
Remove empty entries in ERC8004 form
lubej Nov 21, 2025
3683592
Add rofl-8004 docker image to constants
lubej Nov 29, 2025
725b960
Add chain dropdown in ERC8004Form
lubej Nov 29, 2025
14d670d
ERC8004Form layout changes
lubej Dec 1, 2025
c15cfa7
Add validator address as an option
lubej Dec 2, 2025
c8c6ee8
Set skip ERC-8004 registration as default
lubej Dec 2, 2025
9396725
Add 8004.scan navigation link
lubej Dec 2, 2025
77b72a8
Add rofl-8004-volume
lubej Jan 8, 2026
e2aa40a
Update rofl-8004 image version
lubej Jan 12, 2026
489cabf
Fix in case there is no instance with ERC8004Token
lubej Jan 12, 2026
6a33ae1
Update RPC url
lubej Jan 12, 2026
94aa606
Update 8004scan URL
lubej Jan 12, 2026
5a2c2cc
Use RPC URL from config
lubej Jan 12, 2026
597eaf1
Address PR feedback
lubej Jan 14, 2026
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
1 change: 1 addition & 0 deletions .changelog/361.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add initial ERC-8004 support
8 changes: 7 additions & 1 deletion src/backend/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { toast } from 'sonner'
import { isMachineRemoved } from '../components/MachineStatusIcon/isMachineRemoved.ts'
import { trackEvent } from 'fathom-client'
import { getOasisAddressBytesFromEvm } from '../utils/helpers.ts'
import { addRofl8004ServiceToCompose, hasRofl8004ServiceSecrets } from '../utils/rofl-8004.ts'

const BACKEND_URL = import.meta.env.VITE_ROFL_APP_BACKEND

Expand Down Expand Up @@ -432,7 +433,12 @@ export function useCreateAndDeployApp() {
const manifest = yaml.stringify(
fillTemplate(roflTemplateYaml, appData.metadata!, build, network, appId, address),
)
const compose = template.yaml.compose

let compose = template.yaml.compose
if (hasRofl8004ServiceSecrets(appData)) {
compose = addRofl8004ServiceToCompose(template.yaml.compose, appData)
}

console.log('Build?')
setCurrentStep('building')

Expand Down
45 changes: 42 additions & 3 deletions src/components/CodeDisplay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,26 @@ const MonacoEditor = lazy(async () => {
try {
const monaco = await import('monaco-editor')
const monacoReact = await import('@monaco-editor/react')

window.MonacoEnvironment = {
getWorker() {
getWorker(_, label) {
// JSON worker
if (label === 'json') {
return new Worker(
new URL(
'../../../node_modules/monaco-editor/esm/vs/language/json/json.worker.js',
import.meta.url,
),
{ type: 'module' },
)
}
return new Worker(
new URL('../../../node_modules/monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url),
{ type: 'module' },
)
},
}

monacoReact.loader.config({ monaco })

return monacoReact
Expand All @@ -43,6 +55,7 @@ type CodeDisplayProps = {
onChange?: (value: string | undefined) => void
// CSS white-space set in index.css for proper placeholder rendering
placeholder?: string
language?: 'yaml' | 'json'
}

export const CodeDisplay: FC<CodeDisplayProps> = ({
Expand All @@ -51,6 +64,7 @@ export const CodeDisplay: FC<CodeDisplayProps> = ({
readOnly = true,
onChange,
placeholder,
language = 'yaml',
}) => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const monacoInstanceRef = useRef<typeof monaco | null>(null)
Expand Down Expand Up @@ -130,6 +144,25 @@ export const CodeDisplay: FC<CodeDisplayProps> = ({
monacoInstance.editor.setModelMarkers(model, 'highlightYamlErrors', markers)
}

const highlightJsonErrors = (newData: string | undefined) => {
const monacoInstance = monacoInstanceRef.current
if (!monacoInstance || !editorRef.current || newData === undefined) {
return
}
const model = editorRef.current.getModel()
if (!model) return

const markers: monaco.editor.IMarkerData[] = []

try {
JSON.parse(newData)
} catch (e) {
console.warn(e)
}

monacoInstance.editor.setModelMarkers(model, 'highlightJsonErrors', markers)
}

const handleEditorDidMount = (
editor: monaco.editor.IStandaloneCodeEditor,
monacoInstance: typeof monaco,
Expand All @@ -140,7 +173,13 @@ export const CodeDisplay: FC<CodeDisplayProps> = ({
}

const handleEditorChange = (newData: string | undefined) => {
if (!readOnly) highlightYamlErrors(newData)
if (!readOnly) {
if (language === 'yaml') {
highlightYamlErrors(newData)
} else if (language === 'json') {
highlightJsonErrors(newData)
}
}

highlightErrorLogs(newData)

Expand All @@ -152,7 +191,7 @@ export const CodeDisplay: FC<CodeDisplayProps> = ({
<Suspense fallback={<TextAreaFallback value={data} />}>
<MonacoEditor
loading={<TextAreaFallback value={data} />}
language="yaml"
language={language}
value={data}
theme="customTheme"
beforeMount={handleEditorWillMount}
Expand Down
34 changes: 31 additions & 3 deletions src/components/InputFormField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { Input } from '@oasisprotocol/ui-library/src/components/ui/input'
import { Textarea } from '@oasisprotocol/ui-library/src/components/ui/textarea'
import { Label } from '@oasisprotocol/ui-library/src/components/ui/label'
import { Button } from '@oasisprotocol/ui-library/src/components/ui/button'
import { Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff, Info } from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from '@oasisprotocol/ui-library/src/components/ui/tooltip'

type InputFormFieldProps<T extends FieldValues> = {
control: Control<T>
Expand All @@ -14,6 +20,7 @@ type InputFormFieldProps<T extends FieldValues> = {
type?: 'input' | 'password' | 'number' | 'textarea'
disabled?: boolean
min?: number
info?: string
}

export const InputFormField = <T extends FieldValues>({
Expand All @@ -24,6 +31,7 @@ export const InputFormField = <T extends FieldValues>({
type = 'input',
disabled = false,
min,
info,
}: InputFormFieldProps<T>): ReactNode => {
const [showPassword, setShowPassword] = useState(false)

Expand All @@ -40,7 +48,21 @@ export const InputFormField = <T extends FieldValues>({

return (
<div className="grid gap-2">
{label && <Label htmlFor={name}>{label}</Label>}
{label && (
<div className="flex items-center gap-1.5">
<Label htmlFor={name}>{label}</Label>
{info && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>{info}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
<Controller
control={control}
name={name}
Expand Down Expand Up @@ -78,7 +100,13 @@ export const InputFormField = <T extends FieldValues>({
)}
</div>
) : (
<Textarea id={name} placeholder={placeholder} {...field} aria-invalid={!!fieldState.error} />
<Textarea
id={name}
placeholder={placeholder}
{...field}
aria-invalid={!!fieldState.error}
disabled={disabled}
/>
)}
{fieldState.error && <div className="text-destructive text-sm">{fieldState.error.message}</div>}
</>
Expand Down
22 changes: 22 additions & 0 deletions src/constants/rofl-8004.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { sepolia } from 'viem/chains'

export const ROFL_8004_DOCKER_IMAGE =
'ghcr.io/oasisprotocol/rofl-8004:latest@sha256:671633f634402e147f90115d1c139ba4d955842276eadf95e29a9034d14a7d49'
export const ROFL_8004_SERVICE_NAME = 'rofl-8004'
export const ROFL_8004_VOLUME_NAME = 'rofl-8004-volume'
export const ROFL_8004_SERVICE_ENV_PREFIX = 'ERC8004'

export const ROFL_8004_METADATA_KEY = 'net.oasis.app.erc8004_agent_id' // "{chainId}:{agentId}"

export const ROFL_8004_SUPPORTED_CHAINS = {
[sepolia.id.toString()]: {
chain: sepolia,
identityContract: '0x8004a6090cd10a7288092483047b097295fb8847',
rpcUrl: ['https://ethereum-sepolia-rpc.publicnode.com'],
validatorAddress: '0x7cd2eccd146b6626c8a500ab5644b1a506115e6f',
},
}

export const ERC_8004_ETHEREUM_EIP_URL = 'https://eips.ethereum.org/EIPS/eip-8004'
export const get8004ScanUrl = (chainName: string, agentId: string) =>
`https://www.8004scan.io/agents/${chainName}/${agentId}`
19 changes: 19 additions & 0 deletions src/contracts/ERC-8004_ABI.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"inputs": [
{ "name": "agentId", "type": "uint256" },
{ "name": "key", "type": "string" }
],
"name": "getMetadata",
"outputs": [{ "name": "", "type": "bytes" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "name": "tokenId", "type": "uint256" }],
"name": "tokenURI",
"outputs": [{ "name": "", "type": "string" }],
"stateMutability": "view",
"type": "function"
}
]
45 changes: 45 additions & 0 deletions src/contracts/erc-8004.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ERC_8004_ABI from './ERC-8004_ABI.json'
import { ROFL_8004_SUPPORTED_CHAINS } from '../constants/rofl-8004.ts'
import { createPublicClient, http } from 'viem'

export const getMetadata = async (
chainId: number | string,
agentId: bigint,
key?: string,
): Promise<`0x${string}`> => {
const supportedNetwork = ROFL_8004_SUPPORTED_CHAINS[chainId.toString()]

try {
return (await createPublicClient({
chain: supportedNetwork.chain,
transport: http(supportedNetwork.rpcUrl[0]),
}).readContract({
address: supportedNetwork.identityContract as `0x${string}`,
abi: ERC_8004_ABI,
functionName: 'getMetadata',
args: [agentId, key],
})) as `0x${string}`
} catch (error) {
console.error(`Error fetching metadata for agent ${agentId}`, error)
throw error
}
}

export const getTokenURI = async (chainId: number | string, agentId: bigint): Promise<string> => {
const supportedNetwork = ROFL_8004_SUPPORTED_CHAINS[chainId.toString()]

try {
return (await createPublicClient({
chain: supportedNetwork.chain,
transport: http(supportedNetwork.rpcUrl[0]),
}).readContract({
address: supportedNetwork.identityContract as `0x${string}`,
abi: ERC_8004_ABI,
functionName: 'tokenURI',
args: [agentId],
})) as string
} catch (error) {
console.error(`Error fetching tokenURI for agent ${agentId}:`, error)
throw error
}
}
51 changes: 51 additions & 0 deletions src/hooks/useERC8004TokenURI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useState } from 'react'
import { getTokenURI } from '../contracts/erc-8004.ts'

interface UseERC8004TokenURIReturnType {
data: string | null
isLoading: boolean
error: Error | null
refetch: () => void
}

export function useERC8004TokenURI(
chainId: number | string,
agentId: bigint | undefined,
enabled = true,
): UseERC8004TokenURIReturnType {
const [data, setData] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)

const fetchTokenURI = async () => {
if (!agentId) return

setIsLoading(true)
setError(null)

try {
const result = await getTokenURI(chainId, agentId)

setData(result)
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch token URI'))
console.error('Error fetching ERC-8004 token URI:', err)
} finally {
setIsLoading(false)
}
}

useEffect(() => {
if (enabled && chainId && agentId) {
fetchTokenURI()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agentId, chainId && enabled])

return {
data,
isLoading,
error,
refetch: fetchTokenURI,
}
}
2 changes: 1 addition & 1 deletion src/pages/CreateApp/BuildStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const BuildStep: FC<BuildStepProps> = ({

return (
<CreateLayout
currentStep={3}
currentStep={4}
selectedTemplateName={selectedTemplateName}
selectedTemplateId={selectedTemplateId}
customStepTitle={customStepTitle}
Expand Down
8 changes: 6 additions & 2 deletions src/pages/CreateApp/CreateLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ export const CreateLayout: FC<CreateLayoutProps> = ({
const sidebarItems = [
{ label: 'Input Metadata', active: currentStep === 1 },
{ label: customStepTitle, active: currentStep === 2 },
{ label: 'Configure Machine', active: currentStep === 3 },
{ label: 'Payment', active: currentStep === 4 },
{ label: 'Setup ERC-8004', active: currentStep === 3 },
{ label: 'Configure Machine', active: currentStep === 4 },
{
label: 'Payment',
active: currentStep === 5,
},
]

return (
Expand Down
Loading