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
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ Every keyword links to its chapter of [The Dash Platform Book](https://dashpay.g
- **Property constraints** get their own compartment, rendered as formulas: `rewardSplit.leader + rewardSplit.equal + rewardSplit.actions = 100`.
- **Contract metadata** (top-left): id, owner, version, timestamps, keywords, description, config, **moderation** (lists, who moderates, election windows, moderated types), `schemaDefs`, and every type's chips.

## How a document type is stored (GroveDB layout)

Select a document type and click **GroveDB layout** in the inspector to see every tree and
element Drive writes for it, under `[64, contract id, 1, <type>]`: the documents by id (with
their revisions when the type keeps history) and, for each index, the property and value
trees down to the `[0]` where the index ends. Each layer shows:

- its key (a fixed key, or `‹…›` for one key per document or value; a time window level
shows its grid, `‹$createdAt window: 1d every 1h›`);
- its element, coloured by what it totals: a plain tree, a count, sum or count-and-sum tree
(provable or not), a ranked indexed tree with its axes, or a value / reference;
- the wrapper a continuation tree gets under a counting or summing value tree
(`NonCounted`, `NotSummed`, `NotCountedOrSummed`);
- the indexes that use it, and conditions: the tree a unique index falls back to when a
value is null, `nullSearchable`, `skipIfAbsent`, preallocated indexes, time window overlap;
- a `↗` link to that kind of layer in the
[GroveDB structure viewer](https://dashpay.github.io/grovedb-structure-viewer/), which holds
the general description (element flags, Merk shapes, ...).

The layout is computed by Drive's own rules (`documentTypeLayout` in `@dashevo/evo-sdk`,
[dashpay/platform#5153](https://github.com/dashpay/platform/pull/5153)), held to what Drive
writes by a Drive test, and needs no network. It needs an SDK release that includes it; until
the dependency is bumped the panel says so.

## Compare two versions (contract updates)

**Compare** in the toolbar diagrams two versions of a contract at once: the union of both,
Expand Down Expand Up @@ -97,8 +121,10 @@ and a file (which usually leaves defaults out) compare equal.

## Examples

Bundled, offline, and each one passes full validation by the protocol version 14 parser
(`DataContract.fromJSON(json, true, 14)` from `@dashevo/evo-sdk`), checked in CI:
Bundled, offline, and each one passes the protocol version 14 contract parser as
`@dashevo/evo-sdk` compiles it (`DataContract.fromJSON(json, true, 14)`), checked in CI.
The SDK is built without dpp's `validation` feature, so this does not run the document
meta-schema or the parser checks gated behind that feature:

| Key | What it shows |
|---|---|
Expand Down Expand Up @@ -160,7 +186,9 @@ src/
github.ts # a PR's changed JSON files at merge base and head (GitHub REST API)
sdk/
client.ts # Evo SDK trusted connection, memoised per network
contract.ts # fetch contract (toJSON) -> ContractModel, or a bundled example
contract.ts # any source (id, link, example) -> contract JSON -> ContractModel
layout.ts # documentTypeLayout through the SDK (feature-detected)
pool.ts # pooled connections, resettable without loading the SDK
examples/ # bundled example contracts + registry
model/
introspect.ts # document schemas -> ContractModel (fields, indexes, keywords)
Expand All @@ -169,6 +197,7 @@ src/
constraints.ts # propertyConstraints -> formulas
describe.ts # keyword chips and plain-language descriptions
diff.ts # compare two versions: changes, statuses, the union model
layout.ts # a document type's GroveDB layout (from the SDK) and its display helpers
jsonTokens.ts # JSON syntax highlighting tokens for the inspector
updateRules.ts # each keyword's update rule, per the book, with its error code
types.ts
Expand All @@ -181,7 +210,7 @@ src/
Canvas.tsx # React Flow canvas, legend, export/re-layout panel
selection.ts, exportImage.ts
components/ # Toolbar, InspectorPanel, ContractMetaPanel, PasteContractModal,
# CompareModal, ChangesPanel
# CompareModal, ChangesPanel, LayoutPanel
App.tsx, main.tsx, styles.css
scripts/
validate-examples.mjs # examples through DataContract.fromJSON(…, true, 14)
Expand Down
4 changes: 3 additions & 1 deletion scripts/validate-examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
// npm run validate:examples -- --write # regenerate src/model/fixtures/sdk-references.json
//
// DataContract.fromJSON(json, fullValidation = true, 14) runs the protocol
// version 14 meta-schema and parser (try_from_schema) from @dashevo/evo-sdk.
// version 14 parser (try_from_schema) as @dashevo/evo-sdk compiles it. The SDK
// is built without dpp's `validation` feature, so the document meta-schema and
// the parser checks gated behind that feature do not run here.

import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
Expand Down
37 changes: 31 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useEdgesState, useNodesState, type Edge } from '@xyflow/react';
import { loadConfig, saveOverride, type Network, type ViewKind } from './config';
import { loadContractJson, loadContractModel } from './sdk/contract';
import { loadContractJson } from './sdk/contract';
import { resetConnections } from './sdk/pool';
import { modelFromPastedJson } from './model/introspect';
import { withRelationships } from './model/relationships';
Expand All @@ -15,6 +15,7 @@ import { InspectorPanel } from './components/InspectorPanel';
import { PasteContractModal } from './components/PasteContractModal';
import { ContractMetaPanel } from './components/ContractMetaPanel';
import { ChangesPanel } from './components/ChangesPanel';
import { LayoutPanel } from './components/LayoutPanel';
import { CompareModal, type CompareSpec } from './components/CompareModal';
import { EXAMPLES, exampleId, exampleKey } from './examples';
import { fileLabel, looksLikeUrl, urlFromSource, urlSourceId } from './urlSource';
Expand Down Expand Up @@ -71,6 +72,10 @@ export default function App() {
const [compareError, setCompareError] = useState<string | undefined>(undefined);
const [compareInitialPr, setCompareInitialPr] = useState<string | undefined>(undefined);
const [focus, setFocus] = useState<{ id: string; n: number } | undefined>(undefined);
const [layoutView, setLayoutView] = useState<{ documentType: string; json: unknown } | null>(null);
// The JSON of the contract on screen (both sides in compare mode), for the GroveDB layout panel.
const contractJsonRef = useRef<unknown>(null);
const compareJsonRef = useRef<{ base: unknown; head: unknown } | null>(null);

const [nodes, setNodes, onNodesChange] = useNodesState<DiagramNode>([]);
const [edges, setEdges] = useEdgesState<Edge>([]);
Expand Down Expand Up @@ -126,7 +131,9 @@ export default function App() {
);

const applyModel = useCallback(
async (m: ContractModel, source: string, net: Network, devnet: string) => {
async (m: ContractModel, source: string, net: Network, devnet: string, json: unknown) => {
contractJsonRef.current = json;
compareJsonRef.current = null;
setDiff(null);
diffRef.current = null;
setCompareSpec(null);
Expand All @@ -148,9 +155,10 @@ export default function App() {
setStatus('loading');
setErrorMsg(null);
try {
const m = await loadContractModel({ network: net, contractId: source, devnetName: devnet || undefined, view: viewRef.current });
const json = await loadContractJson({ network: net, contractId: source, devnetName: devnet || undefined, view: viewRef.current });
const m = withRelationships(modelFromPastedJson(json));
saveOverride({ network: net, contractId: source, devnetName: devnet || undefined });
await applyModel(m, source, net, devnet);
await applyModel(m, source, net, devnet, json);
} catch (err) {
setErrorMsg(err instanceof Error ? err.message : String(err));
setStatus('error');
Expand All @@ -172,6 +180,7 @@ export default function App() {
: Promise.resolve(EMPTY_CONTRACT);
const [b, h] = await Promise.all([load1(spec.base), load1(spec.head)]);
const d = diffContracts(b, h);
compareJsonRef.current = { base: b, head: h };
setDiff(d);
diffRef.current = d;
setCompareSpec(spec);
Expand Down Expand Up @@ -278,7 +287,7 @@ export default function App() {
const spec = compareSpec;
if (!d) return;
const headSource = spec?.head ? toSourceId(spec.head) : '';
void applyModel(d.head, headSource, network, devnetName);
void applyModel(d.head, headSource, network, devnetName, compareJsonRef.current?.head ?? null);
if (spec?.head) setContractInput(spec.head);
}, [applyModel, compareSpec, network, devnetName]);

Expand All @@ -296,6 +305,18 @@ export default function App() {
[load, network, devnetName],
);

/** Open the GroveDB layout of a document type: the head's version in compare mode, else the base's. */
const openLayout = useCallback((documentType: string) => {
const compare = compareJsonRef.current;
const d = diffRef.current;
const json = compare && d
? d.head.entities.some((e) => e.name === documentType)
? compare.head
: compare.base
: contractJsonRef.current;
if (json) setLayoutView({ documentType, json });
}, []);

const onNetwork = useCallback((n: Network) => {
setNetwork(n);
resetConnections();
Expand All @@ -308,7 +329,7 @@ export default function App() {
const m = withRelationships(modelFromPastedJson(parsed));
setPasteOpen(false);
setPasteError(undefined);
void applyModel(m, '', network, devnetName);
void applyModel(m, '', network, devnetName, parsed);
} catch (err) {
setPasteError(err instanceof Error ? err.message : String(err));
}
Expand Down Expand Up @@ -460,10 +481,14 @@ export default function App() {
onOpenContract={onOpenContract}
onClose={() => setSelection(null)}
diff={diff ?? undefined}
onShowLayout={openLayout}
/>
)}
</div>

{layoutView && (
<LayoutPanel documentType={layoutView.documentType} contractJson={layoutView.json} onClose={() => setLayoutView(null)} />
)}
{pasteOpen && (
<PasteContractModal onApply={applyPaste} onClose={() => setPasteOpen(false)} error={pasteError} />
)}
Expand Down
25 changes: 22 additions & 3 deletions src/components/InspectorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ interface Props {
onClose: () => void;
/** Compare mode: show the change behind the selected element. */
diff?: ContractDiff;
/** Open the GroveDB layout panel for a document type. */
onShowLayout?: (documentType: string) => void;
}

function changeFor(diff: ContractDiff | undefined, selection: Selection): Change | undefined {
Expand Down Expand Up @@ -268,7 +270,17 @@ function FieldView({ entity, field, version }: { entity: Entity; field: Field; v
);
}

function EntityView({ entity, model, version }: { entity: Entity; model: ContractModel; version?: string }) {
function EntityView({
entity,
model,
onShowLayout,
version,
}: {
entity: Entity;
model: ContractModel;
onShowLayout?: (documentType: string) => void;
version?: string;
}) {
const chips = documentTypeChips(entity);
const shown = new Set(['ttl', 'indexOnly', 'documentsMutable', 'canBeDeleted', 'canBeDeletedByModerators', 'canBeDeletedByModeratorsFor', 'creationRestrictionMode', 'transferable', 'tradeMode', 'documentsKeepHistory', 'keepsTransferHistory', 'keepsPurchaseHistory', 'keepsPricingHistory', 'actionFees', 'tokenCost', 'documentsCountable', 'documentsSummable', 'documentsAverageable', 'rangeCountable', 'rangeSummable', 'rangeAverageable', 'signatureSecurityLevelRequirement', 'requiresIdentityEncryptionBoundedKey', 'requiresIdentityDecryptionBoundedKey']);
const rest = Object.fromEntries(Object.entries(entity.config).filter(([k]) => !shown.has(k)));
Expand All @@ -282,6 +294,11 @@ function EntityView({ entity, model, version }: { entity: Entity; model: Contrac
{entity.indices.length} indexes · {outgoing} references out · {incoming} in
</p>
{entity.description && <p>{entity.description}</p>}
{onShowLayout && (
<button type="button" className="cv-primary cv-layout-button" onClick={() => onShowLayout(entity.name)}>
GroveDB layout
</button>
)}
<ChipList chips={chips} />
{entity.typeReferences.map((r) => (
<ReferenceView key={r.path} reference={r} />
Expand Down Expand Up @@ -336,7 +353,7 @@ function RelationshipView({
);
}

export function InspectorPanel({ selection, model, hiddenEdges, onToggleEdge, onOpenContract, onClose, diff }: Props) {
export function InspectorPanel({ selection, model, hiddenEdges, onToggleEdge, onOpenContract, onClose, diff, onShowLayout }: Props) {
if (!selection) return null;
const change = changeFor(diff, selection);
const version = jsonVersion(diff, selection);
Expand All @@ -349,7 +366,9 @@ export function InspectorPanel({ selection, model, hiddenEdges, onToggleEdge, on

{change && <ChangeSection change={change} />}

{selection.kind === 'entity' && <EntityView entity={selection.entity} model={model} version={version} />}
{selection.kind === 'entity' && (
<EntityView entity={selection.entity} model={model} onShowLayout={onShowLayout} version={version} />
)}

{selection.kind === 'field' && <FieldView entity={selection.entity} field={selection.field} version={version} />}

Expand Down
Loading
Loading