+ }
+
+ /**
+ * @dev Returns the number of decimals used to get its user representation.
+ * For example, if `decimals` equals `2`, a balance of `505` tokens should
+ * be displayed to a user as `5.05` (`505 / 10 ** 2`).
+ *
+ * Tokens usually opt for a value of 18, imitating the relationship between
+ * Ether and Wei. This is the default value returned by this function, unless
+ * it's overridden.
+ *
+ * NOTE: This information is only used for _display_ purposes: it in
+ * no way affects any of the arithmetic of the contract, including
+ * {IERC20-balanceOf} and {IERC20-transfer}.
+ */
+ static decimals() {
+ return 18;
+ }
+
+ /**
+ * @dev See {IERC20-balanceOf}.
+ */
+ balanceOf(account) {
+ return this.balances.get(account);
+ }
+
+ /**
+ * @dev See {IERC20-transfer}.
+ *
+ * Emits an {Approval} event indicating the updated allowance. This is not
+ * required by the EIP. See the note at the beginning of {ERC20}.
+ *
+ * NOTE: Does not update the allowance if the current allowance
+ * is the maximum `uint256`.
+ *
+ * Requirements:
+ *
+ * - `from` and `to` cannot be the zero address.
+ * - `from` must have a balance of at least `amount`.
+ * - the caller must have allowance for ``from``'s tokens of at least
+ * `amount`.
+ */
+ transfer(from, to, amount) {
+ if (!from) throw new Error('ERC20: transfer from the zero address');
+ if (!to) throw new Error('ERC20: transfer to the zero address');
+
+ // _beforeTokenTransfer(from, to, amount);
+
+ const fromBalance = this.balances.get(from);
+ if (fromBalance < amount) throw new Error('ERC20: transfer amount exceeds balance');
+ this.balances.set(from, fromBalance - amount);
+ // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
+ // decrementing then incrementing.
+ this.balances.set(to, this.balances.get(to) + amount);
+
+ // emit Transfer(from, to, amount);
+
+ // _afterTokenTransfer(from, to, amount);
+ }
+
+ /** @dev Creates `amount` tokens and assigns them to `account`, increasing
+ * the total supply.
+ *
+ * Emits a {Transfer} event with `from` set to the zero address.
+ *
+ * Requirements:
+ *
+ * - `account` cannot be the zero address.
+ */
+ mint(account, amount) {
+ if (!account) throw new Error('ERC20: mint to the zero address');
+
+ // _beforeTokenTransfer(address(0), account, amount);
+
+ this.totalSupply += amount;
+ // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
+ this.balances.set(account, this.balances.get(account) + amount);
+ // emit Transfer(address(0), account, amount);
+
+ // _afterTokenTransfer(address(0), account, amount);
+ }
+
+ /**
+ * @dev Destroys `amount` tokens from `account`, reducing the
+ * total supply.
+ *
+ * Emits a {Transfer} event with `to` set to the zero address.
+ *
+ * Requirements:
+ *
+ * - `account` cannot be the zero address.
+ * - `account` must have at least `amount` tokens.
+ */
+ burn(account, amount) {
+ if (!account) throw new Error('ERC20: burn from the zero address');
+
+ // _beforeTokenTransfer(account, address(0), amount);
+
+ const accountBalance = this.balances.get(account);
+ if (accountBalance < amount) throw new Error('ERC20: burn amount exceeds balance');
+ this.balances.set(account, accountBalance - amount);
+ // Overflow not possible: amount <= accountBalance <= totalSupply.
+ this.totalSupply -= amount;
+ // emit Transfer(address(0), account, amount);
+
+ // _afterTokenTransfer(address(0), account, amount);
+ }
+}
diff --git a/src/classes/supporting/erc721.js b/src/classes/supporting/erc721.js
new file mode 100644
index 0000000..9a68166
--- /dev/null
+++ b/src/classes/supporting/erc721.js
@@ -0,0 +1,93 @@
+/**
+ * ERC-721 Non-Fungible Token Standard
+ * See https://eips.ethereum.org/EIPS/eip-721
+ * and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
+ *
+ * This implementation is currently incomplete. It lacks the following:
+ * - Token approvals
+ * - Operator approvals
+ * - Emitting events
+ */
+
+export class ERC721 {
+ constructor(name, symbol) {
+ this.name = name;
+ this.symbol = symbol;
+ this.balances = new Map(); // owner address --> token count
+ this.owners = new Map(); // token id --> owner address
+ // this.tokenApprovals = new Map(); // token id --> approved addresses
+ // this.operatorApprovals = new Map(); // owner --> operator approvals
+
+ this.events = {
+ // Transfer: (_from, _to, _tokenId) => {},
+ // Approval: (_owner, _approved, _tokenId) => {},
+ // ApprovalForAll: (_owner, _operator, _approved) => {},
+ };
+ }
+
+ incrementBalance(owner, increment) {
+ const balance = this.balances.get(owner) ?? 0;
+ this.balances.set(owner, balance + increment);
+ }
+
+ mint(to, tokenId) {
+ console.log('ERC721.mint', { to, tokenId });
+ if (this.owners.get(tokenId)) {
+ throw new Error('ERC721: token already minted');
+ }
+ this.incrementBalance(to, 1);
+ this.owners.set(tokenId, to);
+ }
+
+ burn(tokenId) {
+ const owner = this.owners.get(tokenId);
+ this.incrementBalance(owner, -1);
+ this.owners.delete(tokenId);
+ }
+
+ balanceOf(owner) {
+ if (!owner) {
+ throw new Error('ERC721: address zero is not a valid owner');
+ }
+ return this.balances.get(owner) ?? 0;
+ }
+
+ ownerOf(tokenId) {
+ const owner = this.owners.get(tokenId);
+ if (!owner) {
+ throw new Error(`ERC721: invalid token ID: ${tokenId}`);
+ }
+ return owner;
+ }
+
+ transfer(from, to, tokenId) {
+ console.log('ERC721.transfer', { from, to, tokenId });
+ const owner = this.owners.get(tokenId);
+ if (owner !== from) {
+ throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
+ }
+ this.incrementBalance(from, -1);
+ this.incrementBalance(to, 1);
+ this.owners.set(tokenId, to);
+ }
+
+ /// @notice Enable or disable approval for a third party ("operator") to manage
+ /// all of `msg.sender`'s assets
+ /// @dev Emits the ApprovalForAll event. The contract MUST allow
+ /// multiple operators per owner.
+ /// @param _operator Address to add to the set of authorized operators
+ /// @param _approved True if the operator is approved, false to revoke approval
+ // setApprovalForAll(_operator, _approved) {}
+
+ /// @notice Get the approved address for a single NFT
+ /// @dev Throws if `_tokenId` is not a valid NFT.
+ /// @param _tokenId The NFT to find the approved address for
+ /// @return The approved address for this NFT, or the zero address if there is none
+ // getApproved(_tokenId) {}
+
+ /// @notice Query if an address is an authorized operator for another address
+ /// @param _owner The address that owns the NFTs
+ /// @param _operator The address that acts on behalf of the owner
+ /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
+ // isApprovedForAll(_owner, _operator) {}
+}
diff --git a/src/classes/supporting/post-content.js b/src/classes/supporting/post-content.js
new file mode 100644
index 0000000..5308d36
--- /dev/null
+++ b/src/classes/supporting/post-content.js
@@ -0,0 +1,81 @@
+class Author {
+ constructor(publicKey, weight) {
+ this.publicKey = publicKey;
+ this.weight = weight;
+ }
+
+ toJSON() {
+ return {
+ publicKey: this.publicKey,
+ weight: this.weight,
+ };
+ }
+
+ static fromJSON({ publicKey, weight }) {
+ return new Author(publicKey, weight);
+ }
+}
+
+class Citation {
+ constructor(postId, weight) {
+ this.postId = postId;
+ this.weight = weight;
+ }
+
+ toJSON() {
+ return {
+ postId: this.postId,
+ weight: this.weight,
+ };
+ }
+
+ static fromJSON({ postId, weight }) {
+ return new Citation(postId, weight);
+ }
+}
+
+export class PostContent {
+ constructor(content = {}) {
+ this.content = content;
+ this.authors = [];
+ this.citations = [];
+ }
+
+ addAuthor(authorPublicKey, weight) {
+ const author = new Author(authorPublicKey, weight);
+ this.authors.push(author);
+ return this;
+ }
+
+ addCitation(postId, weight) {
+ const citation = new Citation(postId, weight);
+ this.citations.push(citation);
+ return this;
+ }
+
+ setTitle(title) {
+ this.title = title;
+ return this;
+ }
+
+ toJSON() {
+ return {
+ content: this.content,
+ authors: this.authors.map((author) => author.toJSON()),
+ citations: this.citations.map((citation) => citation.toJSON()),
+ ...(this.id ? { id: this.id } : {}),
+ title: this.title,
+ };
+ }
+
+ static fromJSON({
+ id, content, authors, citations, title,
+ }) {
+ const post = new PostContent(content);
+ post.authors = authors.map((author) => Author.fromJSON(author));
+ post.citations = citations.map((citation) => Citation.fromJSON(citation));
+ post.id = id;
+ post.title = title;
+ return post;
+ }
+}
diff --git a/src/classes/supporting/stake.js b/src/classes/supporting/stake.js
new file mode 100644
index 0000000..8b6ac7e
--- /dev/null
+++ b/src/classes/supporting/stake.js
@@ -0,0 +1,14 @@
+export class Stake {
+ constructor({
+ tokenId, position, amount, lockingTime,
+ }) {
+ this.tokenId = tokenId;
+ this.position = position;
+ this.amount = amount;
+ this.lockingTime = lockingTime;
+ }
+
+ getStakeValue({ lockingTimeExponent } = {}) {
+ return this.amount * this.lockingTime ** lockingTimeExponent;
+ }
+}
diff --git a/src/classes/supporting/vertex.js b/src/classes/supporting/vertex.js
new file mode 100644
index 0000000..fb27be6
--- /dev/null
+++ b/src/classes/supporting/vertex.js
@@ -0,0 +1,159 @@
+import { displayNumber } from '../../util/helpers.js';
+
+import { Edge } from './edge.js';
+
+export class Vertex {
+ constructor(graph, type, id, data, options = {}) {
+ this.graph = graph;
+ this.type = type;
+ this.id = id;
+ this.data = data;
+ this.label = options.label ?? this.id;
+ this.options = options;
+ this.edges = {
+ from: [],
+ to: [],
+ };
+ this.installedClickCallback = false;
+ this.properties = new Map();
+ }
+
+ reset() {
+ this.installedClickCallback = false;
+ }
+
+ getEdges(type, away) {
+ return this.edges[away ? 'from' : 'to'].filter(
+ (edge) => edge.type === type,
+ );
+ }
+
+ setProperty(key, value) {
+ this.properties.set(key, value);
+ return this;
+ }
+
+ displayVertex() {
+ if (this.options.hide) {
+ return;
+ }
+
+ let html = '';
+ html += `${this.label}`;
+ html += '';
+ for (const [key, value] of this.properties.entries()) {
+ const displayValue = typeof value === 'number' ? displayNumber(value) : value;
+ html += `${key} | ${displayValue} |
`;
+ }
+ html += '
';
+ if (this.id !== this.label) {
+ html += `${this.id}
`;
+ }
+ html = html.replaceAll(/\n\s*/g, '');
+ this.graph.flowchart?.log(`${this.id}["${html}"]`);
+
+ if (this.graph.editable && !this.installedClickCallback) {
+ this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
+ this.installedClickCallback = true;
+ }
+ }
+
+ static prepareEditorDocument(graph, doc, vertexId) {
+ doc.clear();
+ const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
+ const form = doc.form().lastElement;
+ doc.remark(`${vertex ? 'Edit' : 'Add'} Vertex
`, { parentEl: form.el });
+ form
+ .textField({
+ id: 'id', name: 'id', defaultValue: vertex?.id,
+ })
+ .textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
+ .textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
+
+ doc.remark('Properties
', { parentEl: form.el });
+ const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem;
+ const addPropertyForm = (key, value) => {
+ const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
+ subForm.textField({ id: 'key', name: 'key', defaultValue: key })
+ .textField({ id: 'value', name: 'value', defaultValue: value })
+ .button({
+ id: 'remove',
+ name: 'Remove Property',
+ cb: () => subFormArray.remove(subForm),
+ });
+ doc.remark('
', { parentEl: subForm.el });
+ };
+
+ if (vertex) {
+ for (const [key, value] of vertex.properties.entries()) {
+ addPropertyForm(key, value);
+ }
+ }
+
+ form.button({
+ id: 'add',
+ name: 'Add Property',
+ cb: () => addPropertyForm('', ''),
+ });
+
+ form.button({
+ id: 'save',
+ name: 'Save',
+ type: 'submit',
+ cb: ({ form: { value: formValue } }) => {
+ let fullRedraw = false;
+ if (vertex && formValue.id !== vertex.id) {
+ fullRedraw = true;
+ }
+ // TODO: preserve data types of properties
+ formValue.properties = new Map(formValue.properties.map(({ key, value }) => [key, value]));
+ if (vertex) {
+ Object.assign(vertex, formValue);
+ vertex.displayVertex();
+ } else {
+ const newVertex = graph.addVertex(formValue.type, formValue.id, null, formValue.label);
+ Object.assign(newVertex, formValue);
+ doc.clear();
+ Vertex.prepareEditorDocument(graph, doc, newVertex.id);
+ }
+ if (fullRedraw) {
+ graph.redraw();
+ }
+ },
+ });
+
+ if (vertex) {
+ form.button({
+ id: 'delete',
+ name: 'Delete Vertex',
+ cb: () => {
+ graph.deleteVertex(vertex.id);
+ graph.redraw();
+ graph.resetEditorDocument();
+ },
+ });
+
+ doc.remark('New Edge
', { parentEl: form.el });
+ const { subForm } = form.subForm({ name: 'newEdge' }).lastItem;
+ subForm.textField({ name: 'to' });
+ subForm.textField({ name: 'type' });
+ subForm.textField({ name: 'weight' });
+ subForm.button({
+ name: 'Save',
+ cb: ({ form: { value: { to, type, weight } } }) => {
+ graph.addEdge(type, vertex, to, weight, null);
+ doc.clear();
+ Edge.prepareEditorDocument(graph, doc, vertex.id, to);
+ },
+ });
+ }
+
+ form.button({
+ id: 'cancel',
+ name: 'Cancel',
+ cb: () => graph.resetEditorDocument(),
+ });
+
+ return doc;
+ }
+}
diff --git a/src/classes/supporting/vm.js b/src/classes/supporting/vm.js
new file mode 100644
index 0000000..03e5dbf
--- /dev/null
+++ b/src/classes/supporting/vm.js
@@ -0,0 +1,69 @@
+import { Action } from '../display/action.js';
+
+class ContractRecord {
+ constructor(id, instance) {
+ this.id = id;
+ this.instance = instance;
+ }
+}
+
+export class VMHandle {
+ constructor(vm, sender) {
+ this.vm = vm;
+ this.sender = sender;
+ this.actions = {
+ call: new Action('call', vm.scene),
+ return: new Action('return', vm.scene),
+ };
+ }
+
+ /**
+ * @param {string} id Contract ID
+ * @param {string} method
+ */
+ async callContract(id, method, ...args) {
+ const instance = this.vm.getContractInstance(id);
+ const fn = instance[method];
+ if (!fn) throw new Error(`Contract ${id} method ${method} not found!`);
+ await this.actions.call.log(this.sender, instance, method);
+ const result = await fn.call(instance, this.sender, ...args);
+ await this.actions.return.log(instance, this.sender, undefined, undefined, '-->>');
+ return result;
+ }
+}
+
+export class VM {
+ constructor(scene) {
+ this.scene = scene;
+ this.contracts = new Map();
+ }
+
+ /**
+ * @param {string} id
+ * @param {class} ContractClass
+ * @param {any[]} ...args Passed to contractClass constructor after `vm`
+ */
+ addContract(id, ContractClass, ...args) {
+ const instance = new ContractClass(this, ...args);
+ const contract = new ContractRecord(id, instance);
+ this.contracts.set(id, contract);
+ }
+
+ getHandle(sender) {
+ return new VMHandle(this, sender);
+ }
+
+ /**
+ * @param {string} id
+ */
+ getContract(id) {
+ return this.contracts.get(id);
+ }
+
+ /**
+ * @param {string} id
+ */
+ getContractInstance(id) {
+ return this.getContract(id)?.instance;
+ }
+}
diff --git a/src/classes/supporting/voter.js b/src/classes/supporting/voter.js
new file mode 100644
index 0000000..ea5fed8
--- /dev/null
+++ b/src/classes/supporting/voter.js
@@ -0,0 +1,14 @@
+export class Voter {
+ constructor(reputationPublicKey) {
+ this.reputationPublicKey = reputationPublicKey;
+ this.voteHistory = [];
+ this.dateLastVote = null;
+ }
+
+ addVoteRecord(stake) {
+ this.voteHistory.push(stake);
+ if (!this.dateLastVote || stake.dateStart > this.dateLastVote) {
+ this.dateLastVote = stake.dateStart;
+ }
+ }
+}
diff --git a/src/classes/supporting/wdg.js b/src/classes/supporting/wdg.js
new file mode 100644
index 0000000..ab083e7
--- /dev/null
+++ b/src/classes/supporting/wdg.js
@@ -0,0 +1,231 @@
+import { Vertex } from './vertex.js';
+import { Edge } from './edge.js';
+import { Document } from '../display/document.js';
+
+const allGraphs = [];
+
+const makeWDGHandler = (graphIndex) => (vertexId) => {
+ const graph = allGraphs[graphIndex];
+ // We want a document for editing this node, which may be a vertex or an edge
+ const { editorDoc } = graph;
+ editorDoc.clear();
+ if (vertexId.startsWith('edge:')) {
+ const [, from, to] = vertexId.split(':');
+ Edge.prepareEditorDocument(graph, editorDoc, from, to);
+ } else {
+ Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
+ }
+};
+
+export class WeightedDirectedGraph {
+ constructor(scene, options = {}) {
+ this.scene = scene;
+ this.vertices = new Map();
+ this.edgeTypes = new Map();
+ this.nextVertexId = 0;
+ this.flowchart = scene?.flowchart;
+ this.editable = options.editable;
+
+ // Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID.
+ // In order to provide the appropriate graph context for each callback, we create a separate callback
+ // function for each graph.
+ this.index = allGraphs.length;
+ allGraphs.push(this);
+ window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
+
+ // TODO: Populate history
+ this.history = {};
+ }
+
+ getHistory() {
+ // record operations that modify the graph
+ return this.history;
+ }
+
+ toJSON() {
+ return {
+ vertices: Array.from(this.vertices.values()),
+ edgeTypes: Array.from(this.edgeTypes.keys()),
+ edges: Array.from(this.edgeTypes.values()).flatMap((edges) => Array.from(edges.values())),
+ history: this.getHistory(),
+ };
+ }
+
+ redraw() {
+ // Call .reset() on all vertices and edges
+ for (const vertex of this.vertices.values()) {
+ vertex.reset();
+ }
+ for (const edges of this.edgeTypes.values()) {
+ for (const edge of edges.values()) {
+ edge.reset();
+ }
+ }
+
+ // Clear the target div
+ this.flowchart?.reset();
+ this.flowchart?.init();
+
+ // Draw all vertices and edges
+ for (const vertex of this.vertices.values()) {
+ vertex.displayVertex();
+ }
+ // Let's flatmap and dedupe by [from, to] since each edge
+ // renders all comorphic edges as well.
+ const edgesFrom = new Map(); // edgeMap[from][to] = edge
+ for (const edges of this.edgeTypes.values()) {
+ for (const edge of edges.values()) {
+ const edgesTo = edgesFrom.get(edge.from) || new Map();
+ edgesTo.set(edge.to, edge);
+ edgesFrom.set(edge.from, edgesTo);
+ }
+ }
+
+ for (const edgesTo of edgesFrom.values()) {
+ for (const edge of edgesTo.values()) {
+ edge.displayEdge();
+ }
+ }
+
+ // Ensure rerender
+ this.flowchart?.render();
+ }
+
+ withFlowchart() {
+ this.scene?.withSectionFlowchart();
+ this.flowchart = this.scene?.lastFlowchart;
+ if (this.editable) {
+ this.editorDoc = new Document('WDGControls', this.flowchart.box.el);
+ this.resetEditorDocument();
+ }
+ this.errorDoc = new Document('WDGErrors', this.flowchart.box.el);
+ return this;
+ }
+
+ resetEditorDocument() {
+ this.editorDoc.clear();
+ Vertex.prepareEditorDocument(this, this.editorDoc);
+ }
+
+ addVertex(type, id, data, label, options) {
+ // Supports simple case of auto-incremented numeric ids
+ if (typeof id === 'object') {
+ data = id;
+ id = this.nextVertexId++;
+ }
+ id = (typeof id === 'number') ? id.toString(10) : id;
+ if (this.vertices.has(id)) {
+ throw new Error(`Vertex already exists with id: ${id}`);
+ }
+ const vertex = new Vertex(this, type, id, data, { ...options, label });
+ this.vertices.set(id, vertex);
+ vertex.displayVertex();
+ return vertex;
+ }
+
+ getVertex(id) {
+ id = (typeof id === 'number') ? id.toString(10) : id;
+ return this.vertices.get(id);
+ }
+
+ getVertexData(id) {
+ return this.getVertex(id)?.data;
+ }
+
+ getVerticesData() {
+ return Array.from(this.vertices.values()).map(({ data }) => data);
+ }
+
+ getEdge(type, from, to) {
+ from = from instanceof Vertex ? from : this.getVertex(from);
+ to = to instanceof Vertex ? to : this.getVertex(to);
+ if (!from || !to) {
+ return undefined;
+ }
+ const edges = this.edgeTypes.get(type);
+ const edgeKey = Edge.getKey({ from, to, type });
+ return edges?.get(edgeKey);
+ }
+
+ getEdgeWeight(type, from, to) {
+ return this.getEdge(type, from, to)?.weight;
+ }
+
+ setEdgeWeight(type, from, to, weight, data, options) {
+ from = from instanceof Vertex ? from : this.getVertex(from);
+ to = to instanceof Vertex ? to : this.getVertex(to);
+ const edge = new Edge(this, type, from, to, weight, data, options);
+ let edges = this.edgeTypes.get(type);
+ if (!edges) {
+ edges = new Map();
+ this.edgeTypes.set(type, edges);
+ }
+ const edgeKey = Edge.getKey(edge);
+ edges.set(edgeKey, edge);
+ edge.displayEdgeNode();
+ return edge;
+ }
+
+ addEdge(type, from, to, weight, data, options) {
+ from = from instanceof Vertex ? from : this.getVertex(from);
+ to = to instanceof Vertex ? to : this.getVertex(to);
+ const existingEdges = this.getEdges(type, from, to);
+ if (this.getEdge(type, from, to)) {
+ throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
+ }
+ const edge = this.setEdgeWeight(type, from, to, weight, data, options);
+ from.edges.from.push(edge);
+ to.edges.to.push(edge);
+ if (existingEdges.length) {
+ edge.displayEdgeNode();
+ } else {
+ edge.displayEdge();
+ }
+ return edge;
+ }
+
+ getEdges(type, from, to) {
+ from = from instanceof Vertex ? from : this.getVertex(from);
+ to = to instanceof Vertex ? to : this.getVertex(to);
+ const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
+ return edgeTypes.flatMap((edgeType) => {
+ const edges = this.edgeTypes.get(edgeType);
+ return Array.from(edges?.values() || []).filter((edge) => {
+ const matchFrom = from === null || from === undefined || from === edge.from;
+ const matchTo = to === null || to === undefined || to === edge.to;
+ return matchFrom && matchTo;
+ });
+ });
+ }
+
+ countVertices(type) {
+ if (!type) {
+ return this.vertices.size;
+ }
+ return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
+ }
+
+ deleteEdge(type, from, to) {
+ from = from instanceof Vertex ? from : this.getVertex(from);
+ to = to instanceof Vertex ? to : this.getVertex(to);
+ const edges = this.edgeTypes.get(type);
+ const edgeKey = Edge.getKey({ type, from, to });
+ if (!edges) return;
+ const edge = edges.get(edgeKey);
+ if (!edge) return;
+ to.edges.from.forEach((x, i) => (x === edge) && to.edges.from.splice(i, 1));
+ from.edges.to.forEach((x, i) => (x === edge) && from.edges.to.splice(i, 1));
+ edges.delete(edgeKey);
+ if (edges.size === 0) {
+ this.edgeTypes.delete(type);
+ }
+ }
+
+ deleteVertex(id) {
+ const vertex = this.getVertex(id);
+ for (const { type, from, to } of [...vertex.edges.to, ...vertex.edges.from]) {
+ this.deleteEdge(type, from, to);
+ }
+ this.vertices.delete(id);
+ }
+}
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 0000000..28aeaa7
Binary files /dev/null and b/src/favicon.ico differ
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..a42bab2
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,94 @@
+body {
+ background-color: #09343f;
+ color: #b6b6b6;
+ font-family: monospace;
+ font-size: 12pt;
+ margin: 1em;
+}
+a {
+ color: #c6f4ff;
+}
+a:visited {
+ color: #85b7c3;
+}
+.box {
+ width: fit-content;
+}
+.box .name {
+ width: 15em;
+ font-weight: bold;
+ text-align: right;
+ margin-right: 6pt;
+}
+.box .value {
+ width: fit-content;
+}
+.flex {
+ display: flex;
+}
+.monospace {
+ font-family: monospace;
+}
+.dim {
+ opacity: 0.25;
+}
+.padded {
+ padding: 20px;
+}
+.top-rail {
+ position: sticky;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 0;
+}
+.scene-controls {
+ position: relative;
+ left: 150px;
+}
+svg {
+ width: 800px;
+}
+th {
+ text-align: left;
+ padding: 10px;
+}
+td {
+ background-color: #0c2025;
+}
+.edge > rect {
+ fill: #216262 !important;
+}
+button {
+ margin: 5px;
+ margin-top: 1em;
+ background-color: #c6f4ff;
+ border-color: #b6b6b6;
+ border-radius: 5px;
+}
+button:disabled {
+ background-color: #2a535e;
+ color: #919191;
+}
+label > input {
+ margin-left: 1em;
+}
+label {
+ font-family: monospace;
+ font-weight: bold;
+ font-size: smaller;
+ color: #999999;
+}
+label > div {
+ display: inline-block;
+ min-width: 50px;
+}
+table {
+ width: 100%;
+}
+form {
+ min-width: 20em;
+}
+span.small {
+ font-size: smaller;
+}
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..8b95bbf
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,68 @@
+
+
+
+ DGF Tests
+
+
+
+
+ Decentralized Governance Framework
+
+ We are building a system to enable experts to collaborate and self-govern in accordance with their values.
+
+
+ For more information please see the DGF
+ Wiki.
+
+ Javascript Prototype: Example Scenarios
+
+ Below are example scenarios with various assertions covering features of our reputation system.
+
+
+ The code for this site is available in GitLab.
+
+