| name | blueprint-frontend |
| description | Build Tangle Blueprint flows for jobs, operators, provisioning, auth, and shared UI. |
Blueprint Frontend
Use this skill when building React frontends for Tangle Network blueprints. Covers the shared UI library (@tangle-network/blueprint-ui), on-chain interaction patterns, session auth, and Web3 provider setup.
For sandbox-specific frontend patterns (agent chat, terminal, sidecar auth), see sandbox-blueprint.
What This Skill Covers
- Registering blueprints and defining job metadata for UI rendering
- On-chain job submission, operator discovery, and RFQ pricing
- Service validation and provision progress tracking
- Session auth (EIP-191 challenge + PASETO tokens)
- Web3 provider setup (wagmi, viem, ConnectKit)
- Theme and styling with blueprint-ui presets
- Form components and job execution dialogs
- Chain configuration and network switching
@tangle-network/blueprint-ui
Chain/contract interaction, job forms, stores, layout primitives. App-agnostic — no product-specific routing or copy.
Source: tangle-network/blueprint-ui
Three export entry points:
@tangle-network/blueprint-ui — hooks, stores, contracts, utilities
@tangle-network/blueprint-ui/components — UI components
@tangle-network/blueprint-ui/preset — UnoCSS theme tokens
Blueprint Registration
Register blueprints at app startup to enable generic job forms and submission:
import { registerBlueprint, type BlueprintDefinition, type JobDefinition } from '@tangle-network/blueprint-ui';
const MY_JOBS: JobDefinition[] = [
{
id: 0,
name: 'create_instance',
label: 'Create Instance',
description: 'Provision a new sandbox instance',
category: 'lifecycle',
icon: 'i-ph:play',
pricingMultiplier: 1.0,
requiresSandbox: false,
fields: [
{
name: 'name',
label: 'Instance Name',
type: 'text',
required: true,
abiType: 'string',
abiParam: 'name',
},
{
name: 'cpu_cores',
label: 'CPU Cores',
type: 'number',
defaultValue: 2,
min: 1,
max: 16,
abiType: 'uint64',
abiParam: 'cpu_cores',
},
{
name: 'runtime_backend',
label: 'Runtime',
type: 'select',
defaultValue: 'docker',
options: [
{ label: 'Docker', value: 'docker' },
{ label: 'Firecracker', value: 'firecracker' },
],
abiType: 'string',
abiParam: 'runtime_backend',
},
],
},
];
const MY_BLUEPRINT: BlueprintDefinition = {
id: 'my-blueprint',
name: 'My Blueprint',
version: '1.0.0',
description: 'Description',
icon: 'i-ph:cube',
color: '#3B82F6',
contracts: {
31337: '0x...',
3799: '0x...',
},
jobs: MY_JOBS,
categories: [
{ key: 'lifecycle', label: 'Lifecycle', icon: 'i-ph:arrows-clockwise' },
],
};
registerBlueprint(MY_BLUEPRINT);
Key Types
type JobCategory = 'lifecycle' | 'execution' | 'batch' | 'workflow' | 'ssh' | 'management';
interface JobFieldDef {
name: string;
label: string;
type: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'json' | 'combobox';
required?: boolean;
defaultValue?: string | number | boolean;
options?: { label: string; value: string }[];
helperText?: string;
min?: number; max?: number; step?: number;
abiType?: string;
abiParam?: string;
internal?: boolean;
}
interface JobDefinition {
id: ;
: ;
: ;
: ;
: ;
: ;
: ;
: [];
: ;
?: ;
?: [];
?: ;
}
On-Chain Job Submission
Flow
useOperators() → discover operators for blueprint
↓
useQuotes() → fetch RFQ pricing from operators
↓
useJobForm() → manage form state from JobDefinition
↓
encodeJobArgs() → ABI-encode form values using field metadata
↓
useSubmitJob() → submit on-chain, track TX lifecycle
↓
useProvisionProgress() → poll provision status until ready
useSubmitJob
import { useSubmitJob } from '@tangle-network/blueprint-ui';
const { submitJob, status, txHash, callId, error, reset } = useSubmitJob();
await submitJob({
serviceId: 1n,
jobId: 0,
args: encodeJobArgs(job, formValues, context),
label: 'Create Instance',
value: quotedPrice,
});
useOperators
import { useOperators } from '@tangle-network/blueprint-ui';
const { operators, operatorCount, isLoading, error } = useOperators();
useQuotes (RFQ Pricing)
import { useQuotes, formatCost } from '@tangle-network/blueprint-ui';
const { quotes, totalCost, isLoading, isSolvingPow } = useQuotes(
operators,
blueprintId,
ttlBlocks,
enabled,
);
useJobPrice (Per-Job Pricing)
import { useJobPrice } from '@tangle-network/blueprint-ui';
const { quote, formattedPrice, isLoading } = useJobPrice(
operatorRpcUrl,
serviceId,
jobIndex,
blueprintId,
enabled,
);
encodeJobArgs
import { encodeJobArgs } from '@tangle-network/blueprint-ui';
const encoded = encodeJobArgs(jobDefinition, formValues, {
sidecar_url: 'http://...',
sandbox_id: '0x...',
});
Service Validation
import { useServiceValidation } from '@tangle-network/blueprint-ui';
const { validate, serviceInfo, isValidating, error } = useServiceValidation();
const info = await validate(serviceId, userAddress);
Provision Progress
import { useProvisionProgress, getPhaseLabel } from '@tangle-network/blueprint-ui';
const { phase, progressPct, sandboxId, sidecarUrl, isReady, isFailed, message } =
useProvisionProgress(callId, operatorRpcUrl, enabled);
Session Auth (EIP-191 + PASETO)
import { useSessionAuth, useAuthenticatedFetch } from '@tangle-network/blueprint-ui';
const { session, isAuthenticated, authenticate, logout } = useSessionAuth(sandboxId, operatorUrl);
const { authFetch } = useAuthenticatedFetch(sandboxId, operatorUrl);
const res = await authFetch('/api/instances');
Stores
infraStore (Blueprint/Service Selection)
import { infraStore, updateInfra, getInfra } from '@tangle-network/blueprint-ui';
updateInfra({ blueprintId: '1', serviceId: '1' });
const { blueprintId, serviceId, serviceInfo } = getInfra();
txListStore (Transaction History)
import { txListStore, addTx, updateTx, pendingCount } from '@tangle-network/blueprint-ui';
addTx({ hash, label: 'Create Instance', status: 'pending', chainId });
sessionMapStore (PASETO Sessions)
import { getSession, setSession, removeSession, gcSessions } from '@tangle-network/blueprint-ui';
const session = getSession(sandboxId);
Web3 Provider Setup
import { Web3Shell } from '@tangle-network/blueprint-ui/components';
import { tangleWalletChains, createTangleTransports, defaultConnectKitOptions } from '@tangle-network/blueprint-ui';
import { createConfig, WagmiProvider } from 'wagmi';
import { getDefaultConfig } from 'connectkit';
const config = createConfig(
getDefaultConfig({
chains: tangleWalletChains,
transports: createTangleTransports(),
walletConnectProjectId: import.meta.env.VITE_WALLETCONNECT_PROJECT_ID,
...defaultConnectKitOptions,
}),
);
<Web3Shell config={config}>
<App />
</Web3Shell>
Chain Configuration
import {
tangleLocal, tangleTestnet, tangleMainnet,
configureNetworks, getNetworks, resolveRpcUrl,
selectedChainIdStore, getPublicClient, getAddresses,
} from '@tangle-network/blueprint-ui';
configureNetworks([
{ chain: tangleLocal, rpcUrl: resolveRpcUrl(), label: 'Local', addresses: { jobs: '0x...', services: '0x...' } },
{ chain: tangleTestnet, rpcUrl: 'https://testnet-rpc.tangle.tools', label: 'Testnet', addresses: { ... } },
]);
selectedChainIdStore.set(3799);
const client = getPublicClient();
const { jobs, services } = getAddresses();
Layout Components
import {
AppDocument, Web3Shell, ChainSwitcher, ThemeToggle, AppToaster, AnimatedPage,
} from '@tangle-network/blueprint-ui/components';
Form Components
import { BlueprintJobForm, JobExecutionDialog } from '@tangle-network/blueprint-ui/components';
import { useJobForm } from '@tangle-network/blueprint-ui';
const { values, errors, onChange, validate } = useJobForm(jobDefinition);
<BlueprintJobForm job={jobDefinition} values={values} onChange={onChange} errors={errors} />
<JobExecutionDialog
open={open}
onOpenChange={setOpen}
job={jobDefinition}
serviceId={serviceId}
context={{ sandbox_id: '0x...' }}
onSuccess={(callId) => watchProvision(callId)}
/>
Theme & Styling
import { bpThemeTokens } from '@tangle-network/blueprint-ui/preset';
export default defineConfig({
theme: {
colors: {
bp: bpThemeTokens('myapp'),
},
},
});
Environment Variables
| Variable | Purpose |
|---|
VITE_CHAIN_ID | Default chain (31337 local, 3799 testnet, 5845 mainnet) |
VITE_RPC_URL | RPC endpoint (defaults to localhost:8545) |
VITE_BLUEPRINT_ID | Default blueprint ID |
VITE_SERVICE_ID | Default service ID |
VITE_OPERATOR_API_URL | Operator API endpoint |
VITE_WALLETCONNECT_PROJECT_ID | WalletConnect project ID |
VITE_OPERATOR_API_TOKEN | Operator bearer token (dev) |
On-Chain vs Off-Chain Split
On-chain (state-changing, via useSubmitJob):
- Create/delete sandbox instances
- Create/trigger/cancel workflows
- Service request/approve
Off-chain (operator HTTP API, via useAuthenticatedFetch):
- exec, prompt, task, stop, resume
- SSH, terminal, secrets
- Snapshot, health checks
- Instance status queries
Jobs mutate state. Everything else goes through the operator API.
Where Operators Come From (Production Deploy)
The operators your frontend discovers via useOperators are not someone hand-running a blueprint's instance binary. That matters for how you reason about availability and the service lifecycle.
Running a blueprint's operator/instance binary directly (e.g. <binary> run with a hardcoded SERVICE_ID / TEST_MODE=true) is for LOCAL TESTING ONLY. In production each operator box runs the Blueprint Manager daemon:
cargo-tangle blueprint run -t --pretty \
--http-rpc-url <HTTP_RPC> --ws-rpc-url <WS_RPC> \
--keystore-uri <KEYSTORE> --data-dir <DATA>/bpm-data \
--chain <testnet|mainnet> --protocol tangle
The manager watches the chain and spawns the per-service instance itself when a service request is approved — you never ExecStart / hand-run the instance binary in production. This is the on-chain lifecycle your UI drives end to end: deploy the manager → operator registers for the blueprint → a user requests a service (your service request/approve flow) selecting registered operators → operators approve → the manager spawns the instance with the assigned service id, at which point useProvisionProgress reports it ready. Reference that does it right: ai-trading-blueprint/deploy/go-live.sh + trading-blueprint.service (ExecStart is cargo-tangle blueprint run, not the validator binary).
Critical Files
src/index.ts — main exports (hooks, stores, contracts, utils)
src/components.ts — component exports
src/preset.ts — UnoCSS theme tokens
src/hooks/useSubmitJob.ts — job submission
src/hooks/useOperators.ts — operator discovery
src/hooks/useQuotes.ts — RFQ pricing with PoW
src/hooks/useJobPrice.ts — per-job pricing
src/hooks/useServiceValidation.ts — service validation
src/hooks/useSessionAuth.ts — PASETO session management
src/hooks/useProvisionProgress.ts — provision tracking
src/hooks/useJobForm.ts — form state management
src/blueprints/registry.ts — blueprint registration
src/contracts/abi.ts — Tangle contract ABIs
src/contracts/chains.ts — chain definitions
src/contracts/publicClient.ts — reactive public client
src/contracts/generic-encoder.ts — ABI argument encoding
src/stores/ — infraStore, sessionMapStore, txListStore, themeStore
src/components/forms/BlueprintJobForm.tsx — job form renderer
src/components/forms/JobExecutionDialog.tsx — complete submission dialog
Rules
- Jobs are mutations only. Reads and operational I/O use
eth_call and operator HTTP API.
- blueprint-ui is app-agnostic. No product-specific routing, copy, or feature orchestration.
- Keep product-specific glue in app-local code. Don't duplicate shared primitives locally.
- Always use
encodeJobArgs for ABI encoding. Don't hand-roll encoding from form values.
- Pre-estimate gas before submission. Bypasses MetaMask RPC issues on Tangle chains.
- Session tokens are sandboxId-scoped. Don't reuse tokens across sandboxes.
- Auto-clean expired sessions. Use
gcSessions() or rely on sessionMapStore's built-in cleanup.