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
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,31 @@ The layout is computed by Drive's own rules (`documentTypeLayout` in `@dashevo/e
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.

## What a document costs

Selecting a document type shows about what creating one of its documents costs, in Dash and
dollars; **Cost** (or *details*) opens the full breakdown:

- two totals: the first document with a set of index values (it creates their trees) and a later
document with the same values (it adds only its own entries);
- the storage, byte for byte: the document itself, and **each index on its own**, with the part it
shares with other indexes (a common prefix, paid once) and the part only it adds; trees prepaid
for other types' preallocated indexes; a `ttl` type's expiration entry;
- the processing: the signature and identity fetch (exact) and the writes (estimated for a number
of stored documents you can change, with the signing key type and a fee increase);
- what the contract charges: its action fee, a token cost, a contest's vote fund;
- what deleting the document refunds;
- **Adjust the document**: an on/off switch for each optional field and a length for each
variable-size one (strings, byte arrays, arrays). Fixed-size fields (identifiers, numbers,
booleans, dates) need no input. By default each variable field sits at the middle of its bounds,
the size Drive's own fee estimates assume, and each optional field is present.

The numbers come from Drive (`documentCreateCost` in `@dashevo/evo-sdk`), which prices every
element an insert writes with GroveDB's byte formulas and is held to the fees Drive charges by its
tests; see [What a Document Costs](https://dashpay.github.io/platform/fees/document-cost.html).
The Dash price comes from CoinGecko (Coinbase if that fails), and you can type your own. Like the
layout, it needs an SDK release that includes it; until then the inspector 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 @@ -183,11 +208,14 @@ One-time: repo settings → **Pages → Source: GitHub Actions**.
src/
config.ts # contract id / example / link + network + view resolution (URL / localStorage / env)
urlSource.ts # ?url= and pasted links: GitHub blob -> raw, fetch + parse
price.ts # the Dash price in dollars (CoinGecko, then Coinbase) and a typed-in override
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 # any source (id, link, example) -> contract JSON -> ContractModel
local.ts # the SDK functions that run from a contract's JSON alone
layout.ts # documentTypeLayout through the SDK (feature-detected)
cost.ts # documentCreateCost through the SDK (feature-detected)
pool.ts # pooled connections, resettable without loading the SDK
examples/ # bundled example contracts + registry
model/
Expand All @@ -198,6 +226,7 @@ src/
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
cost.ts # what a document costs (from the SDK) and its formatting 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 @@ -210,7 +239,7 @@ src/
Canvas.tsx # React Flow canvas, legend, export/re-layout panel
selection.ts, exportImage.ts
components/ # Toolbar, InspectorPanel, ContractMetaPanel, PasteContractModal,
# CompareModal, ChangesPanel, LayoutPanel
# CompareModal, ChangesPanel, LayoutPanel, CostPanel, CostLine
App.tsx, main.tsx, styles.css
scripts/
validate-examples.mjs # examples through DataContract.fromJSON(…, true, 14)
Expand Down
32 changes: 28 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { CostPanel } from './components/CostPanel';
import { LayoutPanel } from './components/LayoutPanel';
import { CompareModal, type CompareSpec } from './components/CompareModal';
import { EXAMPLES, exampleId, exampleKey } from './examples';
Expand Down Expand Up @@ -73,6 +74,7 @@ export default function App() {
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);
const [costView, setCostView] = 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);
Expand Down Expand Up @@ -305,18 +307,35 @@ 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) => {
/** The contract JSON of a document type: the head's version in compare mode, else the base's. */
const contractJsonFor = useCallback((documentType: string): unknown => {
const compare = compareJsonRef.current;
const d = diffRef.current;
const json = compare && d
return compare && d
? d.head.entities.some((e) => e.name === documentType)
? compare.head
: compare.base
: contractJsonRef.current;
if (json) setLayoutView({ documentType, json });
}, []);

/** Open the GroveDB layout of a document type. */
const openLayout = useCallback(
(documentType: string) => {
const json = contractJsonFor(documentType);
if (json) setLayoutView({ documentType, json });
},
[contractJsonFor],
);

/** Open what a document of a type costs. */
const openCost = useCallback(
(documentType: string) => {
const json = contractJsonFor(documentType);
if (json) setCostView({ documentType, json });
},
[contractJsonFor],
);

const onNetwork = useCallback((n: Network) => {
setNetwork(n);
resetConnections();
Expand Down Expand Up @@ -482,13 +501,18 @@ export default function App() {
onClose={() => setSelection(null)}
diff={diff ?? undefined}
onShowLayout={openLayout}
contractJsonFor={contractJsonFor}
onShowCost={openCost}
/>
)}
</div>

{layoutView && (
<LayoutPanel documentType={layoutView.documentType} contractJson={layoutView.json} onClose={() => setLayoutView(null)} />
)}
{costView && (
<CostPanel documentType={costView.documentType} contractJson={costView.json} onClose={() => setCostView(null)} />
)}
{pasteOpen && (
<PasteContractModal onApply={applyPaste} onClose={() => setPasteOpen(false)} error={pasteError} />
)}
Expand Down
68 changes: 68 additions & 0 deletions src/components/CostLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';
import { formatCredits, type DocumentCreateCost } from '../model/cost';
import { CostUnavailableError, loadDocumentCreateCost } from '../sdk/cost';
import { useDashPrice } from './useDashPrice';

type State =
| { status: 'loading' }
| { status: 'ready'; cost: DocumentCreateCost }
| { status: 'error'; message: string; unavailable: boolean };

/**
* The approximate cost of one document of a type, for a document of middle
* sizes, with a button to the full breakdown.
*/
export function CostLine({
documentType,
contractJson,
onShowCost,
}: {
documentType: string;
contractJson: unknown;
onShowCost: () => void;
}) {
const [state, setState] = useState<State>({ status: 'loading' });
const { usdPerDash } = useDashPrice();

useEffect(() => {
let alive = true;
setState({ status: 'loading' });
loadDocumentCreateCost(contractJson, documentType)
.then((cost) => alive && setState({ status: 'ready', cost }))
.catch((err: unknown) => {
if (!alive) return;
setState({
status: 'error',
message: err instanceof Error ? err.message : String(err),
unavailable: err instanceof CostUnavailableError,
});
});
return () => {
alive = false;
};
}, [contractJson, documentType]);

if (state.status === 'loading') return <p className="cv-muted cv-cost-line">Estimating what a document costs…</p>;
if (state.status === 'error') {
return (
<p className="cv-muted cv-cost-line" title={state.message}>
{state.unavailable ? 'The cost estimate appears with the next SDK release.' : 'No cost estimate for this type.'}
</p>
);
}
const { cost } = state;
return (
<p className="cv-cost-line">
About <strong>{formatCredits(cost, cost.totalCredits.newValues, usdPerDash)}</strong> per document
{cost.totalCredits.knownValues < cost.totalCredits.newValues && (
<span className="cv-muted">
{' '}
({formatCredits(cost, cost.totalCredits.knownValues, usdPerDash)} once its index values are stored)
</span>
)}{' '}
<button type="button" className="cv-link-button" onClick={onShowCost}>
details
</button>
</p>
);
}
Loading
Loading