Skip to content
Merged
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
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ RUN npm ci
COPY index.html vite.config.ts tsconfig.json tsconfig.node.json components.json ./
COPY src ./src
COPY create ./create
COPY public ./public
COPY scripts ./scripts
COPY signer-static ./signer-static

Expand Down
2 changes: 1 addition & 1 deletion create/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
<div id="app">
<div class="header">
<img
src="/assets/1Shot-Icon-New.svg"
src="../src/assets/1Shot-Icon-New.svg"
alt=""
width="40"
height="40"
Expand Down
1 change: 1 addition & 0 deletions host/scripts/dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ try {

const https = resolveHttpsOptions({
certsDir: path.join(__dirname, "../certs"),
pathBase: repoRoot,
});
const scheme = https ? "https" : "http";
const address = viteServer.httpServer?.address();
Expand Down
27 changes: 18 additions & 9 deletions host/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ export function App() {
}
proxyRef.current = proxy;

proxy.analytics.on((event) => {
(
proxy as OWSProxy & {
analytics?: {
on: (handler: (event: IOWSAnalyticsEvent) => void) => void;
};
}
).analytics?.on((event) => {
console.info("[oneshot-wallet-host] analytics", event.name, event);
setAnalyticsEvents((prev) =>
[event, ...prev].slice(0, MAX_ANALYTICS_EVENTS),
Expand All @@ -106,7 +112,9 @@ export function App() {
reportStatus(
mode === "design"
? `Design preview connected on ${connectedChain}. Apply setStyle to refresh.`
: `Wallet connected on ${connectedChain}. Enter a message and click Sign.`,
: mode === "analytics"
? "Live analytics — switch to Test to generate events."
: `Wallet connected on ${connectedChain}. Enter a message and click Sign.`,
);
} catch (error) {
reportStatus(
Expand Down Expand Up @@ -171,19 +179,20 @@ export function App() {
<AppHeader />
{mode === "test" ? (
<TestPanel {...walletActionProps} />
) : (
) : mode === "design" ? (
<DesignPanel
ready={ready}
onApplyStyle={handleApplyStyle}
previewMountRef={setPreviewMount}
/>
) : (
<div className="mx-auto w-full max-w-2xl px-6 pb-6">
<AnalyticsPanel
events={analyticsEvents}
onClear={() => setAnalyticsEvents([])}
/>
</div>
)}
<div className="mx-auto w-full max-w-2xl px-6 pb-6">
<AnalyticsPanel
events={analyticsEvents}
onClear={() => setAnalyticsEvents([])}
/>
</div>
</SidebarInset>
{/* Flyout create() target — never reparented; Test mode only. */}
<div ref={flyoutContainerRef} aria-hidden="true" />
Expand Down
14 changes: 11 additions & 3 deletions host/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FlaskConicalIcon, PaletteIcon } from "lucide-react";
import { BarChart3Icon, FlaskConicalIcon, PaletteIcon } from "lucide-react";
import {
Sidebar,
SidebarContent,
Expand All @@ -12,7 +12,7 @@ import {
SidebarRail,
} from "@/components/ui/sidebar";

export type HostMode = "test" | "design";
export type HostMode = "test" | "design" | "analytics";

export interface IAppSidebarProps {
mode: HostMode;
Expand All @@ -37,6 +37,12 @@ const NAV_ITEMS: {
description: "Live setStyle playground",
icon: PaletteIcon,
},
{
mode: "analytics",
label: "Analytics",
description: "Live proxy.analytics event stream",
icon: BarChart3Icon,
},
];

export function AppSidebar({ mode, onModeChange }: IAppSidebarProps) {
Expand Down Expand Up @@ -73,7 +79,9 @@ export function AppSidebar({ mode, onModeChange }: IAppSidebarProps) {
<p className="text-muted-foreground px-2 text-xs leading-relaxed">
{mode === "test"
? "Wallet stays hidden. Use actions below to exercise EIP-1193."
: "Configurator on the left; wallet flyout stays open on the right."}
: mode === "design"
? "Configurator on the left; wallet flyout stays open on the right."
: "Events accumulate across tabs. Switch to Test to generate traffic."}
</p>
</SidebarGroupContent>
</SidebarGroup>
Expand Down
4 changes: 2 additions & 2 deletions host/src/components/TestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export function TestPanel(props: IWalletActionsProps) {
<CardHeader>
<CardTitle className="font-heading text-xl">Test</CardTitle>
<CardDescription>
Exercise connect, personal_sign, chain switch, ERC-20 reads, and EIP-7715
delegations.
Exercise connect, personal_sign, eth_signTypedData_v4, chain switch, ERC-20
reads, and EIP-7715 delegations.
The wallet flyout stays hidden until an action needs it.
</CardDescription>
</CardHeader>
Expand Down
61 changes: 53 additions & 8 deletions host/src/components/WalletActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import type { SignMode } from "@/constants/signDemo";
import {
HOST_CHAINS,
hostChainMeta,
type UsdcMode,
} from "./hostChains";

export type { UsdcMode } from "./hostChains";
export type { SignMode } from "@/constants/signDemo";

export interface IWalletActionsProps {
ready: boolean;
busy: boolean;
chainId: string;
message: string;
signMode: SignMode;
typedDataJson: string;
usdcMode: UsdcMode;
usdcDestination: string;
usdcAmount: string;
Expand All @@ -35,6 +40,8 @@ export interface IWalletActionsProps {
onChainChange: (chainId: string) => void;
onRefreshChain: () => void;
onMessageChange: (message: string) => void;
onSignModeChange: (mode: SignMode) => void;
onTypedDataJsonChange: (json: string) => void;
onUsdcModeChange: (mode: UsdcMode) => void;
onUsdcDestinationChange: (address: string) => void;
onUsdcAmountChange: (amount: string) => void;
Expand Down Expand Up @@ -65,6 +72,8 @@ export function WalletActions({
busy,
chainId,
message,
signMode,
typedDataJson,
usdcMode,
usdcDestination,
usdcAmount,
Expand All @@ -77,6 +86,8 @@ export function WalletActions({
onChainChange,
onRefreshChain,
onMessageChange,
onSignModeChange,
onTypedDataJsonChange,
onUsdcModeChange,
onUsdcDestinationChange,
onUsdcAmountChange,
Expand Down Expand Up @@ -140,14 +151,48 @@ export function WalletActions({
</div>

<div className="flex flex-col gap-1.5">
<Label htmlFor="message-input">Message to sign (EIP-191)</Label>
<Textarea
id="message-input"
rows={3}
value={message}
disabled={!ready}
onChange={(event) => onMessageChange(event.target.value)}
/>
<Tabs
value={signMode}
onValueChange={(value) => {
if (value === "message" || value === "typedData") {
onSignModeChange(value);
}
}}
>
<TabsList className="w-full">
<TabsTrigger value="message" className="flex-1">
Message
</TabsTrigger>
<TabsTrigger value="typedData" className="flex-1">
Typed Data (EIP-712)
</TabsTrigger>
</TabsList>
</Tabs>
{signMode === "message" ? (
<>
<Label htmlFor="message-input">Message to sign</Label>
<Textarea
id="message-input"
rows={3}
value={message}
disabled={!ready}
onChange={(event) => onMessageChange(event.target.value)}
/>
</>
) : (
<>
<Label htmlFor="typed-data-input">Typed Data JSON</Label>
<Textarea
id="typed-data-input"
rows={3}
className="field-sizing-fixed resize-y overflow-auto font-mono text-xs"
spellCheck={false}
value={typedDataJson}
disabled={!ready}
onChange={(event) => onTypedDataJsonChange(event.target.value)}
/>
</>
)}
</div>

<div className="flex flex-wrap gap-2">
Expand Down
30 changes: 30 additions & 0 deletions host/src/components/WalletConfiguratorTextTabSigningSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,36 @@ export function WalletConfiguratorTextTabSigningSections({
value={form.passkeyCancel}
onChange={(value) => patch("passkeyCancel", value)}
/>
<TextField
id="passkey-terms-prefix"
label="Terms acceptance prefix"
value={form.passkeyTermsPrefix}
onChange={(value) => patch("passkeyTermsPrefix", value)}
/>
<TextField
id="passkey-terms-of-service"
label="Terms of Service label"
value={form.passkeyTermsOfServiceLabel}
onChange={(value) => patch("passkeyTermsOfServiceLabel", value)}
/>
<TextField
id="passkey-terms-joiner"
label="Terms acceptance joiner"
value={form.passkeyTermsJoiner}
onChange={(value) => patch("passkeyTermsJoiner", value)}
/>
<TextField
id="passkey-privacy-label"
label="Privacy Policy label"
value={form.passkeyPrivacyLabel}
onChange={(value) => patch("passkeyPrivacyLabel", value)}
/>
<TextField
id="passkey-terms-error"
label="Terms acceptance error"
value={form.passkeyTermsError}
onChange={(value) => patch("passkeyTermsError", value)}
/>
</AccordionContent>
</AccordionItem>

Expand Down
58 changes: 58 additions & 0 deletions host/src/constants/signDemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export type SignMode = "message" | "typedData";

export const DEFAULT_EIP712_TYPED_DATA = {
domain: {
name: "Ether Mail",
version: "1",
chainId: 421614,
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
},
types: {
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" },
],
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person" },
{ name: "contents", type: "string" },
],
},
primaryType: "Mail",
message: {
from: {
name: "Cow",
wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
},
to: {
name: "Bob",
wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
},
contents: "Hello, Bob!",
},
} as const;

export const DEFAULT_TYPED_DATA_JSON = JSON.stringify(
DEFAULT_EIP712_TYPED_DATA,
null,
2,
);

export function parseTypedDataJson(json: string): Record<string, unknown> {
let parsed: unknown;
try {
parsed = JSON.parse(json);
} catch {
throw new Error("Invalid typed data JSON.");
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Typed data must be a JSON object.");
}
const record = parsed as Record<string, unknown>;
for (const key of ["domain", "types", "primaryType", "message"] as const) {
if (!(key in record)) {
throw new Error(`Typed data is missing required field "${key}".`);
}
}
return record;
}
Loading