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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ This app uses Vercel Web Analytics. Two things must stay in place:
| `trackExplorerActiveBlockJump(chain, jump)` | `app/internal-explorer/components/ActiveBlockButton.tsx` — zeronet latest/previous active block |
| `trackValidityOrder(side, status)` | `app/vibenet/demos/validity/ValidityDemo.tsx` — conditional swap submit / include / expiry / replace |
| `trackValidityRace(attempt, status)` | `app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx` — validity/manual comparison attempts |
| `trackValidityLiquidation(attempt, status)` | `app/vibenet/demos/validity/liquidations/LiquidationsDemo.tsx` — validity/manual liquidation attempts |

Add a helper (and a row here) for a new key journey; remove the helper if you
remove its surface. Confirm the wiring with `grep -rn "analytics/events" app`.
Expand Down
7 changes: 7 additions & 0 deletions app/analytics/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,10 @@ export function trackValidityRace(
): void {
track('validity_race', { attempt, status });
}

export function trackValidityLiquidation(
attempt: 'validity' | 'manual',
status: 'submitted' | 'success' | 'reverted' | 'expired' | 'beaten' | 'error',
): void {
track('validity_liquidation', { attempt, status });
}
3 changes: 2 additions & 1 deletion app/sitemap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { describe, expect, it } from 'vitest';
import sitemap from './sitemap';

describe('sitemap', () => {
it('indexes the Validity Transactions group and both nested demos', () => {
it('indexes the Validity Transactions group and every nested demo', () => {
const urls = sitemap().map((entry) => entry.url);

expect(urls).toContain('https://chain.base.org/vibenet/demos/validity');
expect(urls).toContain('https://chain.base.org/vibenet/demos/validity/conditional-swaps');
expect(urls).toContain('https://chain.base.org/vibenet/demos/validity/race-the-agent');
expect(urls).toContain('https://chain.base.org/vibenet/demos/validity/liquidations');
});
});
1 change: 1 addition & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ path: '/vibenet/demos/validity', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity/conditional-swaps', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity/race-the-agent', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity/liquidations', priority: 0.5, changeFrequency: 'weekly' },
];

return routes.map(({ path, priority, changeFrequency }) => ({
Expand Down
12 changes: 12 additions & 0 deletions app/vibenet/demos/catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ export const DEMOS: DemoEntry[] = [
],
available: true,
},
{
href: '/vibenet/demos/validity/liquidations',
title: 'Liquidations',
summary:
'Liquidate loans on a demo lending market before a rival keeper, with a validity transaction that waits for the price to cross.',
points: [
'Bound a pool reserve so the transaction lands only once a loan is under water',
'Pin the position in storage so a lost race is dropped, not reverted',
'Compare inclusion blocks against a conventional keeper bot',
],
available: true,
},
],
},
{
Expand Down
28 changes: 28 additions & 0 deletions app/vibenet/demos/validity/components/CodeSnippet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';

import { tokenizeCode } from './CodeSnippet';

const kinds = (source: string, types?: string[]) =>
tokenizeCode(source, 'solidity', types).filter((t) => t.kind !== 'plain').map((t) => [t.text, t.kind]);

describe('tokenizeCode', () => {
it('round-trips the source text', () => {
const source = 'function liquidate(address borrower) external { // go\n delete positions[borrower];\n}';
expect(tokenizeCode(source, 'solidity').map((t) => t.text).join('')).toBe(source);
});

it('colours caller-supplied types but not identifiers that merely contain them', () => {
expect(kinds('Position memory position = Positions[x];', ['Position'])).toEqual([
['Position', 'type'],
['memory', 'keyword'],
]);
});

it('keeps keywords inside comments and strings as part of them', () => {
expect(kinds('require(x, "no position"); // return 10_000')).toEqual([
['require', 'keyword'],
['"no position"', 'string'],
['// return 10_000', 'comment'],
]);
});
});
93 changes: 93 additions & 0 deletions app/vibenet/demos/validity/components/CodeSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Card } from '../../../../components/ui/Card';
import { cn } from '../../../../components/ui/cn';
import { Text } from '../../../../components/ui/Text';

export type CodeLanguage = 'json' | 'solidity';
type CodeToken = { text: string; kind: 'comment' | 'key' | 'keyword' | 'literal' | 'number' | 'plain' | 'string' | 'type' };

const CODE_TOKEN_CLASS: Record<CodeToken['kind'], string> = {
comment: 'text-bds-gray-50 dark:text-[#7f8c98]',
key: 'text-base-blue dark:text-[#7eb8ff]',
keyword: 'text-purple-700 dark:text-[#c792ea]',
literal: 'text-bds-orange-70 dark:text-[#ff9d76]',
number: 'text-bds-orange-70 dark:text-[#f5c542]',
plain: 'text-bds-gray-80 dark:text-[#d6deeb]',
string: 'text-bds-green-70 dark:text-[#7ee0a8]',
type: 'text-base-blue dark:text-[#82aaff]',
};

const JSON_PATTERN = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?|[{}[\]:,])/g;

const SOLIDITY_KEYWORDS = [
'address', 'bool', 'constant', 'contract', 'delete', 'emit', 'event', 'external', 'function', 'if',
'immutable', 'interface', 'internal', 'mapping', 'memory', 'private', 'public', 'require', 'return',
'returns', 'revert', 'storage', 'struct', 'uint112', 'uint128', 'uint256', 'view',
];

/**
* Tokenize a JSON or Solidity snippet for display. `types` lists identifiers
* (contracts, structs, interfaces) to colour as types in Solidity.
*/
export function tokenizeCode(source: string, language: CodeLanguage, types: readonly string[] = []): CodeToken[] {
const typeSet = new Set(types);
const pattern = language === 'json'
? JSON_PATTERN
: new RegExp(
[
String.raw`\/\/[^\n]*`,
String.raw`"(?:\\.|[^"\\])*"`,
String.raw`\b(?:${SOLIDITY_KEYWORDS.join('|')})\b`,
...(types.length > 0 ? [String.raw`\b(?:${types.join('|')})\b`] : []),
String.raw`\b\d[\d_]*(?:\s+ether)?\b`,
].join('|'),
'g',
);
const tokens: CodeToken[] = [];
let last = 0;
for (const hit of source.matchAll(pattern)) {
const text = hit[0];
const index = hit.index ?? 0;
if (index > last) tokens.push({ text: source.slice(last, index), kind: 'plain' });
let kind: CodeToken['kind'] = 'plain';
if (language === 'json') {
if (text.startsWith('"')) kind = text.endsWith(':') ? 'key' : 'string';
else if (text === 'true' || text === 'false' || text === 'null') kind = 'literal';
else if (/^-?\d/.test(text)) kind = 'number';
} else if (text.startsWith('//')) kind = 'comment';
else if (text.startsWith('"')) kind = 'string';
else if (/^\d/.test(text)) kind = 'number';
else if (typeSet.has(text)) kind = 'type';
else kind = 'keyword';
tokens.push({ text, kind });
last = index + text.length;
}
if (last < source.length) tokens.push({ text: source.slice(last), kind: 'plain' });
return tokens;
}

export function CodeSnippet({
label,
code,
language,
types,
className,
}: {
label: string;
code: string;
language: CodeLanguage;
types?: readonly string[];
className?: string;
}) {
return (
<Card className="min-w-0 overflow-hidden bg-background p-4 dark:bg-white/[.04]">
<Text variant="caption" tone="muted">{label}</Text>
<pre className={cn('mt-3 overflow-auto rounded-xl bg-bds-gray-5 p-4 font-mono text-[11px] leading-5 dark:bg-[#0b0d12]', className)}>
<code>
{tokenizeCode(code, language, types).map((token, index) => (
<span key={`${index}-${token.text}`} className={CODE_TOKEN_CLASS[token.kind]}>{token.text}</span>
))}
</code>
</pre>
</Card>
);
}
Loading
Loading