├── .envrc ├── packages ├── simulator │ ├── src │ │ ├── processors │ │ │ ├── dht-ops.ts │ │ │ ├── files.ts │ │ │ ├── create-conductors.ts │ │ │ └── hash.ts │ │ ├── executor │ │ │ ├── task.ts │ │ │ └── delay-middleware.ts │ │ ├── core │ │ │ ├── hdk │ │ │ │ ├── index.ts │ │ │ │ ├── host-fn │ │ │ │ │ ├── sys_time.ts │ │ │ │ │ ├── actions │ │ │ │ │ │ ├── delete_entry.ts │ │ │ │ │ │ ├── delete_cap_grant.ts │ │ │ │ │ │ ├── common │ │ │ │ │ │ │ ├── create.ts │ │ │ │ │ │ │ ├── delete.ts │ │ │ │ │ │ │ └── update.ts │ │ │ │ │ │ ├── create_cap_grant.ts │ │ │ │ │ │ ├── close_chain.ts │ │ │ │ │ │ ├── create_link.ts │ │ │ │ │ │ ├── open_chain.ts │ │ │ │ │ │ ├── create_entry.ts │ │ │ │ │ │ ├── delete_link.ts │ │ │ │ │ │ └── update_entry.ts │ │ │ │ │ ├── hash_entry.ts │ │ │ │ │ ├── call_remote.ts │ │ │ │ │ ├── get.ts │ │ │ │ │ ├── get_links.ts │ │ │ │ │ ├── must_get_valid_record.ts │ │ │ │ │ ├── get_details.ts │ │ │ │ │ ├── must_get_action.ts │ │ │ │ │ ├── dna_info.ts │ │ │ │ │ ├── must_get_entry.ts │ │ │ │ │ ├── create_clone_cell.ts │ │ │ │ │ ├── get_link_details.ts │ │ │ │ │ ├── get_agent_activity.ts │ │ │ │ │ ├── agent_info.ts │ │ │ │ │ ├── must_get_agent_activity.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ └── query.ts │ │ │ │ ├── host-fn.ts │ │ │ │ └── path.ts │ │ │ ├── cell │ │ │ │ ├── sys_validate │ │ │ │ │ └── types.ts │ │ │ │ ├── source-chain │ │ │ │ │ ├── put.ts │ │ │ │ │ └── get.ts │ │ │ │ ├── index.ts │ │ │ │ ├── cascade │ │ │ │ │ └── types.ts │ │ │ │ ├── workflows │ │ │ │ │ ├── trigger.ts │ │ │ │ │ ├── workflows.ts │ │ │ │ │ ├── produce_dht_ops.ts │ │ │ │ │ ├── publish_dht_ops.ts │ │ │ │ │ ├── integrate_dht_ops.ts │ │ │ │ │ ├── validation_receipt.ts │ │ │ │ │ ├── incoming_dht_ops.ts │ │ │ │ │ ├── genesis.ts │ │ │ │ │ └── app_validation │ │ │ │ │ │ └── types.ts │ │ │ │ └── state │ │ │ │ │ └── metadata.ts │ │ │ ├── bad-agent.ts │ │ │ ├── network │ │ │ │ ├── gossip │ │ │ │ │ ├── types.ts │ │ │ │ │ └── bloom │ │ │ │ │ │ └── index.ts │ │ │ │ ├── dht_arc.ts │ │ │ │ ├── network-request.ts │ │ │ │ ├── connection.ts │ │ │ │ ├── network.ts │ │ │ │ └── kitsune_p2p.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── selectors.ts │ │ ├── bootstrap │ │ │ └── bootstrap-service.ts │ │ └── dnas │ │ │ └── simulated-dna.ts │ ├── test │ │ ├── utils.js │ │ ├── peers.test.js │ │ ├── location.test.js │ │ ├── path.test.js │ │ ├── conductor.test.js │ │ ├── links.test.js │ │ └── stress.test.js │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── .gitignore │ └── package.json ├── cli │ ├── client │ │ ├── .gitignore │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ ├── web-dev-server.config.mjs │ │ ├── package.json │ │ ├── src │ │ │ └── dock-view.ts │ │ └── index.html │ └── server │ │ ├── nodemon.config.json │ │ ├── tsconfig.json │ │ ├── src │ │ ├── urls.ts │ │ ├── index.ts │ │ └── app.ts │ │ ├── README.md │ │ ├── webpack.config.js │ │ └── package.json ├── elements │ ├── .gitignore │ ├── src │ │ ├── store │ │ │ ├── mode.ts │ │ │ ├── polling-store.ts │ │ │ └── utils.ts │ │ ├── elements │ │ │ ├── call-zome-fns │ │ │ │ └── types.ts │ │ │ ├── source-chain │ │ │ │ ├── graph.ts │ │ │ │ └── source-chain.stories.js │ │ │ ├── entry-contents │ │ │ │ ├── entry-contents.stories.js │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ ├── hash.ts │ │ │ │ ├── common-graph-styles.ts │ │ │ │ ├── utils.ts │ │ │ │ └── shared-styles.ts │ │ │ ├── dht-cells │ │ │ │ ├── utils.ts │ │ │ │ └── graph.ts │ │ │ ├── dht-entries │ │ │ │ ├── entry-graph.stories.js │ │ │ │ └── graph.ts │ │ │ ├── helpers │ │ │ │ ├── help-button.ts │ │ │ │ └── editable-field.ts │ │ │ ├── validation-queue │ │ │ │ └── vaadin-grid-template-renderer-column.ts │ │ │ ├── dna-select │ │ │ │ └── index.ts │ │ │ └── select-active-dna │ │ │ │ └── index.ts │ │ ├── base │ │ │ ├── context.ts │ │ │ ├── playground-element.ts │ │ │ ├── connected-playground-context.ts │ │ │ ├── base-playground-context.ts │ │ │ └── simulated-playground-context.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── package.json │ └── README.md └── demo │ ├── src │ └── index.ts │ ├── .gitignore │ ├── vite.config.ts │ ├── tsconfig.json │ ├── .editorconfig │ ├── README.md │ ├── index.html │ ├── LICENSE │ ├── package.json │ └── rollup.config.js ├── .storybook ├── manager.js ├── main.js ├── preview-head.html └── preview.js ├── fixture ├── .gitignore ├── workdir │ ├── web-happ.yaml │ └── happ.yaml ├── ui │ ├── vite.config.ts │ ├── src │ │ ├── contexts.ts │ │ ├── forum │ │ │ └── posts │ │ │ │ ├── types.ts │ │ │ │ ├── comments-for-post.ts │ │ │ │ ├── all-posts.ts │ │ │ │ └── create-comment.ts │ │ └── holochain-app.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── index.html │ └── package.json ├── tests │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── package.json │ └── src │ │ └── forum │ │ └── posts │ │ ├── common.ts │ │ └── all-posts.test.ts ├── dnas │ └── forum │ │ ├── zomes │ │ ├── coordinator │ │ │ └── posts │ │ │ │ ├── src │ │ │ │ └── all_posts.rs │ │ │ │ └── Cargo.toml │ │ └── integrity │ │ │ └── posts │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ └── lib.rs │ │ └── workdir │ │ └── dna.yaml ├── Cargo.toml ├── .github │ └── workflows │ │ └── test.yaml ├── package.json └── README.md ├── pnpm-workspace.yaml ├── .prettierrc ├── eslint.config.js ├── tsconfig.json ├── .editorconfig ├── LICENSE ├── package.json ├── README.md ├── .github └── workflows │ └── build-nix-app.yaml └── .gitignore /.envrc: -------------------------------------------------------------------------------- 1 | use flake . 2 | -------------------------------------------------------------------------------- /packages/simulator/src/processors/dht-ops.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/client/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | out-tsc/ -------------------------------------------------------------------------------- /packages/elements/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | storybook-static -------------------------------------------------------------------------------- /packages/simulator/src/executor/task.ts: -------------------------------------------------------------------------------- 1 | export type Task = () => Promise; 2 | -------------------------------------------------------------------------------- /packages/demo/src/index.ts: -------------------------------------------------------------------------------- 1 | export { HolochainPlayground } from './holochain-playground.js'; 2 | -------------------------------------------------------------------------------- /packages/elements/src/store/mode.ts: -------------------------------------------------------------------------------- 1 | export enum PlaygroundMode { 2 | Connected, 3 | Simulated, 4 | } 5 | -------------------------------------------------------------------------------- /packages/simulator/test/utils.js: -------------------------------------------------------------------------------- 1 | export const sleep = ms => 2 | new Promise(resolve => setTimeout(() => resolve(), ms)); 3 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | 3 | addons.setConfig({ 4 | panelPosition: 'right', 5 | showPanel: false 6 | }); 7 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from './host-fn/actions/create_entry.js'; 2 | export * from './host-fn.js'; 3 | export * from './context.js'; 4 | -------------------------------------------------------------------------------- /fixture/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules/ 3 | /dist/ 4 | /target/ 5 | /.cargo/ 6 | *.happ 7 | *.webhapp 8 | *.zip 9 | *.dna 10 | .hc* 11 | .hc 12 | .running 13 | -------------------------------------------------------------------------------- /fixture/workdir/web-happ.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | manifest_version: "1" 3 | name: forum 4 | ui: 5 | bundled: "../ui/dist.zip" 6 | happ_manifest: 7 | bundled: "./forum.happ" 8 | -------------------------------------------------------------------------------- /fixture/ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | plugins: [ 6 | ] 7 | }); 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/simulator' 3 | - 'packages/elements' 4 | - 'packages/cli/client' 5 | - 'packages/cli/server' 6 | - 'packages/demo' 7 | -------------------------------------------------------------------------------- /packages/simulator/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | testTimeout: 60 * 1000 * 3, // 3 mins, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /fixture/ui/src/contexts.ts: -------------------------------------------------------------------------------- 1 | import { AppClient } from '@holochain/client'; 2 | import { createContext } from '@lit-labs/context'; 3 | 4 | export const clientContext = createContext('appAgentClient'); 5 | -------------------------------------------------------------------------------- /packages/cli/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | build: { 6 | watch: { 7 | include: ['../../../**/*/dist'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /fixture/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/elements/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules/!(@open-wc)"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/simulator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules/!(@open-wc)"] 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/cli/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules/!(@open-wc)"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/server/nodemon.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["./src", "../client/dist"], 3 | "ignore": ["**/*.map.js"], 4 | "exec": "webpack --config webpack.config.js --mode development && node ./dist/app.js ws://localhost:${ADMIN_PORT} ws://localhost:${ADMIN_PORT_2}" 5 | } 6 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/sys_validate/types.ts: -------------------------------------------------------------------------------- 1 | import { DepsMissing } from '../workflows/sys_validation.js'; 2 | 3 | export type ValidationOutcome = 4 | | { 5 | resolved: true; 6 | valid: boolean; 7 | } 8 | | ({ 9 | resolved: false; 10 | } & DepsMissing); 11 | -------------------------------------------------------------------------------- /fixture/tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | poolOptions: { 6 | threads: { 7 | singleThread: true, 8 | }, 9 | }, 10 | testTimeout: 60 * 1000 * 3, // 3 mins 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/simulator/src/executor/delay-middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from './middleware-executor.js'; 2 | 3 | export const sleep = (ms: number) => 4 | new Promise(resolve => setTimeout(() => resolve(), ms)); 5 | 6 | export const DelayMiddleware = (ms: number): Middleware => () => sleep(ms); 7 | -------------------------------------------------------------------------------- /packages/simulator/.gitignore: -------------------------------------------------------------------------------- 1 | ## editors 2 | /.idea 3 | /.vscode 4 | 5 | ## system files 6 | .DS_Store 7 | 8 | ## npm 9 | /node_modules/ 10 | /npm-debug.log 11 | 12 | ## testing 13 | /coverage/ 14 | 15 | ## temp folders 16 | /.tmp/ 17 | 18 | # build 19 | /_site/ 20 | /dist/ 21 | /out-tsc/ 22 | 23 | storybook-static -------------------------------------------------------------------------------- /fixture/dnas/forum/zomes/coordinator/posts/src/all_posts.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | use posts_integrity::*; 3 | #[hdk_extern] 4 | pub fn get_all_posts(_: ()) -> ExternResult> { 5 | let path = Path::from("all_posts"); 6 | get_links(GetLinksInputBuilder::try_new(path.path_entry_hash()?, LinkTypes::AllPosts)?.build()) 7 | } 8 | -------------------------------------------------------------------------------- /packages/elements/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import checker from 'vite-plugin-checker'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: './demo', 7 | build: { 8 | sourcemap: true, 9 | }, 10 | plugins: [ 11 | checker({ 12 | typescript: true, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /packages/demo/.gitignore: -------------------------------------------------------------------------------- 1 | ## editors 2 | /.idea 3 | /.vscode 4 | 5 | ## system files 6 | .DS_Store 7 | 8 | ## npm 9 | /node_modules/ 10 | /npm-debug.log 11 | 12 | ## testing 13 | /coverage/ 14 | 15 | ## temp folders 16 | /.tmp/ 17 | 18 | # build 19 | /_site/ 20 | /dist/ 21 | /out-tsc/ 22 | 23 | storybook-static 24 | custom-elements.json 25 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "avoid", 4 | "useTabs": true, 5 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 6 | "importOrder": ["", "^[./]"], 7 | "importOrderSeparation": true, 8 | "importOrderSortSpecifiers": true, 9 | "importOrderParserPlugins": ["typescript", "decorators-legacy"] 10 | } 11 | -------------------------------------------------------------------------------- /fixture/dnas/forum/zomes/integrity/posts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "posts_integrity" 3 | version = "0.0.1" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | name = "posts_integrity" 9 | 10 | [dependencies] 11 | hdi = { workspace = true } 12 | 13 | holochain_serialized_bytes = { workspace = true } 14 | serde = { workspace = true } 15 | -------------------------------------------------------------------------------- /packages/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import checker from 'vite-plugin-checker'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: '/holochain-playground/', 7 | build: { 8 | sourcemap: true, 9 | }, 10 | plugins: [ 11 | checker({ 12 | typescript: true, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /fixture/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "private": true, 4 | "scripts": { 5 | "test": "vitest run" 6 | }, 7 | "dependencies": { 8 | "@msgpack/msgpack": "^2.7.0", 9 | "@holochain/client": "^0.19.0", 10 | "@holochain/tryorama": "^0.18.0-rc", 11 | "typescript": "^5.4.0", 12 | "vitest": "^1.4.0" 13 | }, 14 | "type": "module" 15 | } -------------------------------------------------------------------------------- /fixture/ui/.gitignore: -------------------------------------------------------------------------------- 1 | ## editors 2 | /.idea 3 | /.vscode 4 | 5 | ## system files 6 | .DS_Store 7 | 8 | ## npm 9 | /**/node_modules/ 10 | /npm-debug.log 11 | 12 | ## testing 13 | /coverage/ 14 | 15 | ## temp folders 16 | /.tmp/ 17 | 18 | # build 19 | /_site/ 20 | /dist/ 21 | /out-tsc/ 22 | /target/ 23 | /.cargo 24 | 25 | storybook-static 26 | .rollup.cache 27 | *.tsbuildinfo -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /packages/elements/src/elements/call-zome-fns/types.ts: -------------------------------------------------------------------------------- 1 | import { CellId } from '@holochain/client'; 2 | 3 | export interface ZomeFunctionResult { 4 | cellId: CellId; 5 | zome: string; 6 | fnName: string; 7 | payload: any; 8 | timestamp: number; 9 | result: 10 | | undefined 11 | | { 12 | success: boolean; 13 | payload: any; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /fixture/dnas/forum/zomes/coordinator/posts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "posts" 3 | version = "0.0.1" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | name = "posts" 9 | 10 | [dependencies] 11 | hdk = { workspace = true } 12 | 13 | holochain_serialized_bytes = { workspace = true } 14 | serde = { workspace = true } 15 | 16 | posts_integrity = { workspace = true } 17 | -------------------------------------------------------------------------------- /fixture/workdir/happ.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | manifest_version: "1" 3 | name: forum 4 | description: ~ 5 | roles: 6 | - name: forum 7 | provisioning: 8 | strategy: create 9 | deferred: false 10 | dna: 11 | bundled: "../dnas/forum/workdir/forum.dna" 12 | modifiers: 13 | network_seed: ~ 14 | properties: ~ 15 | installed_hash: ~ 16 | clone_limit: 0 17 | -------------------------------------------------------------------------------- /packages/elements/src/base/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from '@lit/context'; 2 | 3 | import { ConnectedPlaygroundStore } from '../store/connected-playground-store.js'; 4 | import { SimulatedPlaygroundStore } from '../store/simulated-playground-store.js'; 5 | 6 | export const playgroundContext = createContext< 7 | SimulatedPlaygroundStore | ConnectedPlaygroundStore 8 | >('holochain-playground/store'); 9 | -------------------------------------------------------------------------------- /packages/simulator/src/core/bad-agent.ts: -------------------------------------------------------------------------------- 1 | import { CellMap } from '@darksoil-studio/holochain-utils'; 2 | 3 | import { SimulatedDna } from '../dnas/simulated-dna.js'; 4 | 5 | export interface BadAgentConfig { 6 | disable_validation_before_publish: boolean; 7 | pretend_invalid_records_are_valid: boolean; 8 | } 9 | 10 | export interface BadAgent { 11 | config: BadAgentConfig; 12 | 13 | counterfeitDnas: CellMap; // Segmented by DnaHash / AgentPubKey 14 | } 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import tseslint from "typescript-eslint"; 5 | import eslintConfigPrettier from "eslint-config-prettier"; 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | eslintConfigPrettier, 11 | { 12 | rules: { 13 | "@typescript-eslint/no-unused-vars": "warn", 14 | "@typescript-eslint/no-explicit-any": "warn", 15 | }, 16 | } 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /fixture/Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.dev] 2 | opt-level = "z" 3 | 4 | [profile.release] 5 | opt-level = "z" 6 | 7 | [workspace] 8 | resolver = "2" 9 | members = ["dnas/*/zomes/coordinator/*", "dnas/*/zomes/integrity/*"] 10 | 11 | [workspace.dependencies] 12 | hdi = "0.6.0" 13 | hdk = "0.5.0" 14 | serde = "1.0.193" 15 | holochain_serialized_bytes = "*" 16 | 17 | [workspace.dependencies.posts] 18 | path = "dnas/forum/zomes/coordinator/posts" 19 | 20 | [workspace.dependencies.posts_integrity] 21 | path = "dnas/forum/zomes/integrity/posts" 22 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/sys_time.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, Timestamp } from '@holochain/client'; 2 | 3 | import { getCellId } from '../../cell/source-chain/utils.js'; 4 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 5 | 6 | export type SysTimeFn = () => Promise; 7 | 8 | // Creates a new Create action and its entry in the source chain 9 | export const sys_time: HostFn = 10 | (worskpace: HostFnWorkspace): SysTimeFn => 11 | async (): Promise => { 12 | return Date.now() * 1000; 13 | }; 14 | -------------------------------------------------------------------------------- /fixture/dnas/forum/workdir/dna.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | manifest_version: "1" 3 | name: forum 4 | integrity: 5 | network_seed: ~ 6 | properties: ~ 7 | zomes: 8 | - name: posts_integrity 9 | hash: ~ 10 | bundled: "../../../target/wasm32-unknown-unknown/release/posts_integrity.wasm" 11 | dependencies: ~ 12 | dylib: ~ 13 | coordinator: 14 | zomes: 15 | - name: posts 16 | hash: ~ 17 | bundled: "../../../target/wasm32-unknown-unknown/release/posts.wasm" 18 | dependencies: 19 | - name: posts_integrity 20 | dylib: ~ 21 | -------------------------------------------------------------------------------- /packages/cli/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "sourceMap": true, 6 | "baseUrl": ".", 7 | "types": ["node"], 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | }, 11 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"], 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "experimentalDecorators": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": ["./**/*.ts"], 19 | "exclude": ["node_modules/**/*", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/elements/src/elements/source-chain/graph.ts: -------------------------------------------------------------------------------- 1 | import { commonGraphStyles } from '../utils/common-graph-styles.js'; 2 | 3 | export const graphStyles = ` 4 | ${commonGraphStyles} 5 | node { 6 | width: 30px; 7 | height: 30px; 8 | font-size: 10px; 9 | label: data(label); 10 | text-halign: right; 11 | text-valign: center; 12 | text-margin-x: 4px; 13 | } 14 | 15 | .action { 16 | text-margin-x: -5px; 17 | text-halign: left; 18 | } 19 | 20 | .selected { 21 | border-width: 4px; 22 | border-color: black; 23 | border-style: solid; 24 | } 25 | 26 | `; 27 | -------------------------------------------------------------------------------- /packages/simulator/test/peers.test.js: -------------------------------------------------------------------------------- 1 | import { createConductors, demoHapp } from '../dist'; 2 | import { assert, describe, expect, it } from 'vitest'; 3 | import { sleep } from './utils'; 4 | 5 | describe('Peers', () => { 6 | it('conductors should discover neighbors', async function () { 7 | const conductors = await createConductors(10, [], demoHapp()); 8 | await sleep(2000); 9 | 10 | for (const c of conductors) { 11 | const neighborCount = c.getAllCells()[0].p2p.getState().neighbors.length; 12 | expect(neighborCount).to.be.greaterThan(2); 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn.ts: -------------------------------------------------------------------------------- 1 | import { SimulatedDna } from '../../dnas/simulated-dna.js'; 2 | import { Cascade } from '../cell/cascade/cascade.js'; 3 | import { CellState } from '../cell/state.js'; 4 | import { Conductor } from '../conductor.js'; 5 | import { P2pCell } from '../network/p2p-cell.js'; 6 | 7 | export type HostFn = ( 8 | hostFnWorkspace: HostFnWorkspace, 9 | zome_index: number, 10 | ) => Fn; 11 | 12 | export interface HostFnWorkspace { 13 | conductor_handle: Conductor; 14 | state: CellState; 15 | p2p: P2pCell; 16 | cascade: Cascade; 17 | dna: SimulatedDna; 18 | } 19 | -------------------------------------------------------------------------------- /fixture/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noEmitOnError": true, 7 | "useDefineForClassFields": false, 8 | "lib": ["es2017", "dom"], 9 | "strict": true, 10 | "esModuleInterop": false, 11 | "allowSyntheticDefaultImports": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "outDir": "dist", 15 | "sourceMap": true, 16 | "inlineSources": true, 17 | "incremental": true, 18 | "skipLibCheck": true 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.d.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/delete_entry.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash } from '@holochain/client'; 2 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 3 | import { common_delete } from './common/delete.js'; 4 | 5 | export type DeleteEntryFn = ( 6 | deletes_address: ActionHash 7 | ) => Promise; 8 | 9 | // Creates a new Create action and its entry in the source chain 10 | export const delete_entry: HostFn = 11 | (worskpace: HostFnWorkspace): DeleteEntryFn => 12 | async (deletes_address: ActionHash): Promise => { 13 | return common_delete(worskpace, deletes_address); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["es2017", "dom"], 7 | "allowJs": true, 8 | "strict": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "incremental": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "noImplicitThis": false, 15 | "alwaysStrict": true, 16 | "skipLibCheck": true, 17 | "allowSyntheticDefaultImports": true, 18 | "esModuleInterop": true, 19 | "useDefineForClassFields": false 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules/!(@open-wc)"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noEmitOnError": true, 7 | "lib": ["es2017", "dom"], 8 | "strict": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "outDir": "out-tsc", 14 | "sourceMap": true, 15 | "inlineSources": true, 16 | "rootDir": "./src", 17 | "useDefineForClassFields": false, 18 | "incremental": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": ["src/**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/delete_cap_grant.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash } from '@holochain/client'; 2 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 3 | import { common_delete } from './common/delete.js'; 4 | 5 | export type DeleteCapGrantFn = ( 6 | deletes_address: ActionHash 7 | ) => Promise; 8 | 9 | // Creates a new Create action and its entry in the source chain 10 | export const delete_cap_grant: HostFn = 11 | (worskpace: HostFnWorkspace): DeleteCapGrantFn => 12 | async (deletes_address): Promise => { 13 | return common_delete(worskpace, deletes_address); 14 | }; 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [*.json] 24 | indent_size = 2 25 | 26 | [*.{html,js,md}] 27 | block_comment_start = /** 28 | block_comment = * 29 | block_comment_end = */ 30 | -------------------------------------------------------------------------------- /packages/simulator/test/location.test.js: -------------------------------------------------------------------------------- 1 | import { shortest_arc_distance } from '../dist'; 2 | import { assert, describe, expect, it } from 'vitest'; 3 | 4 | describe('DHT Location', () => { 5 | it('location distance', async () => { 6 | expect(shortest_arc_distance(10, 5)).to.equal(5); 7 | expect(shortest_arc_distance(5, 10)).to.equal(5); 8 | expect(shortest_arc_distance(4294967295 + 5, 4294967295)).to.equal(5); 9 | expect(shortest_arc_distance(0, 4294967295)).to.equal(1); 10 | 11 | const MAX_HALF_LENGTH = Math.floor(4294967295 / 2) + 2; 12 | expect(shortest_arc_distance(0, MAX_HALF_LENGTH)).to.equal(MAX_HALF_LENGTH - 2); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/demo/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [*.json] 24 | indent_size = 2 25 | 26 | [*.{html,js,md}] 27 | block_comment_start = /** 28 | block_comment = * 29 | block_comment_end = */ 30 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/hash_entry.ts: -------------------------------------------------------------------------------- 1 | import { encodeHashToBase64, Entry, EntryHash } from '@holochain/client'; 2 | import { encode } from '@msgpack/msgpack'; 3 | import { hashEntry } from '../../cell/utils.js'; 4 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 5 | 6 | export type HashEntryFn = (args: any) => Promise; 7 | 8 | // Creates a new Create action and its entry in the source chain 9 | export const hash_entry: HostFn = 10 | (worskpace: HostFnWorkspace): HashEntryFn => 11 | async (args): Promise => { 12 | const entry: Entry = { entry_type: 'App', entry: encode(args) }; 13 | return hashEntry(entry); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/source-chain/put.ts: -------------------------------------------------------------------------------- 1 | import { Record } from '@holochain/client'; 2 | 3 | import { CellState } from '../state.js'; 4 | import { extractEntry, hashEntry } from '../utils.js'; 5 | 6 | export const putRecord = 7 | (record: Record) => 8 | (state: CellState): void => { 9 | // Put action in CAS 10 | const actionHash = record.signed_action.hashed.hash; 11 | state.CAS.set(actionHash, record.signed_action); 12 | 13 | // Put entry in CAS if it exist 14 | if ('Present' in record.entry) { 15 | const entryHash = hashEntry(extractEntry(record)!); 16 | state.CAS.set(entryHash, extractEntry(record)); 17 | } 18 | 19 | state.sourceChain.push(actionHash); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/simulator/src/core/network/gossip/types.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, DhtOp } from '@holochain/client'; 2 | import { DhtOpHash, ValidationReceipt } from '@darksoil-studio/holochain-core-types'; 3 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 4 | 5 | import { BadAction } from '../utils.js'; 6 | 7 | // From https://github.com/holochain/holochain/blob/develop/crates/kitsune_p2p/kitsune_p2p/src/types/gossip.rs 8 | 9 | export interface GossipData { 10 | validated_dht_ops: HoloHashMap; 11 | neighbors: Array; 12 | badActions: Array; 13 | } 14 | 15 | export interface GossipDhtOpData { 16 | op: DhtOp; 17 | validation_receipts: ValidationReceipt[]; 18 | } 19 | -------------------------------------------------------------------------------- /packages/simulator/src/core/network/dht_arc.ts: -------------------------------------------------------------------------------- 1 | import { shortest_arc_distance } from '../../processors/hash.js'; 2 | 3 | export interface DhtArc { 4 | center_loc: number; 5 | half_length: number; 6 | } 7 | 8 | export function contains(dht_arc: DhtArc, location: number): boolean { 9 | const do_hold_something = dht_arc.half_length !== 0; 10 | const only_hold_self = 11 | dht_arc.half_length === 1 && dht_arc.half_length === location; 12 | const dist_as_array_length = 13 | shortest_arc_distance(dht_arc.center_loc, location) + 1; 14 | 15 | const within_range = 16 | dht_arc.half_length > 1 && dist_as_array_length <= dht_arc.half_length; 17 | 18 | return do_hold_something && (only_hold_self || within_range); 19 | } 20 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/call_remote.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, CapSecret } from '@holochain/client'; 2 | 3 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 4 | 5 | export type CallRemoteFn = (args: { 6 | agent: AgentPubKey; 7 | zome: string; 8 | fn_name: string; 9 | cap_secret: CapSecret | undefined; 10 | payload: any; 11 | }) => Promise; 12 | 13 | // Creates a new Create action and its entry in the source chain 14 | export const call_remote: HostFn = 15 | (workspace: HostFnWorkspace): CallRemoteFn => 16 | async (args): Promise => { 17 | return workspace.p2p.call_remote( 18 | args.agent, 19 | args.zome, 20 | args.fn_name, 21 | args.cap_secret, 22 | args.payload, 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/cli/server/src/urls.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync } from 'fs'; 2 | 3 | export function getUrls() { 4 | if (process.argv.length > 2) return process.argv.slice(2); 5 | 6 | const ports = getLivePorts(); 7 | 8 | return ports.map(port => `ws://localhost:${port}`); 9 | } 10 | 11 | export function getLivePorts(): number[] { 12 | const files = getHcLiveFiles(); 13 | 14 | const fileContents = files.map(file => readFileSync(file, 'utf8')); 15 | return fileContents.map(c => parseInt(c, 10)); 16 | } 17 | 18 | export function getHcLiveFiles(): string[] { 19 | const currentDir = process.cwd(); 20 | 21 | const dirContents = readdirSync(currentDir); 22 | 23 | return dirContents.filter(name => name.startsWith('.hc_live')); 24 | } 25 | -------------------------------------------------------------------------------- /packages/elements/src/elements/entry-contents/entry-contents.stories.js: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | 3 | export default { 4 | title: 'Entry Contents', 5 | component: 'entry-contents', 6 | }; 7 | 8 | export const Simple = () => { 9 | return html` 10 | { 13 | const conductor = e.detail.conductors[0]; 14 | 15 | const cellId = conductor.getAllCells()[0].cellId; 16 | 17 | e.target.activeAgentPubKey = cellId[1]; 18 | e.target.activeHash = cellId[1]; 19 | }} 20 | > 21 | 22 | 23 | `; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/elements/src/elements/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import { encodeHashToBase64 } from '@holochain/client'; 2 | 3 | export function shortenStrRec(object: any, shorten = false): any { 4 | if (object === undefined || object === null) { 5 | return object; 6 | } else if (Array.isArray(object)) { 7 | return object.map(o => shortenStrRec(o, shorten)); 8 | } else if (typeof object === 'object') { 9 | if (object.buffer && ArrayBuffer.isView(object)) { 10 | const hash = encodeHashToBase64(object as Uint8Array); 11 | 12 | return shorten ? `${hash.slice(0, 7)}...` : hash; 13 | } 14 | const o: any = {}; 15 | for (const key of Object.keys(object)) { 16 | o[key] = shortenStrRec(object[key], shorten); 17 | } 18 | return o; 19 | } 20 | return object; 21 | } 22 | -------------------------------------------------------------------------------- /packages/simulator/src/processors/files.ts: -------------------------------------------------------------------------------- 1 | /* import { Conductor } from '../core/conductor.js'; 2 | import { hookUpConductors } from './message.js'; 3 | 4 | export function downloadFile(name: string, blob: Blob) { 5 | const url = window.URL.createObjectURL(blob); 6 | const a = document.createRecord('a'); 7 | a.style.display = 'none'; 8 | a.href = url; 9 | // the filename you want 10 | a.download = name; 11 | document.body.appendChild(a); 12 | a.click(); 13 | window.URL.revokeObjectURL(url); 14 | } 15 | 16 | export function fileToPlayground(json): PlaygroundContext { 17 | const conductors = json.conductors.map((c) => new Conductor(c)); 18 | hookUpConductors(conductors); 19 | return { 20 | ...json, 21 | conductors, 22 | }; 23 | } 24 | */ -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/get.ts: -------------------------------------------------------------------------------- 1 | import { AnyDhtHash, Record, encodeHashToBase64 } from '@holochain/client'; 2 | 3 | import { GetOptions, GetStrategy } from '../../../types.js'; 4 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 5 | 6 | export type GetFn = ( 7 | args: AnyDhtHash, 8 | options?: GetOptions, 9 | ) => Promise; 10 | 11 | // Creates a new Create action and its entry in the source chain 12 | export const get: HostFn = 13 | (workspace: HostFnWorkspace): GetFn => 14 | async (hash, options): Promise => { 15 | if (!hash) throw new Error(`Cannot get with undefined hash`); 16 | 17 | options = options || { strategy: GetStrategy.Contents }; 18 | return workspace.cascade.dht_get(hash, options); 19 | }; 20 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { setCustomElements } from '@storybook/web-components'; 2 | import ce from '../custom-elements.json'; 3 | 4 | setCustomElements(ce); 5 | 6 | export const parameters = { 7 | actions: { argTypesRegex: '^on[A-Z].*' }, 8 | }; 9 | 10 | import { 11 | HolochainPlaygroundContainer, 12 | DhtCells, 13 | SourceChain, 14 | EntryContents, 15 | EntryGraph, 16 | CallZomeFns, 17 | } from '../dist'; 18 | customElements.define( 19 | 'holochain-playground-container', 20 | HolochainPlaygroundContainer 21 | ); 22 | customElements.define('dht-cells', DhtCells); 23 | customElements.define('source-chain', SourceChain); 24 | customElements.define('entry-graph', EntryGraph); 25 | customElements.define('entry-contents', EntryContents); 26 | customElements.define('call-zome-fns', CallZomeFns); 27 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/get_links.ts: -------------------------------------------------------------------------------- 1 | import { AnyDhtHash, Link, LinkType } from '@holochain/client'; 2 | 3 | import { GetLinksOptions, GetStrategy } from '../../../types.js'; 4 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 5 | 6 | export type GetLinksFn = ( 7 | base_address: AnyDhtHash, 8 | link_type: LinkType, 9 | options?: GetLinksOptions, 10 | ) => Promise; 11 | 12 | export const get_links: HostFn = 13 | (workspace: HostFnWorkspace): GetLinksFn => 14 | async (base_address, link_type: LinkType, options): Promise => { 15 | if (!base_address) throw new Error(`Cannot get with undefined hash`); 16 | 17 | options = options || { strategy: GetStrategy.Contents }; 18 | return workspace.cascade.dht_get_links(base_address, link_type, options); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/common/create.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash, Entry, EntryType, Record } from '@holochain/client'; 2 | 3 | import { 4 | buildCreate, 5 | buildShh, 6 | } from '../../../../cell/source-chain/builder-actions.js'; 7 | import { putRecord } from '../../../../cell/source-chain/put.js'; 8 | import { HostFnWorkspace } from '../../../host-fn.js'; 9 | 10 | export function common_create( 11 | worskpace: HostFnWorkspace, 12 | entry: Entry, 13 | entry_type: EntryType 14 | ): ActionHash { 15 | const create = buildCreate(worskpace.state, entry, entry_type); 16 | 17 | const record: Record = { 18 | signed_action: buildShh(create), 19 | entry: { 20 | Present: entry 21 | } 22 | }; 23 | 24 | putRecord(record)(worskpace.state); 25 | 26 | return record.signed_action.hashed.hash; 27 | } 28 | -------------------------------------------------------------------------------- /packages/simulator/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core/utils.js'; 2 | export * from './core/cell/index.js'; 3 | export * from './core/network/network.js'; 4 | export * from './core/network/p2p-cell.js'; 5 | export * from './core/network/network-request.js'; 6 | export * from './core/network/kitsune_p2p.js'; 7 | export * from './core/network/utils.js'; 8 | export * from './core/conductor.js'; 9 | export * from './core/bad-agent.js'; 10 | 11 | export * as Hdk from './core/hdk/index.js'; 12 | 13 | export * from './dnas/simulated-dna.js'; 14 | export * from './dnas/demo-dna.js'; 15 | 16 | export * from './executor/middleware-executor.js'; 17 | export * from './executor/delay-middleware.js'; 18 | 19 | export * from './processors/hash.js'; 20 | export * from './processors/create-conductors.js'; 21 | export * from './selectors.js'; 22 | export * from './types.js'; 23 | -------------------------------------------------------------------------------- /fixture/.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "test" 2 | on: 3 | # Trigger the workflow on push or pull request, 4 | # but only for the main branch 5 | push: 6 | branches: [ main, develop ] 7 | pull_request: 8 | branches: [ main, develop ] 9 | 10 | jobs: 11 | testbuild: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install nix 17 | uses: cachix/install-nix-action@v18 18 | with: 19 | install_url: https://releases.nixos.org/nix/nix-2.12.0/install 20 | extra_nix_config: | 21 | experimental-features = flakes nix-command 22 | 23 | - uses: cachix/cachix-action@v10 24 | with: 25 | name: holochain-ci 26 | 27 | - name: Install and test 28 | run: | 29 | nix develop --command bash -c "npm i && npm t" 30 | 31 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/must_get_valid_record.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash, Record } from '@holochain/client'; 2 | 3 | import { GetStrategy } from '../../../types.js'; 4 | import { MissingDependenciesError } from '../../cell/workflows/app_validation/types.js'; 5 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 6 | 7 | export type MustGetValidRecordFn = (args: ActionHash) => Promise; 8 | 9 | export const must_get_valid_record: HostFn = 10 | (workspace: HostFnWorkspace): MustGetValidRecordFn => 11 | async (hash): Promise => { 12 | if (!hash) throw new Error(`Cannot get with undefined hash`); 13 | 14 | const record = await workspace.cascade.dht_get(hash, { 15 | strategy: GetStrategy.Contents, 16 | }); 17 | 18 | if (record) return record; 19 | 20 | throw new MissingDependenciesError([hash]); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/get_details.ts: -------------------------------------------------------------------------------- 1 | import { AnyDhtHash } from '@holochain/client'; 2 | import { Details } from '@darksoil-studio/holochain-core-types'; 3 | 4 | import { GetOptions, GetStrategy } from '../../../types.js'; 5 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 6 | 7 | export type GetDetailsFn = ( 8 | args: AnyDhtHash, 9 | options?: GetOptions, 10 | ) => Promise
; 11 | 12 | // Creates a new Create action and its entry in the source chain 13 | export const get_details: HostFn = 14 | (workspace: HostFnWorkspace): GetDetailsFn => 15 | async (hash, options): Promise
=> { 16 | if (!hash) throw new Error(`Cannot get with undefined hash`); 17 | 18 | options = options || { strategy: GetStrategy.Contents }; 19 | 20 | return workspace.cascade.dht_get_details(hash, options); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cli/server/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import semver from 'semver'; 3 | import chalk from 'chalk'; 4 | import { launchApp } from './app'; 5 | 6 | const pkg = require('../package.json'); 7 | 8 | console.log(`@holochain-playground/cli v${pkg.version}`); 9 | 10 | (async () => { 11 | try { 12 | if (!semver.gte(process.version, '14.0.0')) { 13 | console.log( 14 | chalk.bgRed('\nUh oh! Looks like you dont have Node v14 installed!\n') 15 | ); 16 | console.log(`You can do this by going to ${chalk.underline.blue( 17 | `https://nodejs.org/` 18 | )} 19 | Or if you use nvm: 20 | $ nvm install node ${chalk.gray( 21 | `# "node" is an alias for the latest version` 22 | )} 23 | $ nvm use node 24 | `); 25 | } else { 26 | launchApp(); 27 | } 28 | } catch (err) { 29 | console.log(err); 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/must_get_action.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash, SignedActionHashed } from '@holochain/client'; 2 | 3 | import { GetStrategy } from '../../../types.js'; 4 | import { MissingDependenciesError } from '../../cell/workflows/app_validation/types.js'; 5 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 6 | 7 | export type MustGetActionFn = (args: ActionHash) => Promise; 8 | 9 | export const must_get_action: HostFn = 10 | (workspace: HostFnWorkspace): MustGetActionFn => 11 | async (hash): Promise => { 12 | if (!hash) throw new Error(`Cannot get with undefined hash`); 13 | 14 | const action = await workspace.cascade.retrieve_action(hash, { 15 | strategy: GetStrategy.Latest, 16 | }); 17 | 18 | if (action) return action; 19 | 20 | throw new MissingDependenciesError([hash]); 21 | }; 22 | -------------------------------------------------------------------------------- /fixture/dnas/forum/zomes/integrity/posts/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod comment; 2 | pub use comment::*; 3 | pub mod post; 4 | use hdi::prelude::*; 5 | pub use post::*; 6 | #[derive(Serialize, Deserialize)] 7 | #[serde(tag = "type")] 8 | #[hdk_entry_types] 9 | #[unit_enum(UnitEntryTypes)] 10 | pub enum EntryTypes { 11 | Post(Post), 12 | Comment(Comment), 13 | } 14 | #[derive(Serialize, Deserialize)] 15 | #[hdk_link_types] 16 | pub enum LinkTypes { 17 | PostUpdates, 18 | PostToComments, 19 | AllPosts, 20 | } 21 | #[hdk_extern] 22 | pub fn genesis_self_check(_data: GenesisSelfCheckData) -> ExternResult { 23 | Ok(ValidateCallbackResult::Valid) 24 | } 25 | pub fn validate_agent_joining( 26 | _agent_pub_key: AgentPubKey, 27 | _membrane_proof: &Option, 28 | ) -> ExternResult { 29 | Ok(ValidateCallbackResult::Valid) 30 | } 31 | -------------------------------------------------------------------------------- /packages/elements/src/base/playground-element.ts: -------------------------------------------------------------------------------- 1 | import { consume } from '@lit/context'; 2 | import { SignalWatcher } from '@darksoil-studio/holochain-signals'; 3 | import { LitElement } from 'lit'; 4 | import { state } from 'lit/decorators.js'; 5 | 6 | import { ConnectedPlaygroundStore } from '../store/connected-playground-store.js'; 7 | import { SimulatedPlaygroundStore } from '../store/simulated-playground-store.js'; 8 | import { playgroundContext } from './context.js'; 9 | 10 | export class PlaygroundElement< 11 | T extends ConnectedPlaygroundStore | SimulatedPlaygroundStore = 12 | | ConnectedPlaygroundStore 13 | | SimulatedPlaygroundStore, 14 | > extends SignalWatcher(LitElement) { 15 | @consume({ context: playgroundContext, subscribe: true }) 16 | @state() 17 | private _store!: ConnectedPlaygroundStore | SimulatedPlaygroundStore; 18 | 19 | get store(): T { 20 | return this._store as T; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/elements/src/elements/dht-cells/utils.ts: -------------------------------------------------------------------------------- 1 | import { Signal } from '@darksoil-studio/holochain-signals'; 2 | 3 | let needsEnqueue = true; 4 | 5 | const w = new Signal.subtle.Watcher(() => { 6 | if (needsEnqueue) { 7 | needsEnqueue = false; 8 | queueMicrotask(processPending); 9 | } 10 | }); 11 | 12 | function processPending() { 13 | needsEnqueue = true; 14 | 15 | for (const s of w.getPending()) { 16 | s.get(); 17 | } 18 | 19 | w.watch(); 20 | } 21 | 22 | export function effect(callback: () => (() => void) | void) { 23 | let cleanup: (() => void) | void; 24 | 25 | const computed = new Signal.Computed(() => { 26 | typeof cleanup === 'function' && cleanup(); 27 | cleanup = callback(); 28 | }); 29 | 30 | w.watch(computed); 31 | computed.get(); 32 | 33 | return () => { 34 | w.unwatch(computed); 35 | typeof cleanup === 'function' && cleanup(); 36 | cleanup = undefined; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/simulator/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | ActionType, 4 | EntryHash, 5 | EntryType, 6 | } from '@holochain/client'; 7 | 8 | export type GetOptions = { 9 | strategy: GetStrategy; 10 | }; 11 | export type GetLinksOptions = {}; 12 | 13 | export enum GetStrategy { 14 | Latest, 15 | Contents, 16 | } 17 | 18 | export type ChainQueryFilterRange = 19 | | { 20 | Unbounded: undefined; 21 | } 22 | | { 23 | ActionSeqRange: [number, number]; 24 | } 25 | | { 26 | ActionHashRange: [ActionHash, ActionHash]; 27 | } 28 | | { 29 | ActionHashTerminated: [ActionHash, number]; 30 | }; 31 | 32 | export interface ChainQueryFilter { 33 | sequence_range: ChainQueryFilterRange; 34 | entry_type: Array | undefined; 35 | entry_hashes: Array | undefined; 36 | action_type: Array | undefined; 37 | include_entries: boolean; 38 | order_descending: boolean; 39 | } 40 | 41 | export type Dictionary = Record; 42 | -------------------------------------------------------------------------------- /fixture/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 14 | 15 | 29 | Forum 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cascade/cascade.js'; 2 | export * from './cascade/authority.js'; 3 | export * from './cascade/types.js'; 4 | 5 | export * from './dht/get.js'; 6 | export * from './dht/put.js'; 7 | 8 | export * from './source-chain/get.js'; 9 | export * from './source-chain/put.js'; 10 | export * from './source-chain/utils.js'; 11 | export * from './source-chain/builder-actions.js'; 12 | 13 | export * from './workflows/app_validation.js'; 14 | export * from './workflows/call_zome_fn.js'; 15 | export * from './workflows/genesis.js'; 16 | export * from './workflows/incoming_dht_ops.js'; 17 | export * from './workflows/integrate_dht_ops.js'; 18 | export * from './workflows/produce_dht_ops.js'; 19 | export * from './workflows/publish_dht_ops.js'; 20 | export * from './workflows/sys_validation.js'; 21 | export * from './workflows/workflows.js'; 22 | 23 | export * from './state.js'; 24 | export * from './utils.js'; 25 | export * from './cell.js'; 26 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/dna_info.ts: -------------------------------------------------------------------------------- 1 | import { DnaHash, DnaModifiers, ZomeName } from '@holochain/client'; 2 | import { encode } from '@msgpack/msgpack'; 3 | 4 | import { getCellId } from '../../cell/source-chain/utils.js'; 5 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 6 | 7 | export interface DnaInfo { 8 | name: string; 9 | hash: DnaHash; 10 | modifiers: DnaModifiers; 11 | zome_names: Array; 12 | } 13 | 14 | export type DnaInfoFn = () => Promise; 15 | 16 | export const dna_info: HostFn = 17 | (workspace: HostFnWorkspace): DnaInfoFn => 18 | async (): Promise => { 19 | const cellId = getCellId(workspace.state); 20 | const dnaHash = cellId[0]; 21 | return { 22 | name: '', 23 | hash: dnaHash, 24 | modifiers: { 25 | network_seed: workspace.dna.networkSeed, 26 | properties: encode(workspace.dna.properties), 27 | }, 28 | zome_names: workspace.dna.zomes.map(z => z.name), 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /fixture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forum-dev", 3 | "private": true, 4 | "workspaces": [ 5 | "ui", 6 | "tests" 7 | ], 8 | "scripts": { 9 | "start": "AGENTS=2 npm run network", 10 | "network": "hc s clean && npm run build:happ && cross-env UI_PORT=8888 concurrently -k \"npm start -w ui\" \"hc pilot workdir/forum.happ --ui-port 8888 --admin-port $ADMIN_PORT_2\" \"hc pilot workdir/forum.happ --ui-port 8888 --admin-port $ADMIN_PORT\" ", 11 | "test": "npm run build:happ && npm t -w tests", 12 | "build:happ": "npm run build:zomes && npm run pack:happ", 13 | "pack:happ": "hc dna pack dnas/forum/workdir && hc app pack workdir", 14 | "build:zomes": "RUSTFLAGS='' CARGO_TARGET_DIR=target cargo build --release --target wasm32-unknown-unknown" 15 | }, 16 | "devDependencies": { 17 | "cross-env": "^7.0.3", 18 | "concurrently": "^6.2.1", 19 | "rimraf": "^3.0.2", 20 | "new-port-cli": "^1.0.0" 21 | }, 22 | "engines": { 23 | "npm": ">=7.0.0" 24 | } 25 | } -------------------------------------------------------------------------------- /packages/elements/src/elements/dht-entries/entry-graph.stories.js: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | 3 | export default { 4 | title: 'Dht Entries', 5 | component: 'dht-entries', 6 | }; 7 | 8 | export const Simple = () => { 9 | return html` 10 | { 13 | const conductor = e.detail.conductors[0]; 14 | 15 | const cellId = conductor.getAllCells()[0].cellId; 16 | conductor.callZomeFn({ 17 | cellId, 18 | zome: 'demo_entries', 19 | fnName: 'create_entry', 20 | payload: { 21 | content: { test: 'bon dia pel matí!' }, 22 | entry_type: 'haha', 23 | }, 24 | cap: null, 25 | }); 26 | 27 | e.target.activeAgentPubKey = cellId[1]; 28 | }} 29 | > 30 | 31 | 32 | `; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/must_get_entry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyDhtHash, 3 | Entry, 4 | EntryHash, 5 | HoloHashed, 6 | Record, 7 | encodeHashToBase64, 8 | } from '@holochain/client'; 9 | 10 | import { GetStrategy } from '../../../types.js'; 11 | import { MissingDependenciesError } from '../../cell/workflows/app_validation/types.js'; 12 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 13 | 14 | export type MustGetEntryFn = (args: EntryHash) => Promise>; 15 | 16 | export const must_get_entry: HostFn = 17 | (workspace: HostFnWorkspace): MustGetEntryFn => 18 | async (hash): Promise> => { 19 | if (!hash) throw new Error(`Cannot get with undefined hash`); 20 | 21 | const entry = await workspace.cascade.retrieve_entry(hash, { 22 | strategy: GetStrategy.Latest, 23 | }); 24 | 25 | if (entry) 26 | return { 27 | content: entry, 28 | hash, 29 | }; 30 | 31 | throw new MissingDependenciesError([hash]); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/elements/src/elements/dht-entries/graph.ts: -------------------------------------------------------------------------------- 1 | import { commonGraphStyles } from '../utils/common-graph-styles.js'; 2 | 3 | export const graphStyles = ` 4 | ${commonGraphStyles} 5 | 6 | node { 7 | font-size: 10px; 8 | width: 16px; 9 | label: data(label); 10 | height: 16px; 11 | } 12 | 13 | .not-held { 14 | height: 10px; 15 | width: 10px; 16 | background-color: grey; 17 | opacity: 0.3; 18 | } 19 | 20 | .entry { 21 | background-color: grey; 22 | } 23 | 24 | node > node { 25 | height: 1px; 26 | } 27 | 28 | .selected { 29 | border-width: 1px; 30 | border-color: black; 31 | border-style: solid; 32 | } 33 | 34 | .update-edge { 35 | width: 1; 36 | line-style: dashed; 37 | } 38 | .updated { 39 | opacity: 0.5; 40 | } 41 | .deleted { 42 | opacity: 0.3 !important; 43 | } 44 | 45 | `; 46 | 47 | export const cytoscapeConfig = { 48 | boxSelectionEnabled: false, 49 | autoungrabify: false, 50 | userZoomingEnabled: true, 51 | userPanningEnabled: true, 52 | style: graphStyles, 53 | }; 54 | -------------------------------------------------------------------------------- /packages/demo/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## Open-wc Starter App 6 | 7 | [![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](https://github.com/open-wc) 8 | 9 | ## Quickstart 10 | 11 | To get started: 12 | 13 | ```sh 14 | npm init @open-wc 15 | # requires node 10 & npm 6 or higher 16 | ``` 17 | 18 | ## Scripts 19 | 20 | - `start` runs your app for development, reloading on file changes 21 | - `start:build` runs your app after it has been built using the build command 22 | - `build` builds your app and outputs it in your `dist` directory 23 | - `test` runs your test suite with Web Test Runner 24 | - `lint` runs the linter for your project 25 | 26 | ## Tooling configs 27 | 28 | For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project. 29 | 30 | If you customize the configuration a lot, you can consider moving them to individual files. -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/create_clone_cell.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CapSecret, 3 | CellId, 4 | CloneId, 5 | DnaHash, 6 | DnaModifiers, 7 | } from '@holochain/client'; 8 | 9 | import { Dictionary } from '../../../types.js'; 10 | import { Conductor } from '../../conductor.js'; 11 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 12 | 13 | export interface CreateCloneCellInput { 14 | cell_id: CellId; 15 | modifiers: Dictionary; 16 | membrane_proof: Uint8Array | undefined; 17 | name: string | undefined; 18 | } 19 | 20 | export interface ClonedCell { 21 | cell_id: CellId; 22 | clone_id: CloneId; 23 | original_dna_hash: DnaHash; 24 | dna_modifiers: DnaModifiers; 25 | name: string; 26 | enabled: boolean; 27 | } 28 | 29 | export type CreateCloneCellFn = (input: CreateCloneCellInput) => Promise; 30 | 31 | export const create_clone_cell: HostFn = 32 | (workspace: HostFnWorkspace): CreateCloneCellFn => 33 | async (input): Promise => { 34 | // UNIMPLEMENTED! 35 | }; 36 | -------------------------------------------------------------------------------- /packages/elements/src/elements/source-chain/source-chain.stories.js: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | 3 | export default { 4 | title: 'Source Chain', 5 | component: 'source-chain', 6 | }; 7 | 8 | export const Simple = () => { 9 | return html` 10 | { 13 | const conductor = e.detail.conductors[0]; 14 | 15 | const cellId = conductor.getAllCells()[0].cellId; 16 | conductor.callZomeFn({ 17 | cellId, 18 | zome: 'demo_entries', 19 | fnName: 'create_entry', 20 | payload: { 21 | content: { test: 'bon dia pel matí!' }, 22 | entry_type: 'haha', 23 | }, 24 | cap: null, 25 | }); 26 | 27 | e.target.activeAgentPubKey = cellId[1]; 28 | }} 29 | > 30 | 33 | 34 | `; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/get_link_details.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyDhtHash, 3 | Link, 4 | LinkType, 5 | SignedActionHashed, 6 | } from '@holochain/client'; 7 | 8 | import { GetLinksOptions, GetStrategy } from '../../../types.js'; 9 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 10 | 11 | export type LinkDetails = Array< 12 | [SignedActionHashed, Array] 13 | >; 14 | 15 | export type GetLinkDetailsFn = ( 16 | base_address: AnyDhtHash, 17 | link_type: LinkType, 18 | options?: GetLinksOptions, 19 | ) => Promise; 20 | 21 | export const get_link_details: HostFn = 22 | (workspace: HostFnWorkspace): GetLinkDetailsFn => 23 | async (base_address, link_type: LinkType, options): Promise => { 24 | if (!base_address) throw new Error(`Cannot get with undefined hash`); 25 | 26 | options = options || { strategy: GetStrategy.Contents }; 27 | return workspace.cascade.dht_get_link_details( 28 | base_address, 29 | link_type, 30 | options, 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 27 | Holochain Playground 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/elements/src/base/connected-playground-context.ts: -------------------------------------------------------------------------------- 1 | import { PropertyValues, html } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | 4 | import { ConnectedPlaygroundStore } from '../store/connected-playground-store.js'; 5 | import { BasePlaygroundContext } from './base-playground-context.js'; 6 | 7 | @customElement('connected-playground-context') 8 | export class ConnectedPlaygroundContext extends BasePlaygroundContext { 9 | @property() 10 | urls!: string[]; 11 | 12 | buildStore() { 13 | const store = new ConnectedPlaygroundStore(); 14 | store.setConductors(this.urls).then(() => { 15 | this.dispatchEvent( 16 | new CustomEvent('playground-ready', { 17 | bubbles: true, 18 | composed: true, 19 | detail: { 20 | store, 21 | }, 22 | }), 23 | ); 24 | }); 25 | return store; 26 | } 27 | 28 | updated(cv: PropertyValues) { 29 | super.updated(cv); 30 | 31 | if (this.store && cv.has('urls')) { 32 | (this.store as ConnectedPlaygroundStore).setConductors(this.urls); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/elements/src/store/polling-store.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSignal, AsyncState, Signal } from '@darksoil-studio/holochain-signals'; 2 | 3 | export function pollingSignal( 4 | pollingRequest: (currentState: T | undefined) => Promise, 5 | pollingIntervalMs = 1000, 6 | ): AsyncSignal { 7 | let interval: any = undefined; 8 | const signal = new AsyncState( 9 | { 10 | status: 'pending', 11 | }, 12 | { 13 | [Signal.subtle.watched]: () => { 14 | interval = setInterval(async () => { 15 | let currentValue: T | undefined; 16 | const currentResult = signal.get(); 17 | if (currentResult.status === 'completed') 18 | currentValue = currentResult.value; 19 | const value = await pollingRequest(currentValue); 20 | signal.set({ 21 | status: 'completed', 22 | value, 23 | }); 24 | }, pollingIntervalMs); 25 | }, 26 | [Signal.subtle.unwatched]: () => { 27 | signal.set({ 28 | status: 'pending', 29 | }); 30 | clearInterval(interval); 31 | interval = undefined; 32 | }, 33 | }, 34 | ); 35 | 36 | return signal; 37 | } 38 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/create_cap_grant.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | CapSecret, 5 | Entry, 6 | ZomeCallCapGrant, 7 | } from '@holochain/client'; 8 | 9 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 10 | import { common_create } from './common/create.js'; 11 | 12 | export type CreateCapGrantFn = ( 13 | cap_grant: ZomeCallCapGrant, 14 | ) => Promise; 15 | 16 | // Creates a new Create action and its entry in the source chain 17 | export const create_cap_grant: HostFn = 18 | (worskpace: HostFnWorkspace): CreateCapGrantFn => 19 | async (cap_grant: ZomeCallCapGrant): Promise => { 20 | if ( 21 | cap_grant.access.type === 'assigned' && 22 | cap_grant.access.value.assignees.find(a => !!a && !ArrayBuffer.isView(a)) 23 | ) { 24 | throw new Error('Tried to assign a capability to an invalid agent'); 25 | } 26 | 27 | const entry: Entry = { 28 | entry_type: 'CapGrant', 29 | entry: cap_grant, 30 | } as unknown as Entry; 31 | 32 | return common_create(worskpace, entry, 'CapGrant'); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/demo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 devcamp-8 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Holochain Playground 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/cascade/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Create, 3 | CreateLink, 4 | Delete, 5 | DeleteLink, 6 | Entry, 7 | EntryHash, 8 | EntryType, 9 | SignedActionHashed, 10 | Update, 11 | } from '@holochain/client'; 12 | 13 | import { ValidationStatus } from '../state.js'; 14 | 15 | export interface GetEntryResponse { 16 | entry: Entry; 17 | entry_type: EntryType; 18 | actions: SignedActionHashed[]; 19 | deletes: SignedActionHashed[]; 20 | updates: SignedActionHashed[]; 21 | } 22 | 23 | export interface GetRecordResponse { 24 | signed_action: SignedActionHashed; 25 | /// If there is an entry associated with this action it will be here 26 | maybe_entry: Entry | undefined; 27 | /// The validation status of this record. 28 | validation_status: ValidationStatus; 29 | /// All deletes on this action 30 | deletes: SignedActionHashed[]; 31 | /// Any updates on this entry. 32 | updates: SignedActionHashed[]; 33 | } 34 | 35 | export type GetResult = GetRecordResponse | GetEntryResponse; 36 | 37 | export interface GetLinksResponse { 38 | link_adds: SignedActionHashed[]; 39 | link_removes: SignedActionHashed[]; 40 | } 41 | -------------------------------------------------------------------------------- /packages/simulator/test/path.test.js: -------------------------------------------------------------------------------- 1 | import { createConductors, demoHapp } from '../dist'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { sleep } from './utils'; 4 | 5 | describe('Paths', () => { 6 | it('ensure a path', async function () { 7 | const conductors = await createConductors(10, [], demoHapp()); 8 | await sleep(200); 9 | 10 | const cell = conductors[0].getAllCells()[0]; 11 | 12 | await conductors[0].callZomeFn({ 13 | cellId: cell.cellId, 14 | cap: null, 15 | fnName: 'ensure_path', 16 | payload: { path: 'a.sample.path', link_type: 0 }, 17 | zome: 'demo_paths', 18 | }); 19 | const entryHash = await conductors[0].callZomeFn({ 20 | cellId: cell.cellId, 21 | cap: null, 22 | fnName: 'hash_entry', 23 | payload: { entry: 'a' }, 24 | zome: 'demo_entries', 25 | }); 26 | 27 | await sleep(200); 28 | 29 | const links = await conductors[0].callZomeFn({ 30 | cellId: cell.cellId, 31 | cap: null, 32 | fnName: 'get_links', 33 | payload: { base: entryHash, link_type: 0 }, 34 | zome: 'demo_links', 35 | }); 36 | expect(links.length).to.equal(1); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/close_chain.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AnyLinkableHash, 5 | DnaHash, 6 | LinkType, 7 | CloseChain as NativeCloseChain, 8 | Record, 9 | } from '@holochain/client'; 10 | 11 | import { 12 | MigrationTarget, 13 | buildCloseChain, 14 | buildCreateLink, 15 | buildShh, 16 | } from '../../../cell/source-chain/builder-actions.js'; 17 | import { putRecord } from '../../../cell/source-chain/put.js'; 18 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 19 | 20 | export type CloseChainFn = (args: { 21 | new_target: MigrationTarget; 22 | }) => Promise; 23 | 24 | // Creates a new CreateLink action in the source chain 25 | export const close_chain: HostFn = 26 | (worskpace: HostFnWorkspace, zome_index: number): CloseChainFn => 27 | async (args): Promise => { 28 | const closeChain = buildCloseChain(worskpace.state, args.new_target); 29 | 30 | const record: Record = { 31 | signed_action: buildShh(closeChain as unknown as NativeCloseChain), 32 | entry: { NotApplicable: undefined }, 33 | }; 34 | putRecord(record)(worskpace.state); 35 | return record.signed_action.hashed.hash; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cli/client/web-dev-server.config.mjs: -------------------------------------------------------------------------------- 1 | import { fromRollup } from '@web/dev-server-rollup'; 2 | import rollupCommonjs from '@rollup/plugin-commonjs'; 3 | // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr'; 4 | 5 | /** Use Hot Module replacement by adding --hmr to the start command */ 6 | const hmr = process.argv.includes('--hmr'); 7 | const commonjs = fromRollup(rollupCommonjs); 8 | 9 | export default /** @type {import('@web/dev-server').DevServerConfig} */ ({ 10 | open: '/', 11 | watch: !hmr, 12 | /** Resolve bare module imports */ 13 | nodeResolve: { 14 | preferBuiltins: false, 15 | browser: true, 16 | mainFields: ['browser', 'module', 'main'], 17 | }, 18 | 19 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */ 20 | // esbuildTarget: 'auto' 21 | 22 | /** Set appIndex to enable SPA routing */ 23 | // appIndex: 'demo/index.html', 24 | 25 | plugins: [ 26 | commonjs(), 27 | /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */ 28 | // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }), 29 | ], 30 | 31 | // See documentation for all available options 32 | }); 33 | -------------------------------------------------------------------------------- /packages/elements/src/elements/helpers/help-button.ts: -------------------------------------------------------------------------------- 1 | import { mdiHelpCircleOutline } from '@mdi/js'; 2 | import '@shoelace-style/shoelace/dist/components/button/button.js'; 3 | import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'; 4 | import SlDialog from '@shoelace-style/shoelace/dist/components/dialog/dialog.js'; 5 | import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'; 6 | import { wrapPathInSvg } from '@darksoil-studio/holochain-elements'; 7 | import { LitElement, html } from 'lit'; 8 | import { customElement, property, query } from 'lit/decorators.js'; 9 | 10 | @customElement('help-button') 11 | export class HelpButton extends LitElement { 12 | @property({ type: String }) 13 | heading!: string; 14 | 15 | @query('#help-dialog') 16 | _helpDialog!: SlDialog; 17 | 18 | renderHelpDialog() { 19 | return html` 20 | 21 | 22 | 23 | `; 24 | } 25 | 26 | render() { 27 | return html` 28 | ${this.renderHelpDialog()} 29 | this._helpDialog.show()} 32 | > 33 | `; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/elements/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base/context.js'; 2 | export * from './base/simulated-playground-context.js'; 3 | export * from './base/connected-playground-context.js'; 4 | export * from './base/base-playground-context.js'; 5 | export * from './base/playground-element.js'; 6 | export * from './base/selectors.js'; 7 | 8 | export * from './elements/call-zome-fns/index.js'; 9 | export * from './elements/run-steps/index.js'; 10 | export * from './elements/dht-cells/index.js'; 11 | export * from './elements/select-active-dna/index.js'; 12 | export * from './elements/conductor-admin/index.js'; 13 | export * from './elements/entry-contents/index.js'; 14 | export * from './elements/source-chain/index.js'; 15 | export * from './elements/dht-entries/index.js'; 16 | export * from './elements/conductor-happs/index.js'; 17 | export * from './elements/validation-queue/index.js'; 18 | 19 | export * from './elements/helpers/call-functions.js'; 20 | export * from './elements/helpers/search-dht-entry.js'; 21 | export * from './elements/helpers/cell-tasks.js'; 22 | export * from './elements/helpers/help-button.js'; 23 | export * from './elements/utils/shared-styles.js'; 24 | 25 | export * from './store/connected-playground-store.js'; 26 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/create_link.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AnyLinkableHash, 4 | LinkType, 5 | Record, 6 | } from '@holochain/client'; 7 | 8 | import { 9 | buildCreateLink, 10 | buildShh, 11 | } from '../../../cell/source-chain/builder-actions.js'; 12 | import { putRecord } from '../../../cell/source-chain/put.js'; 13 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 14 | 15 | export type CreateLinkFn = (args: { 16 | base: AnyLinkableHash; 17 | target: AnyLinkableHash; 18 | link_type: LinkType; 19 | tag: any; 20 | }) => Promise; 21 | 22 | // Creates a new CreateLink action in the source chain 23 | export const create_link: HostFn = 24 | (worskpace: HostFnWorkspace, zome_index: number): CreateLinkFn => 25 | async (args): Promise => { 26 | const createLink = buildCreateLink( 27 | worskpace.state, 28 | zome_index, 29 | args.base, 30 | args.target, 31 | args.link_type, 32 | args.tag, 33 | ); 34 | 35 | const element: Record = { 36 | signed_action: buildShh(createLink), 37 | entry: { NotApplicable: undefined }, 38 | }; 39 | putRecord(element)(worskpace.state); 40 | return element.signed_action.hashed.hash; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/elements/src/elements/validation-queue/vaadin-grid-template-renderer-column.ts: -------------------------------------------------------------------------------- 1 | import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js'; 2 | import * as c from '@vaadin/grid/src/vaadin-grid-column-mixin.js'; 3 | import { LitElement, TemplateResult, html, render } from 'lit'; 4 | import { customElement, property } from 'lit/decorators.js'; 5 | 6 | // @ts-ignore 7 | const mixin = c.GridColumnMixin as any; 8 | 9 | // @ts-ignore 10 | @customElement('vaadin-grid-template-renderer-column') 11 | export class VaadinGridTemplateRendererColumn extends mixin( 12 | PolylitMixin(LitElement), 13 | ) { 14 | @property({}) 15 | getId!: (item: any) => string; 16 | 17 | @property({}) 18 | templateRenderer!: (item: any) => TemplateResult; 19 | 20 | renderer = (root: HTMLElement, _: any, model: any) => { 21 | const id = this.getId(model.item); 22 | if (!this.instances[id]) { 23 | const div = document.createElement('div'); 24 | render(this.templateRenderer(model.item), div); 25 | this.instances[id] = div; 26 | } 27 | if (root.firstChild !== this.instances[id]) { 28 | root.innerHTML = ''; 29 | root.appendChild(this.instances[id]); 30 | } 31 | }; 32 | 33 | instances: { [key: string]: HTMLElement } = {}; 34 | } 35 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/trigger.ts: -------------------------------------------------------------------------------- 1 | import { app_validation_task } from './app_validation.js'; 2 | import { integrate_dht_ops_task } from './integrate_dht_ops.js'; 3 | import { produce_dht_ops_task } from './produce_dht_ops.js'; 4 | import { publish_dht_ops_task } from './publish_dht_ops.js'; 5 | import { sys_validation_task } from './sys_validation.js'; 6 | import { validation_receipt_task } from './validation_receipt.js'; 7 | import { Workflow, WorkflowType } from './workflows.js'; 8 | 9 | export function triggeredWorkflowFromType( 10 | type: WorkflowType 11 | ): Workflow { 12 | switch (type) { 13 | case WorkflowType.APP_VALIDATION: 14 | return app_validation_task(); 15 | case WorkflowType.INTEGRATE_DHT_OPS: 16 | return integrate_dht_ops_task(); 17 | case WorkflowType.PRODUCE_DHT_OPS: 18 | return produce_dht_ops_task(); 19 | case WorkflowType.PUBLISH_DHT_OPS: 20 | return publish_dht_ops_task(); 21 | case WorkflowType.SYS_VALIDATION: 22 | return sys_validation_task(); 23 | case WorkflowType.VALIDATION_RECEIPT: 24 | return validation_receipt_task(); 25 | default: 26 | throw new Error('Trying to trigger a workflow that cannot be triggered'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fixture/ui/src/forum/posts/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Record, 3 | ActionHash, 4 | DnaHash, 5 | SignedActionHashed, 6 | EntryHash, 7 | AgentPubKey, 8 | Create, 9 | Update, 10 | Delete, 11 | CreateLink, 12 | DeleteLink 13 | } from '@holochain/client'; 14 | 15 | export type PostsSignal = { 16 | type: 'EntryCreated'; 17 | action: SignedActionHashed; 18 | app_entry: EntryTypes; 19 | } | { 20 | type: 'EntryUpdated'; 21 | action: SignedActionHashed; 22 | app_entry: EntryTypes; 23 | original_app_entry: EntryTypes; 24 | } | { 25 | type: 'EntryDeleted'; 26 | action: SignedActionHashed; 27 | original_app_entry: EntryTypes; 28 | } | { 29 | type: 'LinkCreated'; 30 | action: SignedActionHashed; 31 | link_type: string; 32 | } | { 33 | type: 'LinkDeleted'; 34 | action: SignedActionHashed; 35 | link_type: string; 36 | }; 37 | 38 | export type EntryTypes = 39 | | ({ type: 'Comment'; } & Comment) 40 | | ({ type: 'Post'; } & Post); 41 | 42 | 43 | 44 | export interface Post { 45 | title: string; 46 | 47 | content: string; 48 | } 49 | 50 | 51 | 52 | 53 | 54 | export interface Comment { 55 | comment: string; 56 | 57 | post_hash: ActionHash; 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /packages/elements/src/base/base-playground-context.ts: -------------------------------------------------------------------------------- 1 | import { provide } from '@lit/context'; 2 | import { LitElement, css, html } from 'lit'; 3 | import { property, query } from 'lit/decorators.js'; 4 | 5 | import { sharedStyles } from '../elements/utils/shared-styles.js'; 6 | import { ConnectedPlaygroundStore } from '../store/connected-playground-store.js'; 7 | import { PlaygroundStore } from '../store/playground-store.js'; 8 | import { SimulatedPlaygroundStore } from '../store/simulated-playground-store.js'; 9 | import { playgroundContext } from './context.js'; 10 | 11 | export abstract class BasePlaygroundContext< 12 | T extends SimulatedPlaygroundStore | ConnectedPlaygroundStore, 13 | > extends LitElement { 14 | /** Context variables */ 15 | abstract buildStore(): T; 16 | 17 | @provide({ context: playgroundContext }) 18 | store!: SimulatedPlaygroundStore | ConnectedPlaygroundStore; 19 | 20 | firstUpdated() { 21 | const store = this.buildStore(); 22 | 23 | this.store = store; 24 | 25 | this.requestUpdate(); 26 | } 27 | 28 | render() { 29 | return html``; 30 | } 31 | 32 | static get styles() { 33 | return [ 34 | sharedStyles, 35 | css` 36 | :host { 37 | display: contents; 38 | } 39 | `, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/simulator/src/selectors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DhtOp, 3 | Entry, 4 | NewEntryAction, 5 | Record, 6 | SignedActionHashed, 7 | } from '@holochain/client'; 8 | import { ValidationStatus } from '@darksoil-studio/holochain-core-types'; 9 | 10 | import { isPublic } from './core/cell/index.js'; 11 | import { CellState } from './core/cell/state.js'; 12 | 13 | export function selectSourceChain(cellState: CellState): Record[] { 14 | const actionHashes = cellState.sourceChain; 15 | 16 | return actionHashes.map(hash => { 17 | const signed_action: SignedActionHashed = { ...cellState.CAS.get(hash) }; 18 | 19 | const newEntryAction = signed_action.hashed.content as NewEntryAction; 20 | const { entry_hash } = newEntryAction; 21 | let entry: Entry | undefined; 22 | if (entry_hash) { 23 | const storedEntry = cellState.CAS.get(entry_hash); 24 | if (storedEntry) { 25 | entry = { ...storedEntry }; 26 | } 27 | } 28 | 29 | const publicEntryType = isPublic(newEntryAction.entry_type); 30 | return { 31 | signed_action, 32 | entry: entry 33 | ? { 34 | Present: entry, 35 | } 36 | : publicEntryType 37 | ? { 38 | NotStored: undefined, 39 | } 40 | : { 41 | Hidden: undefined, 42 | }, 43 | }; 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/open_chain.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AnyLinkableHash, 5 | DnaHash, 6 | LinkType, 7 | OpenChain as NativeOpenChain, 8 | Record, 9 | } from '@holochain/client'; 10 | 11 | import { 12 | MigrationTarget, 13 | buildCreateLink, 14 | buildOpenChain, 15 | buildShh, 16 | } from '../../../cell/source-chain/builder-actions.js'; 17 | import { putRecord } from '../../../cell/source-chain/put.js'; 18 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 19 | 20 | export type OpenChainFn = (args: { 21 | prev_target: MigrationTarget; 22 | close_hash: ActionHash; 23 | }) => Promise; 24 | 25 | // Creates a new CreateLink action in the source chain 26 | export const open_chain: HostFn = 27 | (worskpace: HostFnWorkspace, zome_index: number): OpenChainFn => 28 | async (args): Promise => { 29 | const openChain = buildOpenChain( 30 | worskpace.state, 31 | args.prev_target, 32 | args.close_hash, 33 | ); 34 | 35 | const record: Record = { 36 | signed_action: buildShh(openChain as unknown as NativeOpenChain), 37 | entry: { NotApplicable: undefined }, 38 | }; 39 | putRecord(record)(worskpace.state); 40 | return record.signed_action.hashed.hash; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain-playground/demo", 3 | "description": "Webcomponent devcamp-8 following open-wc recommendations", 4 | "private": true, 5 | "scripts": { 6 | "start": "vite --open", 7 | "build": "vite build", 8 | "lint": "eslint src && prettier \"**/*.ts\" --check --ignore-path .gitignore", 9 | "format": "eslint src && prettier \"**/*.ts\" --write --ignore-path .gitignore", 10 | "publish": "npm run build && gh-pages -d ./dist -b gh-pages" 11 | }, 12 | "dependencies": { 13 | "@darksoil-studio/holochain-core-types": "^0.500.0", 14 | "@darksoil-studio/holochain-elements": "^0.500.0", 15 | "@holochain-playground/simulator": "workspace:^0.500.0", 16 | "@holochain-playground/elements": "workspace:^0.500.0", 17 | "@mdi/js": "^7.4.47", 18 | "@shoelace-style/shoelace": "^2.16.0", 19 | "lit": "^3.0.0", 20 | "lodash-es": "^4.17.21" 21 | }, 22 | "devDependencies": { 23 | "@custom-elements-manifest/analyzer": "^0.5.7", 24 | "@types/lodash-es": "^4.17.5", 25 | "deepmerge": "^4.2.2", 26 | "gh-pages": "^3.2.3", 27 | "eslint": "^9.0.0", 28 | "prettier": "^3.2.5", 29 | "tslib": "^2.3.1", 30 | "typescript": "^5.4.0", 31 | "vite": "^4.1.2", 32 | "vite-plugin-checker": "^0.5.6" 33 | }, 34 | "customElements": "custom-elements.json" 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/server/README.md: -------------------------------------------------------------------------------- 1 | # @holochain-playground/cli 2 | 3 | Small CLI utility to run the [Holochain Playground](https://holochain-playground.github.io/) connected to a real Holochain conductor. 4 | 5 | This is useful as an introspection tool, to understand what's really going on in your Holochain node. 6 | 7 | ## Running directly pointing to a running conductor 8 | 9 | ```bash 10 | npx @holochain-playground/cli ws://localhost:8888 ws://localhost:8889 11 | ``` 12 | 13 | This URL should point to the Admin interfaces of the conductors. 14 | 15 | ## Setting up in an NPM hApp development environment that uses `hc sandbox` 16 | 17 | If you run this CLI from the same folder from which you run your hc sandboxes, it will automatically connect with the conductors that are live. 18 | 19 | 1. Install the CLI with: 20 | 21 | ```bash 22 | npm install -D @holochain-playground/cli 23 | ``` 24 | 25 | 2. Add a `playground` script in your `package.json`: 26 | 27 | ```json 28 | { 29 | ... 30 | "scripts": { 31 | "start": "concurrently \"npm run start:hc\" \"npm run playground\"", 32 | "start:hc": "hc s generate --run=8888", 33 | "playground": "holochain-playground" 34 | } 35 | } 36 | ``` 37 | 38 | Now, when you run `npm start`, it will bring up the playground connected to the conductor. -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/get_agent_activity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AnyDhtHash, 5 | Record, 6 | Warrant, 7 | encodeHashToBase64, 8 | } from '@holochain/client'; 9 | 10 | import { ChainQueryFilter, GetOptions, GetStrategy } from '../../../types.js'; 11 | import { ChainStatus, HighestObserved } from '../../cell/state/metadata.js'; 12 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 13 | 14 | // TODO: implement 15 | export interface ActivityRequest {} 16 | 17 | export interface AgentActivity { 18 | valid_activity: Array<[number, ActionHash]>; 19 | rejected_activity: Array<[number, ActionHash]>; 20 | status: ChainStatus; 21 | highest_observed: HighestObserved | undefined; 22 | warrants: Array; 23 | } 24 | 25 | export type GetAgentActivityFn = ( 26 | agent: AgentPubKey, 27 | query: ChainQueryFilter, 28 | request: ActivityRequest, 29 | ) => Promise; 30 | 31 | export const get_agent_activity: HostFn = 32 | (workspace: HostFnWorkspace): GetAgentActivityFn => 33 | async (agent, query, request): Promise => { 34 | if (!agent) 35 | throw new Error(`Cannot get_agent_activity with undefined agent`); 36 | 37 | return workspace.cascade.dht_get_agent_activity(agent, query, request); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/agent_info.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash, AgentPubKey, Timestamp } from '@holochain/client'; 2 | 3 | import { 4 | getCellId, 5 | getRecord, 6 | getTipOfChain, 7 | } from '../../cell/source-chain/utils.js'; 8 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 9 | 10 | export interface AgentInfo { 11 | agent_initial_pubkey: AgentPubKey; 12 | agent_latest_pubkey: AgentPubKey; 13 | chain_head: [ActionHash, number, Timestamp]; 14 | } 15 | 16 | export type AgentInfoFn = () => Promise; 17 | 18 | // Creates a new Create action and its entry in the source chain 19 | export const agent_info: HostFn = 20 | (workspace: HostFnWorkspace): AgentInfoFn => 21 | async (): Promise => { 22 | const cellId = getCellId(workspace.state); 23 | const agentPubKey = cellId[1]; 24 | 25 | const chainTipHash = getTipOfChain(workspace.state); 26 | const chainTip = getRecord(workspace.state, chainTipHash); 27 | 28 | const content = chainTip.signed_action.hashed.content; 29 | 30 | const sequenceNumber = (content as { action_seq: number }).action_seq || 0; 31 | 32 | return { 33 | agent_initial_pubkey: agentPubKey, 34 | agent_latest_pubkey: agentPubKey, 35 | chain_head: [chainTipHash, sequenceNumber, content.timestamp], 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/simulator/src/core/network/network-request.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, DhtOp, DnaHash } from '@holochain/client'; 2 | import { AnyDhtHashB64, DhtOpHash } from '@darksoil-studio/holochain-core-types'; 3 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 4 | 5 | import { GetOptions } from '../../types.js'; 6 | import { Cell } from '../cell/cell.js'; 7 | 8 | export enum NetworkRequestType { 9 | CALL_REMOTE = 'Call Remote', 10 | PUBLISH_REQUEST = 'Publish Request', 11 | GET_REQUEST = 'Get Request', 12 | WARRANT = 'Warrant', 13 | GOSSIP = 'Gossip', 14 | CONNECT = 'Connect', 15 | } 16 | 17 | export type NetworkRequest = (cell: Cell) => Promise; 18 | 19 | export interface NetworkRequestInfo { 20 | dnaHash: DnaHash; 21 | fromAgent: AgentPubKey; 22 | toAgent: AgentPubKey; 23 | type: T; 24 | details: D; 25 | } 26 | 27 | export type PublishRequestInfo = NetworkRequestInfo< 28 | NetworkRequestType.PUBLISH_REQUEST, 29 | { 30 | dhtOps: HoloHashMap; 31 | } 32 | >; 33 | 34 | export type GetRequestInfo = NetworkRequestInfo< 35 | NetworkRequestType.GET_REQUEST, 36 | { 37 | hash: AnyDhtHashB64; 38 | options: GetOptions; 39 | } 40 | >; 41 | 42 | export type CallRemoteRequestInfo = NetworkRequestInfo< 43 | NetworkRequestType.CALL_REMOTE, 44 | {} 45 | >; 46 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/common/delete.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash, NewEntryAction, Record } from '@holochain/client'; 2 | 3 | import { GetStrategy } from '../../../../../types.js'; 4 | import { 5 | buildDelete, 6 | buildShh, 7 | } from '../../../../cell/source-chain/builder-actions.js'; 8 | import { putRecord } from '../../../../cell/source-chain/put.js'; 9 | import { HostFnWorkspace } from '../../../host-fn.js'; 10 | 11 | export async function common_delete( 12 | worskpace: HostFnWorkspace, 13 | action_hash: ActionHash, 14 | ): Promise { 15 | const actionToDelete = await worskpace.cascade.retrieve_action(action_hash, { 16 | strategy: GetStrategy.Contents, 17 | }); 18 | 19 | if (!actionToDelete) throw new Error('Could not find record to be deleted'); 20 | 21 | const deletesEntryAddress = (actionToDelete.hashed.content as NewEntryAction) 22 | .entry_hash; 23 | 24 | if (!deletesEntryAddress) 25 | throw new Error(`Trying to delete an record with no entry`); 26 | 27 | const deleteAction = buildDelete( 28 | worskpace.state, 29 | action_hash, 30 | deletesEntryAddress, 31 | ); 32 | 33 | const record: Record = { 34 | signed_action: buildShh(deleteAction), 35 | entry: { NotApplicable: undefined }, 36 | }; 37 | putRecord(record)(worskpace.state); 38 | 39 | return record.signed_action.hashed.hash; 40 | } 41 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/must_get_agent_activity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AnyDhtHash, 5 | Record, 6 | RegisterAgentActivity, 7 | Warrant, 8 | encodeHashToBase64, 9 | } from '@holochain/client'; 10 | 11 | import { ChainQueryFilter, GetOptions, GetStrategy } from '../../../types.js'; 12 | import { ChainStatus, HighestObserved } from '../../cell/state/metadata.js'; 13 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 14 | 15 | export type ChainFilters = 16 | | { 17 | ToGenesis: void; 18 | } 19 | | { 20 | Take: number; 21 | } 22 | | { 23 | Until: Array; 24 | } 25 | | { 26 | Both: [number, Array]; 27 | }; 28 | 29 | export interface ChainFilter { 30 | chain_top: ActionHash; 31 | filters: Array; 32 | include_chached_entries: boolean; 33 | } 34 | 35 | export type MustGetAgentActivityFn = ( 36 | agent: AgentPubKey, 37 | filter: ChainFilter, 38 | ) => Promise>; 39 | 40 | export const must_get_agent_activity: HostFn = 41 | (workspace: HostFnWorkspace): MustGetAgentActivityFn => 42 | async (agent, filter): Promise> => { 43 | if (!agent) 44 | throw new Error(`Cannot get_agent_activity with undefined agent`); 45 | 46 | return workspace.cascade.dht_must_get_agent_activity(agent, filter); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/elements/src/base/simulated-playground-context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SimulatedHappBundle, 3 | createConductors, 4 | demoHapp, 5 | } from '@holochain-playground/simulator'; 6 | import { customElement, property } from 'lit/decorators.js'; 7 | 8 | import { 9 | SimulatedConductorStore, 10 | SimulatedPlaygroundStore, 11 | } from '../store/simulated-playground-store.js'; 12 | import { BasePlaygroundContext } from './base-playground-context.js'; 13 | 14 | @customElement('simulated-playground-context') 15 | export class SimulatedPlaygroundContext extends BasePlaygroundContext { 16 | @property({ type: Number }) 17 | numberOfSimulatedConductors: number = 10; 18 | 19 | @property({ type: Object }) 20 | simulatedHapp: SimulatedHappBundle = demoHapp(); 21 | 22 | /** Context variables */ 23 | 24 | buildStore() { 25 | const store = new SimulatedPlaygroundStore([], this.simulatedHapp); 26 | createConductors( 27 | this.numberOfSimulatedConductors, 28 | [], 29 | this.simulatedHapp, 30 | ).then(conductors => { 31 | store.conductors.set(conductors.map(c => new SimulatedConductorStore(c))); 32 | store.activeDna.set(conductors[0].cells.cellIds()[0][0]); 33 | this.dispatchEvent( 34 | new CustomEvent('playground-ready', { 35 | bubbles: true, 36 | composed: true, 37 | detail: { 38 | store, 39 | }, 40 | }), 41 | ); 42 | }); 43 | return store; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/simulator/src/core/network/connection.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey } from '@holochain/client'; 2 | import { areEqual } from '../../processors/hash.js'; 3 | 4 | import { Cell } from '../cell/cell.js'; 5 | import { NetworkRequest } from './network-request.js'; 6 | 7 | export class Connection { 8 | private _closed = false; 9 | 10 | get closed() { 11 | return this._closed; 12 | } 13 | 14 | close() { 15 | this._closed = false; 16 | } 17 | 18 | constructor(public opener: Cell, public receiver: Cell) { 19 | if ( 20 | opener.p2p.badAgents.find((a) => areEqual(a, receiver.agentPubKey)) || 21 | receiver.p2p.badAgents.find((a) => areEqual(a, opener.agentPubKey)) 22 | ) { 23 | throw new Error('Connection closed!'); 24 | } 25 | } 26 | 27 | sendRequest( 28 | fromAgent: AgentPubKey, 29 | networkRequest: NetworkRequest 30 | ): Promise { 31 | if (this.closed) throw new Error('Connection closed!'); 32 | 33 | if (areEqual(this.opener.agentPubKey, fromAgent)) { 34 | return networkRequest(this.receiver); 35 | } else if (areEqual(this.receiver.agentPubKey, fromAgent)) { 36 | return networkRequest(this.opener); 37 | } 38 | throw new Error('Bad request'); 39 | } 40 | 41 | getPeer(myAgentPubKey: AgentPubKey): Cell { 42 | if (areEqual(myAgentPubKey, this.opener.agentPubKey)) return this.receiver; 43 | return this.opener; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/simulator/test/conductor.test.js: -------------------------------------------------------------------------------- 1 | import { createConductors, demoHapp } from '../dist'; 2 | import { assert, describe, expect, it } from 'vitest'; 3 | import { sleep } from './utils'; 4 | 5 | describe('Conductor', () => { 6 | it('create conductors and call zome fn', async function () { 7 | const conductors = await createConductors(10, [], demoHapp()); 8 | await sleep(10000); 9 | 10 | const cell = conductors[0].getAllCells()[0]; 11 | 12 | let hash = await conductors[0].callZomeFn({ 13 | cellId: cell.cellId, 14 | cap: null, 15 | fnName: 'create_entry', 16 | payload: { content: 'hi' }, 17 | zome: 'demo_entries', 18 | }); 19 | 20 | expect(hash).to.be.ok; 21 | await sleep(5000); 22 | expect( 23 | Array.from(cell.getState().integratedDHTOps.keys()).length 24 | ).to.be.greaterThan(6); 25 | 26 | let getresult = await conductors[0].callZomeFn({ 27 | cellId: cell.cellId, 28 | cap: null, 29 | fnName: 'get', 30 | payload: { 31 | hash, 32 | }, 33 | zome: 'demo_entries', 34 | }); 35 | 36 | expect(getresult).to.be.ok; 37 | 38 | getresult = await conductors[0].callZomeFn({ 39 | cellId: cell.cellId, 40 | cap: null, 41 | fnName: 'get', 42 | payload: { 43 | hash, 44 | }, 45 | zome: 'demo_entries', 46 | }); 47 | 48 | expect(getresult).to.be.ok; 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/create_entry.ts: -------------------------------------------------------------------------------- 1 | import { Entry, ActionHash, EntryType } from '@holochain/client'; 2 | import { encode } from '@msgpack/msgpack'; 3 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 4 | import { common_create } from './common/create.js'; 5 | 6 | export type CreateEntryFn = (args: { 7 | content: any; 8 | entry_def_id: string; 9 | }) => Promise; 10 | 11 | // Creates a new Create action and its entry in the source chain 12 | export const create_entry: HostFn = 13 | (workspace: HostFnWorkspace, zome_index: number): CreateEntryFn => 14 | async (args: { content: any; entry_def_id: string }): Promise => { 15 | const entry: Entry = { entry_type: 'App', entry: encode(args.content) }; 16 | 17 | const entryDefIndex = workspace.dna.zomes[zome_index].entry_defs.findIndex( 18 | (entry_def) => entry_def.id === args.entry_def_id 19 | ); 20 | if (entryDefIndex < 0) { 21 | throw new Error( 22 | `Given entry def id ${args.entry_def_id} does not exist in this zome` 23 | ); 24 | } 25 | 26 | const entry_type: EntryType = { 27 | App: { 28 | entry_index: entryDefIndex, 29 | zome_index, 30 | visibility: 31 | workspace.dna.zomes[zome_index].entry_defs[entryDefIndex].visibility, 32 | }, 33 | }; 34 | 35 | return common_create(workspace, entry, entry_type); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/elements/src/elements/dht-cells/graph.ts: -------------------------------------------------------------------------------- 1 | import { commonGraphStyles } from '../utils/common-graph-styles.js'; 2 | 3 | export const layoutConfig = { 4 | startAngle: (4 / 2) * Math.PI, 5 | ready: (e: any) => { 6 | e.cy.resize(); 7 | }, 8 | }; 9 | 10 | export const graphStyles = ` 11 | ${commonGraphStyles} 12 | node { 13 | background-color: lightblue; 14 | border-color: black; 15 | border-width: 2px; 16 | label: data(label); 17 | font-size: 20px; 18 | width: 50px; 19 | height: 50px; 20 | } 21 | 22 | .selected { 23 | border-width: 4px; 24 | border-color: black; 25 | border-style: solid; 26 | } 27 | 28 | .highlighted { 29 | background-color: yellow; 30 | } 31 | 32 | edge { 33 | width: 1; 34 | } 35 | 36 | .network-request { 37 | target-arrow-shape: triangle; 38 | label: data(label); 39 | width: 10px; 40 | height: 10px; 41 | background-color: grey; 42 | border-width: 0px; 43 | } 44 | 45 | .neighbor-edge { 46 | line-style: solid; 47 | } 48 | 49 | .far-neighbor-edge { 50 | line-style: dashed; 51 | } 52 | 53 | .not-held { 54 | height: 10px; 55 | width: 10px; 56 | background-color: grey; 57 | opacity: 0.3; 58 | } 59 | `; 60 | 61 | export const cytoscapeOptions = { 62 | boxSelectionEnabled: false, 63 | autoungrabify: true, 64 | userPanningEnabled: false, 65 | userZoomingEnabled: false, 66 | style: graphStyles, 67 | }; 68 | -------------------------------------------------------------------------------- /packages/cli/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const webpackPath = require('webpack-path-resolve'); 5 | require('dotenv').config(); 6 | 7 | const resolve = webpackPath.resolve(require.resolve.paths); 8 | 9 | module.exports = { 10 | entry: './src/index.ts', 11 | target: 'node', 12 | mode: process.env.NODE_ENV, 13 | devtool: 14 | process.env.NODE_ENV === 'development' ? 'eval-source-map' : 'source-map', 15 | watchOptions: {}, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | use: 'ts-loader', 21 | exclude: /node_modules/, 22 | }, 23 | { 24 | test: /\.m?js$/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['@babel/preset-env'], 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | extensions: ['.tsx', '.ts', '.js'], 36 | }, 37 | output: { 38 | globalObject: 'this', 39 | filename: 'app.js', 40 | path: path.resolve(__dirname, 'dist'), 41 | }, 42 | node: { 43 | global: false, 44 | __filename: false, 45 | __dirname: false, 46 | }, 47 | plugins: [ 48 | new CopyPlugin({ 49 | patterns: [ 50 | { 51 | from: `${resolve('@holochain-playground/cli-client')}/dist`, 52 | to: './public/', 53 | }, 54 | ], 55 | }), 56 | new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true }), 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /packages/elements/src/elements/utils/common-graph-styles.ts: -------------------------------------------------------------------------------- 1 | export const commonGraphStyles = ` 2 | .action { 3 | } 4 | 5 | .entry { 6 | shape: round-rectangle; 7 | } 8 | 9 | .Dna { 10 | background-color: green; 11 | } 12 | .AgentValidationPkg { 13 | background-color: lime; 14 | } 15 | .Create { 16 | background-color: blue; 17 | } 18 | .Delete { 19 | background-color: red; 20 | } 21 | .Update { 22 | background-color: cyan; 23 | } 24 | .CreateLink { 25 | background-color: purple; 26 | } 27 | .DeleteLink { 28 | background-color: #fc0388; 29 | } 30 | .OpenChain { 31 | background-color: #03fc66; 32 | } 33 | .CloseChain { 34 | background-color: black; 35 | } 36 | 37 | .embedded-reference { 38 | width: 4; 39 | target-arrow-shape: triangle; 40 | curve-style: bezier; 41 | line-style: dotted; 42 | } 43 | .embedded-reference[label] { 44 | label: data(label); 45 | font-size: 7px; 46 | text-rotation: autorotate; 47 | text-margin-x: 0px; 48 | text-margin-y: -5px; 49 | text-valign: top; 50 | text-halign: center; 51 | } 52 | 53 | .explicit-link { 54 | width: 2; 55 | target-arrow-shape: triangle; 56 | curve-style: bezier; 57 | } 58 | 59 | .explicit-link[label] { 60 | label: data(label); 61 | font-size: 7px; 62 | text-rotation: autorotate; 63 | text-margin-x: 0px; 64 | text-margin-y: -5px; 65 | text-valign: top; 66 | text-halign: center; 67 | } 68 | 69 | .not-held { 70 | height: 1px; 71 | width: 1px; 72 | } 73 | `; 74 | -------------------------------------------------------------------------------- /fixture/tests/src/forum/posts/common.ts: -------------------------------------------------------------------------------- 1 | import { CallableCell } from '@holochain/tryorama'; 2 | import { NewEntryAction, ActionHash, Record, AppBundleSource, fakeActionHash, fakeAgentPubKey, fakeEntryHash, fakeDnaHash } from '@holochain/client'; 3 | 4 | 5 | 6 | export async function samplePost(cell: CallableCell, partialPost = {}) { 7 | return { 8 | ...{ 9 | title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 10 | content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 11 | }, 12 | ...partialPost 13 | }; 14 | } 15 | 16 | export async function createPost(cell: CallableCell, post = undefined): Promise { 17 | return cell.callZome({ 18 | zome_name: "posts", 19 | fn_name: "create_post", 20 | payload: post || await samplePost(cell), 21 | }); 22 | } 23 | 24 | 25 | 26 | export async function sampleComment(cell: CallableCell, partialComment = {}) { 27 | return { 28 | ...{ 29 | comment: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 30 | post_hash: (await createPost(cell)).signed_action.hashed.hash, 31 | }, 32 | ...partialComment 33 | }; 34 | } 35 | 36 | export async function createComment(cell: CallableCell, comment = undefined): Promise { 37 | return cell.callZome({ 38 | zome_name: "posts", 39 | fn_name: "create_comment", 40 | payload: comment || await sampleComment(cell), 41 | }); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/workflows.ts: -------------------------------------------------------------------------------- 1 | import { SimulatedDna } from '../../../dnas/simulated-dna.js'; 2 | import { BadAgentConfig } from '../../bad-agent.js'; 3 | import { Conductor } from '../../conductor.js'; 4 | import { P2pCell } from '../../network/p2p-cell.js'; 5 | import { CellState } from '../state.js'; 6 | 7 | export interface Workspace { 8 | conductor_handle: Conductor; 9 | state: CellState; 10 | p2p: P2pCell; 11 | dna: SimulatedDna; 12 | badAgentConfig?: BadAgentConfig & { counterfeit_dna?: SimulatedDna }; 13 | } 14 | 15 | export interface Workflow { 16 | type: WorkflowType; 17 | details: D; 18 | task: (worskpace: Workspace) => Promise>; 19 | } 20 | export type WorkflowReturn = { 21 | result: R; 22 | triggers: Array>; 23 | }; 24 | 25 | export enum WorkflowType { 26 | CALL_ZOME = 'Call Zome Function', 27 | SYS_VALIDATION = 'System Validation', 28 | PUBLISH_DHT_OPS = 'Publish DHT Ops', 29 | PRODUCE_DHT_OPS = 'Produce DHT Ops', 30 | APP_VALIDATION = 'App Validation', 31 | AGENT_VALIDATION = 'Validate Agent', 32 | INTEGRATE_DHT_OPS = 'Integrate DHT Ops', 33 | GENESIS = 'Genesis', 34 | INCOMING_DHT_OPS = 'Incoming DHT Ops', 35 | VALIDATION_RECEIPT = 'Validation Receipt', 36 | } 37 | 38 | export function workflowPriority(workflowType: WorkflowType): number { 39 | switch (workflowType) { 40 | case WorkflowType.GENESIS: 41 | return 0; 42 | case WorkflowType.CALL_ZOME: 43 | return 1; 44 | default: 45 | return 10; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/simulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain-playground/simulator", 3 | "version": "0.500.1", 4 | "description": "", 5 | "author": "guillem.cordoba@gmail.com", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/index.js", 9 | "files": [ 10 | "src", 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "tsc", 15 | "build:watch": "tsc --watch --preserveWatchOutput", 16 | "lint": "eslint src && prettier \"**/*.ts\" --check --ignore-path .gitignore", 17 | "format": "eslint src && prettier \"**/*.ts\" --write --ignore-path .gitignore", 18 | "test": "pnpm build && vitest run", 19 | "test:watch": "concurrently -k -r \"pnpm build:watch\" \"vitest\"", 20 | "prepare": "tsc" 21 | }, 22 | "dependencies": { 23 | "@holochain/client": "^0.19.0", 24 | "@msgpack/msgpack": "^2.8.0", 25 | "@darksoil-studio/holochain-core-types": "^0.500.0", 26 | "@darksoil-studio/holochain-utils": "^0.500.0", 27 | "blakejs": "^1.2.1", 28 | "js-base64": "^3.7.7", 29 | "lodash-es": "^4.17.21", 30 | "unique-names-generator": "^4.7.1" 31 | }, 32 | "devDependencies": { 33 | "@types/lodash-es": "^4.17.12", 34 | "concurrently": "^5.3.0", 35 | "deepmerge": "^4.3.1", 36 | "eslint": "^8.57.1", 37 | "prettier": "^3.4.2", 38 | "tslib": "^2.8.1", 39 | "typescript": "^4.9.5", 40 | "vite": "^4.5.5", 41 | "vitest": "^1.6.0" 42 | }, 43 | "type": "module", 44 | "publishConfig": { 45 | "access": "public" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/cli/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain-playground/cli-client", 3 | "description": "Webcomponent devcamp-8 following open-wc recommendations", 4 | "license": "MIT", 5 | "author": "guillem.cordoba@gmail.com", 6 | "version": "0.500.0", 7 | "scripts": { 8 | "lint": "eslint src && prettier \"**/*.ts\" --check --ignore-path .gitignore", 9 | "format": "eslint src && prettier \"**/*.ts\" --write --ignore-path .gitignore", 10 | "build": "vite build", 11 | "build:watch": "vite build --watch", 12 | "prepare": "pnpm build" 13 | }, 14 | "dependencies": { 15 | "@darksoil-studio/holochain-core-types": "^0.500.0", 16 | "@darksoil-studio/holochain-elements": "^0.500.0", 17 | "@darksoil-studio/holochain-signals": "^0.500.0", 18 | "@holochain-playground/elements": "workspace:^0.500.0", 19 | "@holochain-playground/simulator": "workspace:^0.500.0", 20 | "@mdi/js": "^7.4.47", 21 | "@shoelace-style/shoelace": "^2.16.0", 22 | "dockview-core": "^1.17.1", 23 | "lit": "^3.0.0", 24 | "lodash-es": "^4.17.21", 25 | "socket.io-client": "^4.4.0" 26 | }, 27 | "devDependencies": { 28 | "@custom-elements-manifest/analyzer": "^0.5.7", 29 | "@types/lodash-es": "^4.17.5", 30 | "concurrently": "^5.3.0", 31 | "cross-env": "^7.0.3", 32 | "deepmerge": "^4.2.2", 33 | "dotenv": "^16.4.5", 34 | "eslint": "^9.0.0", 35 | "prettier": "^3.2.5", 36 | "tslib": "^2.3.1", 37 | "typescript": "^5.4.0", 38 | "vite": "^4.1.2" 39 | }, 40 | "customElements": "custom-elements.json" 41 | } -------------------------------------------------------------------------------- /packages/simulator/src/processors/create-conductors.ts: -------------------------------------------------------------------------------- 1 | import { Config, names, uniqueNamesGenerator } from 'unique-names-generator'; 2 | 3 | import { BootstrapService } from '../bootstrap/bootstrap-service.js'; 4 | import { Conductor } from '../core/conductor.js'; 5 | import { SimulatedHappBundle } from '../dnas/simulated-dna.js'; 6 | 7 | const config: Config = { 8 | dictionaries: [names], 9 | }; 10 | 11 | export async function createConductors( 12 | conductorsToCreate: number, 13 | currentConductors: Conductor[], 14 | happ: SimulatedHappBundle, 15 | ): Promise { 16 | const newConductors = await createConductorsWithoutHapp( 17 | conductorsToCreate, 18 | currentConductors, 19 | ); 20 | 21 | await Promise.all(newConductors.map(async c => c.installApp(happ, {}))); 22 | 23 | return newConductors; 24 | } 25 | 26 | export async function createConductorsWithoutHapp( 27 | conductorsToCreate: number, 28 | currentConductors: Conductor[], 29 | ): Promise { 30 | const bootstrapService = 31 | currentConductors.length === 0 32 | ? new BootstrapService() 33 | : currentConductors[0].network.bootstrapService; 34 | 35 | const newConductorsPromises: Promise[] = []; 36 | for (let i = 0; i < conductorsToCreate; i++) { 37 | const characterName: string = uniqueNamesGenerator(config); 38 | const conductor = Conductor.create(bootstrapService, characterName); 39 | newConductorsPromises.push(conductor); 40 | } 41 | 42 | const newConductors = await Promise.all(newConductorsPromises); 43 | 44 | return newConductors; 45 | } 46 | -------------------------------------------------------------------------------- /fixture/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "start": "vite --port $UI_PORT --clearScreen false", 6 | "build": "vite build", 7 | "lint": "eslint src && prettier \"**/*.ts\" --check --ignore-path .gitignore", 8 | "format": "eslint src && prettier \"**/*.ts\" --write --ignore-path .gitignore", 9 | "package": "npm run build && cd dist && bestzip ../dist.zip *" 10 | }, 11 | "dependencies": { 12 | "@holochain/client": "^0.19.0", 13 | "@lit-labs/context": "^0.2.0", 14 | "@lit-labs/task": "^2.0.0", 15 | "@material/mwc-circular-progress": "^0.27.0", 16 | "@material/mwc-button": "^0.27.0", 17 | "@material/mwc-textfield": "^0.27.0", 18 | "@material/mwc-textarea": "^0.27.0", 19 | "@material/mwc-checkbox": "^0.27.0", 20 | "@material/mwc-slider": "^0.27.0", 21 | "@material/mwc-icon-button": "^0.27.0", 22 | "@material/mwc-select": "^0.27.0", 23 | "@material/mwc-snackbar": "^0.27.0", 24 | "@material/mwc-formfield": "^0.27.0", 25 | "@msgpack/msgpack": "^2.7.2", 26 | "@vaadin/date-time-picker": "^23.2.8", 27 | "lit": "^2.6.1" 28 | }, 29 | "devDependencies": { 30 | "@open-wc/eslint-config": "^4.3.0", 31 | "@typescript-eslint/eslint-plugin": "^5.43.0", 32 | "@typescript-eslint/parser": "^5.43.0", 33 | "bestzip": "^2.2.0", 34 | "eslint": "^9.0.0", 35 | "eslint-config-prettier": "^9.0.0", 36 | "prettier": "^2.3.2", 37 | "rimraf": "^3.0.2", 38 | "vite": "^4.0.0", 39 | "typescript": "^5.4.0" 40 | }, 41 | "type": "module" 42 | } -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/delete_link.ts: -------------------------------------------------------------------------------- 1 | import { ActionHash, CreateLink, Record } from '@holochain/client'; 2 | 3 | import { GetStrategy } from '../../../../types.js'; 4 | import { 5 | buildDeleteLink, 6 | buildShh, 7 | } from '../../../cell/source-chain/builder-actions.js'; 8 | import { putRecord } from '../../../cell/source-chain/put.js'; 9 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 10 | 11 | export type DeleteLinkFn = (deletes_address: ActionHash) => Promise; 12 | 13 | // Creates a new Create action and its entry in the source chain 14 | export const delete_link: HostFn = 15 | (worskpace: HostFnWorkspace): DeleteLinkFn => 16 | async (deletes_address): Promise => { 17 | const elementToDelete = await worskpace.cascade.dht_get(deletes_address, { 18 | strategy: GetStrategy.Contents, 19 | }); 20 | 21 | if (!elementToDelete) 22 | throw new Error('Could not find element to be deleted'); 23 | 24 | const baseAddress = ( 25 | elementToDelete.signed_action.hashed.content as CreateLink 26 | ).base_address; 27 | 28 | if (!baseAddress) 29 | throw new Error('Action for the given hash is not a CreateLink action'); 30 | 31 | const deleteAction = buildDeleteLink( 32 | worskpace.state, 33 | baseAddress, 34 | deletes_address, 35 | ); 36 | 37 | const element: Record = { 38 | signed_action: buildShh(deleteAction), 39 | entry: { NotApplicable: undefined }, 40 | }; 41 | putRecord(element)(worskpace.state); 42 | 43 | return element.signed_action.hashed.hash; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/common/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NewEntryAction, 3 | Entry, 4 | EntryType, 5 | ActionHash, 6 | Record, 7 | } from '@holochain/client'; 8 | 9 | import { GetStrategy } from '../../../../../types.js'; 10 | import { 11 | buildDelete, 12 | buildShh, 13 | buildUpdate, 14 | } from '../../../../cell/source-chain/builder-actions.js'; 15 | import { putRecord } from '../../../../cell/source-chain/put.js'; 16 | import { HostFnWorkspace } from '../../../host-fn.js'; 17 | 18 | export async function common_update( 19 | worskpace: HostFnWorkspace, 20 | original_action_hash: ActionHash, 21 | entry: Entry, 22 | entry_type: EntryType 23 | ): Promise { 24 | const actionToUpdate = await worskpace.cascade.retrieve_action( 25 | original_action_hash, 26 | { 27 | strategy: GetStrategy.Contents, 28 | } 29 | ); 30 | 31 | if (!actionToUpdate) throw new Error('Could not find record to be updated'); 32 | 33 | const original_entry_hash = (actionToUpdate.hashed.content as NewEntryAction) 34 | .entry_hash; 35 | if (!original_entry_hash) 36 | throw new Error(`Trying to update an record with no entry`); 37 | 38 | const updateAction = buildUpdate( 39 | worskpace.state, 40 | entry, 41 | entry_type, 42 | original_entry_hash, 43 | original_action_hash 44 | ); 45 | 46 | const record: Record = { 47 | signed_action: buildShh(updateAction), 48 | entry: { 49 | Present: entry, 50 | }, 51 | }; 52 | putRecord(record)(worskpace.state); 53 | 54 | return record.signed_action.hashed.hash; 55 | } 56 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/produce_dht_ops.ts: -------------------------------------------------------------------------------- 1 | import { HashType, hash } from '@darksoil-studio/holochain-utils'; 2 | 3 | import { getNewActions } from '../source-chain/get.js'; 4 | import { getRecord } from '../source-chain/utils.js'; 5 | import { recordToDhtOps } from '../utils.js'; 6 | import { publish_dht_ops_task } from './publish_dht_ops.js'; 7 | import { 8 | Workflow, 9 | WorkflowReturn, 10 | WorkflowType, 11 | Workspace, 12 | } from './workflows.js'; 13 | 14 | // From https://github.com/holochain/holochain/blob/develop/crates/holochain/src/core/workflow/produce_dht_ops_workflow.rs 15 | export const produce_dht_ops = async ( 16 | worskpace: Workspace, 17 | ): Promise> => { 18 | const newActionHashes = getNewActions(worskpace.state); 19 | 20 | for (const newActionHash of newActionHashes) { 21 | const record = getRecord(worskpace.state, newActionHash); 22 | const dhtOps = recordToDhtOps(record); 23 | 24 | for (const dhtOp of dhtOps) { 25 | const dhtOpHash = hash(dhtOp, HashType.DHTOP); 26 | const dhtOpValue = { 27 | op: dhtOp, 28 | last_publish_time: undefined, 29 | receipt_count: 0, 30 | }; 31 | 32 | worskpace.state.authoredDHTOps.set(dhtOpHash, dhtOpValue); 33 | } 34 | } 35 | 36 | return { 37 | result: undefined, 38 | triggers: [publish_dht_ops_task()], 39 | }; 40 | }; 41 | 42 | export type ProduceDhtOpsWorkflow = Workflow; 43 | 44 | export function produce_dht_ops_task(): ProduceDhtOpsWorkflow { 45 | return { 46 | type: WorkflowType.PRODUCE_DHT_OPS, 47 | details: undefined, 48 | task: worskpace => produce_dht_ops(worskpace), 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/actions/update_entry.ts: -------------------------------------------------------------------------------- 1 | import { Entry, ActionHash, EntryType } from '@holochain/client'; 2 | import { encode } from '@msgpack/msgpack'; 3 | 4 | import { HostFn, HostFnWorkspace } from '../../host-fn.js'; 5 | import { common_update } from './common/update.js'; 6 | 7 | export type UpdateEntryFn = ( 8 | original_action_address: ActionHash, 9 | newEntry: { 10 | content: any; 11 | entry_def_id: string; 12 | } 13 | ) => Promise; 14 | 15 | // Creates a new Create action and its entry in the source chain 16 | export const update_entry: HostFn = 17 | (workspace: HostFnWorkspace, zome_index: number): UpdateEntryFn => 18 | async ( 19 | original_action_address: ActionHash, 20 | newEntry: { 21 | content: any; 22 | entry_def_id: string; 23 | } 24 | ): Promise => { 25 | const entry: Entry = { entry_type: 'App', entry: encode(newEntry.content) }; 26 | 27 | const entryDefIndex = workspace.dna.zomes[zome_index].entry_defs.findIndex( 28 | (entry_def) => entry_def.id === newEntry.entry_def_id 29 | ); 30 | if (entryDefIndex < 0) { 31 | throw new Error( 32 | `Given entry def id ${newEntry.entry_def_id} does not exist in this zome` 33 | ); 34 | } 35 | 36 | const entry_type: EntryType = { 37 | App: { 38 | entry_index: entryDefIndex, 39 | zome_index, 40 | visibility: 41 | workspace.dna.zomes[zome_index].entry_defs[entryDefIndex].visibility, 42 | }, 43 | }; 44 | 45 | return common_update(workspace, original_action_address, entry, entry_type); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/elements/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain-playground/elements", 3 | "version": "0.500.0", 4 | "description": "Holochain playground elements for visual introspection and simulator interaction", 5 | "author": "guillem.cordoba@gmail.com", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/index.js", 9 | "exports": { 10 | ".": "./dist/index.js", 11 | "./dist/*": "./dist/*" 12 | }, 13 | "scripts": { 14 | "start": "vite --clearScreen false --open", 15 | "build": "tsc", 16 | "prepare": "tsc", 17 | "build:watch": "tsc --watch" 18 | }, 19 | "dependencies": { 20 | "@alenaksu/json-viewer": "^2.1.2", 21 | "@holochain-playground/simulator": "workspace:^0.500.0", 22 | "@holochain/client": "^0.19.0", 23 | "@lit/context": "^1.1.3", 24 | "@mdi/js": "^7.4.47", 25 | "@msgpack/msgpack": "^2.8.0", 26 | "@scoped-elements/cytoscape": "^0.2.7", 27 | "@shoelace-style/shoelace": "^2.19.1", 28 | "@darksoil-studio/holochain-core-types": "^0.500.0", 29 | "@darksoil-studio/holochain-elements": "^0.500.0", 30 | "@darksoil-studio/holochain-signals": "^0.500.0", 31 | "@darksoil-studio/holochain-utils": "^0.500.0", 32 | "@vaadin/grid": "^24.6.2", 33 | "@vaadin/component-base": "^24.0.0", 34 | "cytoscape": "^3.31.0", 35 | "js-base64": "^3.7.7", 36 | "lit": "^3.2.1", 37 | "lodash-es": "^4.17.21" 38 | }, 39 | "devDependencies": { 40 | "@types/cytoscape": "^3.21.8", 41 | "@types/lodash-es": "^4.17.12", 42 | "eslint": "^8.57.1", 43 | "prettier": "^3.4.2", 44 | "typescript": "^5.7.3", 45 | "vite": "^4.5.9", 46 | "vite-plugin-checker": "^0.5.6" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | }, 51 | "type": "module" 52 | } 53 | -------------------------------------------------------------------------------- /packages/cli/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import express from 'express'; 3 | import dotenv from 'dotenv'; 4 | import bodyParser from 'body-parser'; 5 | import open from 'open'; 6 | import http from 'http'; 7 | import { Server } from 'socket.io'; 8 | import { getPort } from 'get-port-please'; 9 | 10 | import { getUrls } from './urls'; 11 | 12 | export async function launchApp() { 13 | dotenv.config(); 14 | 15 | const app = express(); 16 | const server = http.createServer(app); 17 | 18 | const port = await getPort({ port: 8282 }); 19 | const PORT = process.env.SERVER_PORT || port; 20 | const URL = `http://localhost:${PORT}`; 21 | 22 | app.set('port', PORT); 23 | app.use(bodyParser.json()); 24 | app.use(bodyParser.urlencoded({ extended: true })); 25 | 26 | const publicPath = `${__dirname }/public/`; 27 | app.get('/', (req, res) => { 28 | res.sendFile(`${publicPath }index.html`); 29 | }); 30 | app.use(express.static(publicPath)); 31 | 32 | server.listen(app.get('port'), () => { 33 | console.log( 34 | 'App is running at http://localhost:%d in %s mode', 35 | app.get('port'), 36 | app.get('env') 37 | ); 38 | console.log('Press CTRL-C to stop\n'); 39 | }); 40 | 41 | const io = new Server(server, { 42 | cors: { 43 | origin: [URL], 44 | }, 45 | maxHttpBufferSize: 1e8, 46 | }); 47 | 48 | console.log(''); 49 | console.log('Welcome to the Holochain Playground!'); 50 | console.log(''); 51 | 52 | // opens the url in the default browser 53 | open(URL); 54 | 55 | io.on('connection', (socket) => { 56 | setInterval(() => { 57 | socket.emit('urls-updated', { urls: getUrls() }); 58 | }, 1000); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /packages/elements/src/elements/dna-select/index.ts: -------------------------------------------------------------------------------- 1 | import { DnaHash, encodeHashToBase64 } from '@holochain/client'; 2 | import '@shoelace-style/shoelace/dist/components/card/card.js'; 3 | import '@shoelace-style/shoelace/dist/components/select/select.js'; 4 | import { css, html } from 'lit'; 5 | import { customElement } from 'lit/decorators.js'; 6 | import isEqual from 'lodash-es/isEqual.js'; 7 | 8 | import { PlaygroundElement } from '../../base/playground-element.js'; 9 | import { sharedStyles } from '../utils/shared-styles.js'; 10 | 11 | @customElement('select-active-dna') 12 | export class SelectActiveDna extends PlaygroundElement { 13 | selectDNA(dna: DnaHash) { 14 | this.store.activeDna.set(dna); 15 | } 16 | 17 | renderDna(dna: DnaHash) { 18 | const strDna = encodeHashToBase64(dna); 19 | const activeDna = this.store.activeDna.get(); 20 | 21 | return html` 22 | ${strDna} 25 | `; 26 | } 27 | 28 | render() { 29 | const allDnasResult = this.store.allDnas.get(); 30 | const allDnas = 31 | allDnasResult.status === 'completed' ? allDnasResult.value : []; 32 | return html` 33 |
34 | Select Active Dna 37 | this.selectDNA(allDnas[e.detail.index])} 39 | > 40 | ${allDnas.map(dna => this.renderDna(dna))} 41 | 42 |
43 | `; 44 | } 45 | 46 | static get styles() { 47 | return [ 48 | css` 49 | :host { 50 | display: flex; 51 | flex: 1; 52 | } 53 | `, 54 | sharedStyles, 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/elements/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { AGENT_PREFIX, locationBytes } from '@darksoil-studio/holochain-utils'; 2 | import { 3 | AgentPubKey, 4 | AppInfo, 5 | CellId, 6 | CellInfo, 7 | CellType, 8 | DnaHash, 9 | DnaModifiers, 10 | } from '@holochain/client'; 11 | import { Base64 } from 'js-base64'; 12 | 13 | export function cellCount(appInfo: AppInfo): number { 14 | return Object.values(appInfo.cell_info).reduce( 15 | (acc, next) => acc + next.length, 16 | 0, 17 | ); 18 | } 19 | 20 | export function dnaHash(cellInfo: CellInfo): DnaHash { 21 | if (cellInfo.type === CellType.Provisioned) { 22 | return cellInfo.value.cell_id[0]; 23 | } else if (cellInfo.type === CellType.Cloned) { 24 | return cellInfo.value.cell_id[0]; 25 | } else { 26 | return cellInfo.value.dna; 27 | } 28 | } 29 | 30 | export function cellName(cellInfo: CellInfo): string { 31 | if (cellInfo.type === CellType.Provisioned) { 32 | return cellInfo.value.name; 33 | } else if (cellInfo.type === CellType.Cloned) { 34 | return cellInfo.value.clone_id; 35 | } else { 36 | return cellInfo.value.name!; 37 | } 38 | } 39 | 40 | export function dnaModifiers(cellInfo: CellInfo): DnaModifiers { 41 | if (cellInfo.type === CellType.Provisioned) { 42 | return cellInfo.value.dna_modifiers; 43 | } else if (cellInfo.type === CellType.Cloned) { 44 | return cellInfo.value.dna_modifiers; 45 | } else { 46 | return cellInfo.value.dna_modifiers!; 47 | } 48 | } 49 | 50 | export function kitsuneAgentToAgentPubKey(kitsuneAgent: string): AgentPubKey { 51 | const kitsuneAgentHash = Base64.toUint8Array(kitsuneAgent); 52 | 53 | return new Uint8Array([ 54 | ...Base64.toUint8Array(AGENT_PREFIX), 55 | ...kitsuneAgentHash, 56 | ...locationBytes(kitsuneAgentHash), 57 | ]); 58 | } 59 | -------------------------------------------------------------------------------- /packages/simulator/src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 2 | import { CellInfo, CellType, DnaHash, DnaModifiers } from '@holochain/client'; 3 | import { encode } from '@msgpack/msgpack'; 4 | 5 | import { AppRole, SimulatedDna } from '../dnas/simulated-dna'; 6 | import { Dictionary } from '../types'; 7 | 8 | export function simulatedRolesToCellInfo( 9 | roles: Dictionary, 10 | registeredDnas: HoloHashMap, 11 | ): Dictionary { 12 | const origin_time = Date.now() * 1000; 13 | const quantum_time = { 14 | secs: 1000, 15 | nanos: 0, 16 | }; 17 | const cellInfo: Dictionary = {}; 18 | for (const [roleName, role] of Object.entries(roles)) { 19 | cellInfo[roleName] = role.is_provisioned 20 | ? [ 21 | { 22 | type: CellType.Provisioned, 23 | value: { 24 | cell_id: role.base_cell_id, 25 | dna_modifiers: { 26 | network_seed: registeredDnas.get(role.base_cell_id[0]) 27 | .networkSeed, 28 | properties: encode( 29 | registeredDnas.get(role.base_cell_id[0]).properties, 30 | ), 31 | }, 32 | name: roleName, 33 | }, 34 | }, 35 | ] 36 | : []; 37 | 38 | for (const [cloneName, clone] of Object.entries(role.clones)) { 39 | cellInfo[roleName].push({ 40 | type: CellType.Cloned, 41 | value: { 42 | cell_id: clone, 43 | enabled: true, 44 | original_dna_hash: role.base_cell_id[0], 45 | dna_modifiers: { 46 | network_seed: registeredDnas.get(clone[0]).networkSeed, 47 | properties: encode(registeredDnas.get(clone[0]).properties), 48 | }, 49 | clone_id: cloneName, 50 | name: roleName, 51 | }, 52 | }); 53 | } 54 | } 55 | return cellInfo; 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain-playground/dev", 3 | "private": true, 4 | "scripts": { 5 | "start": "pnpm -F @holochain-playground/simulator build && pnpm -F @holochain-playground/elements start ", 6 | "start:cli": "pnpm build:libs && cross-env ADMIN_PORT=$(port) ADMIN_PORT_2=$(port) concurrently \"cd fixture && pnpm start\" \"pnpm -F @holochain-playground/elements build:watch\" \"pnpm -F @holochain-playground/cli-client build:watch\" \"sleep 5 && pnpm -F @holochain-playground/cli start\"", 7 | "start:cli:running": "pnpm build:libs && concurrently \"pnpm -F @holochain-playground/elements build:watch\" \"pnpm -F @holochain-playground/cli-client build:watch\" \"sleep 5 && pnpm -F @holochain-playground/cli start\"", 8 | "build:cli": "pnpm -F @holochain-playground/simulator build && pnpm -F @holochain-playground/elements build && pnpm -F @holochain-playground/cli-client build && pnpm -F @holochain-playground/cli build", 9 | "test": "pnpm -F @holochain-playground/simulator test", 10 | "build:libs": "pnpm -F @holochain-playground/simulator build && pnpm -F @holochain-playground/elements build && pnpm -F @holochain-playground/cli-client build", 11 | "demo:start": "pnpm -F @holochain-playground/simulator build && pnpm -F @holochain-playground/elements build && pnpm -F @holochain-playground/demo start", 12 | "demo:publish": "pnpm -r build && pnpm -F @holochain-playground/demo run publish" 13 | }, 14 | "devDependencies": { 15 | "@eslint/js": "^9.17.0", 16 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 17 | "concurrently": "^6.5.1", 18 | "cross-env": "^7.0.3", 19 | "eslint": "^9.17.0", 20 | "eslint-config-prettier": "^9.1.0", 21 | "new-port-cli": "^1.0.0", 22 | "prettier": "^3.4.2", 23 | "typescript": "^5.7.2", 24 | "typescript-eslint": "^8.18.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/client/src/dock-view.ts: -------------------------------------------------------------------------------- 1 | import { sharedStyles } from '@darksoil-studio/holochain-elements'; 2 | import { DockviewApi, SerializedDockview, createDockview } from 'dockview-core'; 3 | // @ts-ignore 4 | import styles from 'dockview-core/dist/styles/dockview.css?raw'; 5 | import { LitElement, css, html } from 'lit'; 6 | import { unsafeCSS } from 'lit'; 7 | import { customElement, property } from 'lit/decorators.js'; 8 | 9 | @customElement('dock-view') 10 | export class DockViewEl extends LitElement { 11 | @property() 12 | layout: SerializedDockview | undefined; 13 | 14 | dockview!: DockviewApi; 15 | 16 | firstUpdated() { 17 | this.renderDockview( 18 | this.shadowRoot!.getElementById('dockview') as HTMLElement, 19 | ); 20 | } 21 | 22 | renderDockview(el: HTMLElement) { 23 | this.dockview = createDockview(el, { 24 | createComponent(options) { 25 | const element = document.createElement(options.name); 26 | // element.style.width = '100%'; 27 | element.style.flex = '1'; 28 | // element.style.margin = '16px'; 29 | return { 30 | element, 31 | init(parameters) {}, 32 | }; 33 | }, 34 | }); 35 | if (this.layout) { 36 | this.dockview.fromJSON(this.layout); 37 | } 38 | this.dispatchEvent( 39 | new CustomEvent('dockview-ready', { 40 | bubbles: true, 41 | composed: true, 42 | detail: { 43 | dockview: this.dockview, 44 | }, 45 | }), 46 | ); 47 | } 48 | 49 | render() { 50 | return html`
`; 55 | } 56 | 57 | static styles = [ 58 | css` 59 | :host { 60 | display: flex; 61 | } 62 | ${unsafeCSS(styles)} 63 | 64 | .content-container { 65 | padding: 16px; 66 | } 67 | .groupview > .content-container { 68 | display: flex; 69 | } 70 | `, 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Holochain Playground 2 | 3 | The playground is a CLI to introspect running holochain nodes. 4 | 5 | ## Running the playground directly 6 | 7 | ```bash 8 | nix run github:darksoil-studio/holochain-playground/main-0.4 ws://localhost:8888 ws://localhost:8889 9 | ``` 10 | 11 | This URL should point to the admin interfaces of the conductors. 12 | 13 | ## Importing in a flake 14 | 15 | Add it to your `flake.nix` with: 16 | 17 | ```diff 18 | { 19 | inputs = { 20 | holonix.url = "github:holochain/holonix/main-0.4"; 21 | nixpkgs.follows = "holonix/nixpkgs"; 22 | 23 | + holochain-playground.url = "github:darksoil-studio/holochain-playground/main-0.4"; 24 | }; 25 | 26 | nixConfig = { 27 | extra-substituters = [ 28 | "https://holochain-ci.cachix.org" 29 | + "https://darksoil-studio.cachix.org" 30 | ]; 31 | extra-trusted-public-keys = [ 32 | "holochain-ci.cachix.org-1:5IUSkZc0aoRS53rfkvH9Kid40NpyjwCMCzwRTXy+QN8=" 33 | + "darksoil-studio.cachix.org-1:UEi+aujy44s41XL/pscLw37KEVpTEIn8N/kn7jO8rkc=" 34 | ]; 35 | }; 36 | 37 | outputs = inputs@{ ... }: 38 | inputs.holonix.inputs.flake-parts.lib.mkFlake { inherit inputs; } { 39 | systems = builtins.attrNames inputs.holonix.devShells; 40 | perSystem = { config, pkgs, system, inputs', lib, ... }: rec { 41 | devShells.default = pkgs.mkShell { 42 | inputsFrom = [ 43 | inputs.holonix.devShells.${system}.default 44 | ]; 45 | packages = [ 46 | + inputs'.holochain-playground.packages.hc-playground 47 | ]; 48 | }; 49 | }; 50 | }; 51 | } 52 | ``` 53 | 54 | Then, you should have an `hc-playground` binary in your nix shell. 55 | 56 | To have the playground running for development conductors, just run `hc-playground` in the same folder where you have run `hc sandbox`, `hc spin` or `hc launch`. 57 | -------------------------------------------------------------------------------- /packages/simulator/src/bootstrap/bootstrap-service.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, AnyDhtHash, CellId, DnaHash } from '@holochain/client'; 2 | import { CellMap } from '@darksoil-studio/holochain-utils'; 3 | 4 | import { Cell } from '../core/cell/cell.js'; 5 | import { 6 | getClosestNeighbors, 7 | getFarthestNeighbors, 8 | } from '../core/network/utils.js'; 9 | import { areEqual } from '../processors/hash.js'; 10 | 11 | export class BootstrapService { 12 | cells: CellMap = new CellMap(); 13 | 14 | announceCell(cellId: CellId, cell: Cell) { 15 | this.cells.set(cellId, cell); 16 | } 17 | 18 | removeCell(cellId: CellId) { 19 | this.cells.delete(cellId); 20 | } 21 | 22 | getNeighborhood( 23 | dnaHash: DnaHash, 24 | basis_dht_hash: AnyDhtHash, 25 | numNeighbors: number, 26 | filteredAgents: AgentPubKey[] = [], 27 | ): Cell[] { 28 | const dnaCells = this.cells.valuesForDna(dnaHash); 29 | 30 | const cells = dnaCells.filter( 31 | cell => !filteredAgents.find(fa => areEqual(fa, cell.agentPubKey)), 32 | ); 33 | 34 | const neighborsKeys = getClosestNeighbors( 35 | cells.map(c => c.agentPubKey), 36 | basis_dht_hash, 37 | numNeighbors, 38 | ); 39 | 40 | return neighborsKeys.map( 41 | pubKey => dnaCells.find(c => areEqual(pubKey, c.agentPubKey)) as Cell, 42 | ); 43 | } 44 | 45 | getFarKnownPeers( 46 | dnaHash: DnaHash, 47 | agentPubKey: AgentPubKey, 48 | filteredAgents: AgentPubKey[] = [], 49 | ): Cell[] { 50 | const dnaAgents = this.cells.agentsForDna(dnaHash); 51 | 52 | const cells = dnaAgents.filter( 53 | peerPubKey => 54 | !areEqual(peerPubKey, agentPubKey) && 55 | !filteredAgents.find(a => areEqual(peerPubKey, a)), 56 | ); 57 | 58 | const farthestKeys = getFarthestNeighbors(cells, agentPubKey); 59 | 60 | return farthestKeys.map( 61 | pubKey => this.cells.get([dnaHash, pubKey]) as Cell, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/path.ts: -------------------------------------------------------------------------------- 1 | import { EntryHash, LinkType } from '@holochain/client'; 2 | import { HashType, retype } from '@darksoil-studio/holochain-utils'; 3 | 4 | import { areEqual } from '../../processors/hash.js'; 5 | import { Hdk } from './context.js'; 6 | 7 | function rootHash(hdk: Hdk): Promise { 8 | return hdk.hash_entry('ROOT_ANCHOR'); 9 | } 10 | 11 | export const ensure = 12 | (hdk: Hdk) => 13 | async (path: string, link_type: LinkType): Promise => { 14 | const components = path.split('.'); 15 | 16 | if (components.length === 1) { 17 | const root = await rootHash(hdk); 18 | const pathHash = await hdk.hash_entry(path); 19 | 20 | const links = (await hdk.get_links(root, link_type)) || []; 21 | const linksForThisPath = links.filter(link => 22 | areEqual(retype(link.target, HashType.ENTRY), pathHash), 23 | ); 24 | 25 | if (linksForThisPath.length === 0) { 26 | await hdk.create_link({ 27 | base: root, 28 | target: pathHash, 29 | link_type, 30 | tag: path, 31 | }); 32 | } 33 | } else if (components.length > 1) { 34 | components.splice(components.length - 1, 1); 35 | const parent = components.join('.'); 36 | 37 | await ensure(hdk)(parent, link_type); 38 | 39 | const pathHash = await hdk.hash_entry(path); 40 | const parentHash = await hdk.hash_entry(parent); 41 | 42 | const links = (await hdk.get_links(parentHash, link_type)) || []; 43 | const linksForThisPath = links.filter(link => 44 | areEqual(retype(link.target, HashType.ENTRY), pathHash), 45 | ); 46 | 47 | if (linksForThisPath.length === 0) { 48 | await hdk.create_link({ 49 | base: parentHash, 50 | target: pathHash, 51 | link_type, 52 | tag: path, 53 | }); 54 | } 55 | } 56 | }; 57 | 58 | export interface Path { 59 | ensure: (path: string, link_type: LinkType) => Promise; 60 | } 61 | -------------------------------------------------------------------------------- /packages/cli/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 43 | Holochain Playground 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /.github/workflows/build-nix-app.yaml: -------------------------------------------------------------------------------- 1 | name: "build-nix-app" 2 | on: 3 | # Trigger the workflow on push or pull request, 4 | # but only for the main branch 5 | push: 6 | branches: [ main, main-0.4 ] 7 | pull_request: 8 | branches: [ main, main-0.4 ] 9 | 10 | jobs: 11 | build-and-cache-nix-tauri-app: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-22.04, macos-latest, macos-13] 15 | 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Extend space 21 | if: ${{ matrix.os == 'ubuntu-latest' }} 22 | uses: ./.github/actions/extend-space 23 | 24 | - name: Install nix 25 | uses: cachix/install-nix-action@v31 26 | with: 27 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 28 | nix_path: nixpkgs=channel:nixos-25.05 29 | 30 | - uses: cachix/cachix-action@v15 31 | with: 32 | name: holochain-ci 33 | 34 | - uses: cachix/cachix-action@v15 35 | with: 36 | name: darksoil-studio 37 | 38 | - name: Build hc-playground 39 | env: 40 | CACHIX_AUTH_TOKEN: "${{ secrets.DARKSOIL_CACHIX_AUTH_TOKEN }}" 41 | run: | 42 | cachix watch-exec darksoil-studio -- nix build -L --accept-flake-config --no-update-lock-file .#hc-playground 43 | 44 | - name: 'Setup jq' 45 | uses: dcarbone/install-jq-action@v2 46 | 47 | - name: Pin hc-playground 48 | if: github.event_name != 'pull_request' && github.ref_name == 'main' 49 | env: 50 | CACHIX_AUTH_TOKEN: "${{ secrets.DARKSOIL_CACHIX_AUTH_TOKEN }}" 51 | run: | 52 | cachix push darksoil-studio $(nix path-info --json --accept-flake-config --no-warn-dirty .#hc-playground | jq -r 'keys[0]') 53 | cachix pin darksoil-studio hc-playground $(nix path-info --json --accept-flake-config --no-warn-dirty .#hc-playground | jq -r 'keys[0]') 54 | -------------------------------------------------------------------------------- /fixture/ui/src/holochain-app.ts: -------------------------------------------------------------------------------- 1 | import { AppClient, AppWebsocket } from '@holochain/client'; 2 | import { provide } from '@lit-labs/context'; 3 | import '@material/mwc-circular-progress'; 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property, state } from 'lit/decorators.js'; 6 | 7 | import { clientContext } from './contexts'; 8 | import './forum/posts/all-posts'; 9 | import { AllPosts } from './forum/posts/all-posts'; 10 | import './forum/posts/create-post'; 11 | 12 | @customElement('holochain-app') 13 | export class HolochainApp extends LitElement { 14 | @state() loading = true; 15 | 16 | @state() result: string | undefined; 17 | 18 | @provide({ context: clientContext }) 19 | @property({ type: Object }) 20 | client!: AppClient; 21 | 22 | async firstUpdated() { 23 | this.client = await AppWebsocket.connect(); 24 | 25 | this.loading = false; 26 | } 27 | 28 | render() { 29 | if (this.loading) 30 | return html` 31 | 32 | `; 33 | 34 | return html` 35 |
36 |

Forum

37 | 38 |
39 |

All Posts

40 | 41 | 42 |
43 |
44 | `; 45 | } 46 | 47 | static styles = css` 48 | :host { 49 | min-height: 100vh; 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | justify-content: flex-start; 54 | font-size: calc(10px + 2vmin); 55 | color: #1a2b42; 56 | max-width: 960px; 57 | margin: 0 auto; 58 | text-align: center; 59 | background-color: var(--lit-element-background-color); 60 | } 61 | 62 | main { 63 | flex-grow: 1; 64 | } 65 | 66 | .app-footer { 67 | font-size: calc(12px + 0.5vmin); 68 | align-items: center; 69 | } 70 | 71 | .app-footer a { 72 | margin-left: 5px; 73 | } 74 | `; 75 | } 76 | -------------------------------------------------------------------------------- /packages/elements/src/elements/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from '@holochain/client'; 2 | import { decode } from '@msgpack/msgpack'; 3 | 4 | import { shortenStrRec } from './hash.js'; 5 | 6 | export const sleep = (ms: number) => 7 | new Promise(r => setTimeout(() => r(), ms)); 8 | 9 | export function utf32Decode(bytes: Uint8Array): string { 10 | const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 11 | let result = ''; 12 | 13 | for (let i = 0; i < bytes.length; i += 4) { 14 | result += String.fromCodePoint(view.getInt32(i, true)); 15 | } 16 | 17 | return result; 18 | } 19 | 20 | export function getEntryContents(entry: Entry): any { 21 | let entryContent: any = entry.entry; 22 | if (entry.entry_type === 'App') { 23 | entryContent = decode(entry.entry); 24 | } 25 | 26 | return shortenStrRec({ 27 | ...entry, 28 | entry: entryContent, 29 | }); 30 | } 31 | 32 | export function decodeComponent(component: Uint8Array): string { 33 | try { 34 | const result = utf32Decode(decode(component) as any); 35 | return result; 36 | } catch (e) {} 37 | try { 38 | const result2 = JSON.stringify(decode(component)); 39 | return result2; 40 | } catch (e) {} 41 | 42 | return bin2String(component); 43 | } 44 | 45 | export function decodePath(path: Uint8Array[]): string { 46 | return path.map(c => decodeComponent(c)).join('.'); 47 | } 48 | 49 | export function getLinkTagStr(linkTag: Uint8Array): string { 50 | let tagStr = getLinkTagStrInner(linkTag); 51 | 52 | if (tagStr.length > 15) tagStr = `${tagStr.slice(0, 13)}...`; 53 | return tagStr; 54 | } 55 | 56 | export function getLinkTagStrInner(linkTag: Uint8Array): string { 57 | // Check if this tag belongs to a Path 58 | 59 | return decodeComponent(linkTag); 60 | } 61 | 62 | function bin2String(array: any) { 63 | let result = ''; 64 | for (let i = 0; i < array.length; i += 1) { 65 | result += String.fromCharCode(array[i]); 66 | } 67 | return result; 68 | } 69 | -------------------------------------------------------------------------------- /packages/simulator/test/links.test.js: -------------------------------------------------------------------------------- 1 | import { assert, describe, expect, it } from 'vitest'; 2 | 3 | import { createConductors, demoHapp } from '../dist'; 4 | import { sleep } from './utils'; 5 | 6 | describe('Links', () => { 7 | it('create entry and link, get_links, delete_links', async function () { 8 | const conductors = await createConductors(10, [], demoHapp()); 9 | await sleep(300); 10 | 11 | const cell = conductors[0].getAllCells()[0]; 12 | 13 | let baseHash = await conductors[0].callZomeFn({ 14 | cellId: cell.cellId, 15 | cap: null, 16 | fnName: 'create_entry', 17 | payload: { content: 'hi' }, 18 | zome: 'demo_entries', 19 | }); 20 | 21 | expect(baseHash).to.be.ok; 22 | await sleep(300); 23 | 24 | const create_link_hash = await conductors[0].callZomeFn({ 25 | cellId: cell.cellId, 26 | cap: null, 27 | fnName: 'create_link', 28 | payload: { 29 | base: baseHash, 30 | target: cell.cellId[1], 31 | link_type: 0, 32 | tag: 'hello', 33 | }, 34 | zome: 'demo_links', 35 | }); 36 | 37 | expect(create_link_hash).to.be.ok; 38 | 39 | await sleep(300); 40 | 41 | let links = await conductors[0].callZomeFn({ 42 | cellId: cell.cellId, 43 | cap: null, 44 | fnName: 'get_links', 45 | payload: { 46 | base: baseHash, 47 | link_type: 0, 48 | }, 49 | zome: 'demo_links', 50 | }); 51 | expect(links.length).to.equal(1); 52 | 53 | await sleep(300); 54 | 55 | await conductors[0].callZomeFn({ 56 | cellId: cell.cellId, 57 | cap: null, 58 | fnName: 'delete_link', 59 | payload: { 60 | create_link_hash, 61 | }, 62 | zome: 'demo_links', 63 | }); 64 | await sleep(3000); 65 | 66 | links = await conductors[0].callZomeFn({ 67 | cellId: cell.cellId, 68 | cap: null, 69 | fnName: 'get_links', 70 | payload: { 71 | base: baseHash, 72 | link_type: 0, 73 | }, 74 | zome: 'demo_links', 75 | }); 76 | 77 | expect(links.length).to.equal(0); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/simulator/test/stress.test.js: -------------------------------------------------------------------------------- 1 | import { createConductors, demoHapp } from '../dist'; 2 | import { assert, describe, expect, it } from 'vitest'; 3 | import { sleep } from './utils.js'; 4 | 5 | describe('Stress tests links', () => { 6 | it('create multiple links', async function () { 7 | const start = Date.now(); 8 | 9 | for (let i = 0; i < 2; i++) { 10 | await oneRound(); 11 | } 12 | 13 | async function oneRound() { 14 | const conductors = await createConductors(10, [], demoHapp()); 15 | await sleep(200); 16 | 17 | const cell = conductors[0].getAllCells()[0]; 18 | 19 | await conductors[0].callZomeFn({ 20 | cellId: cell.cellId, 21 | cap: null, 22 | fnName: 'create_entry', 23 | payload: { content: 'hi' }, 24 | zome: 'demo_entries', 25 | }); 26 | 27 | let baseHash = await conductors[0].callZomeFn({ 28 | cellId: cell.cellId, 29 | cap: null, 30 | fnName: 'hash_entry', 31 | payload: { entry: { content: 'hi', entry_def_id: 'demo_entry' } }, 32 | zome: 'demo_entries', 33 | }); 34 | 35 | expect(baseHash).to.be.ok; 36 | await sleep(200); 37 | 38 | const add_link_hash = await conductors[0].callZomeFn({ 39 | cellId: cell.cellId, 40 | cap: null, 41 | fnName: 'create_link', 42 | payload: { 43 | base: baseHash, 44 | target: cell.cellId[1], 45 | tag: 'hello', 46 | link_type: 0, 47 | }, 48 | zome: 'demo_links', 49 | }); 50 | 51 | expect(add_link_hash).to.be.ok; 52 | 53 | await sleep(1000); 54 | 55 | let links = await conductors[0].callZomeFn({ 56 | cellId: cell.cellId, 57 | cap: null, 58 | fnName: 'get_links', 59 | payload: { 60 | base: baseHash, 61 | link_type: 0, 62 | }, 63 | zome: 'demo_links', 64 | }); 65 | 66 | expect(links.length).to.equal(1); 67 | } 68 | 69 | console.log('Ended in ', (Date.now() - start) / 1000); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/source-chain/get.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | ChainOp, 4 | NewEntryAction, 5 | Record, 6 | SignedActionHashed, 7 | } from '@holochain/client'; 8 | import { HashType, hash, hashAction } from '@darksoil-studio/holochain-utils'; 9 | 10 | import { areEqual } from '../../../processors/hash.js'; 11 | import { CellState } from '../state.js'; 12 | import { getDhtOpAction, isWarrantOp } from '../utils.js'; 13 | 14 | /** 15 | * Returns the action hashes which don't have their DHTOps in the authoredDHTOps DB 16 | */ 17 | export function getNewActions(state: CellState): Array { 18 | const dhtOps = Array.from(state.authoredDHTOps.values()); 19 | const actionHashesAlreadyPublished = dhtOps 20 | .filter(value => !isWarrantOp(value.op)) 21 | .map(value => (value.op as { ChainOp: ChainOp }).ChainOp) 22 | .map(dhtOp => hashAction(getDhtOpAction(dhtOp))); 23 | 24 | return state.sourceChain.filter( 25 | actionHash => 26 | !actionHashesAlreadyPublished.find(h => areEqual(h, actionHash)), 27 | ); 28 | } 29 | 30 | export function getAllAuthoredActions( 31 | state: CellState, 32 | ): Array { 33 | return state.sourceChain.map(actionHash => state.CAS.get(actionHash)); 34 | } 35 | 36 | export function getSourceChainRecords( 37 | state: CellState, 38 | fromIndex: number, 39 | toIndex: number, 40 | ): Record[] { 41 | const elements: Record[] = []; 42 | 43 | for (let i = fromIndex; i < toIndex; i++) { 44 | const element = getSourceChainRecord(state, i); 45 | if (element) elements.push(element); 46 | } 47 | 48 | return elements; 49 | } 50 | 51 | export function getSourceChainRecord( 52 | state: CellState, 53 | index: number, 54 | ): Record | undefined { 55 | const actionHash = state.sourceChain[index]; 56 | const signed_action: SignedActionHashed = state.CAS.get(actionHash); 57 | 58 | let entry = undefined; 59 | const entryHash = (signed_action.hashed.content as NewEntryAction).entry_hash; 60 | if (entryHash) { 61 | entry = state.CAS.get(entryHash); 62 | } 63 | 64 | return { 65 | entry, 66 | signed_action, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/call.ts: -------------------------------------------------------------------------------- 1 | import { CapSecret, CellId } from '@holochain/client'; 2 | 3 | import { Conductor } from '../../conductor.js'; 4 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 5 | 6 | export type CallTargetCell = 7 | | { OtherCell: CellId } 8 | | { OtherRole: string } 9 | | { Local: void }; 10 | 11 | export type CallFn = ( 12 | to_cell: CallTargetCell, 13 | zome: string, 14 | fn_name: string, 15 | cap_secret: CapSecret | undefined, 16 | payload: any, 17 | ) => Promise; 18 | 19 | function getCellId( 20 | callerCell: CellId, 21 | targetCell: CallTargetCell, 22 | conductorHandle: Conductor, 23 | ) { 24 | if ('Local' in targetCell) { 25 | return callerCell; 26 | } else if ('OtherCell' in targetCell) { 27 | return targetCell.OtherCell; 28 | } else { 29 | const happ = Object.values(conductorHandle.installedHapps).find(happ => 30 | Object.entries(happ.roles).find(([role, cells]) => 31 | isEqual(cells.base_cell_id, callerCell), 32 | ), 33 | ); 34 | if (!happ) 35 | throw new Error(`A non-existant cell is making a call zome fn request.`); 36 | const role = targetCell.OtherRole; 37 | 38 | const isClone = role.includes('.'); 39 | 40 | if (isClone) { 41 | const roleWithoutClone = role.split('.')[0]; 42 | return happ.roles[roleWithoutClone].clones[role]; 43 | } else { 44 | return happ.roles[role].base_cell_id; 45 | } 46 | } 47 | } 48 | function isEqual(cellId1: CellId, cellId2: CellId): boolean { 49 | return cellId1.toString() === cellId2.toString(); 50 | } 51 | 52 | export const call: HostFn = 53 | (workspace: HostFnWorkspace): CallFn => 54 | async (to_cell, zome, fn_name, cap_secret, payload): Promise => { 55 | const cellId = getCellId( 56 | [workspace.state.dnaHash, workspace.state.agentPubKey], 57 | to_cell, 58 | workspace.conductor_handle, 59 | ); 60 | if (!cellId) throw new Error('Target cell was not found.'); 61 | 62 | return workspace.conductor_handle.callZomeFn({ 63 | cellId, 64 | cap: cap_secret, 65 | fnName: fn_name, 66 | zome: zome, 67 | payload: payload, 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /packages/elements/src/store/utils.ts: -------------------------------------------------------------------------------- 1 | import { CellId } from '@holochain/client'; 2 | import { AsyncResult, JoinAsyncOptions, joinAsync } from '@darksoil-studio/holochain-signals'; 3 | import { CellMap } from '@darksoil-studio/holochain-utils'; 4 | 5 | export function cellChanges( 6 | currentCellIds: CellId[], 7 | targetCellIds: CellId[], 8 | ): { cellsToAdd: CellId[]; cellsToRemove: CellId[] } { 9 | const cellsToAdd = targetCellIds.filter( 10 | cellId => !contains(currentCellIds, cellId), 11 | ); 12 | const cellsToRemove = currentCellIds.filter( 13 | cellId => !contains(targetCellIds, cellId), 14 | ); 15 | 16 | return { 17 | cellsToAdd, 18 | cellsToRemove, 19 | }; 20 | } 21 | 22 | export function contains(cellIds: CellId[], lookingForCellId: CellId) { 23 | return cellIds.find(c => isEqual(c, lookingForCellId)); 24 | } 25 | 26 | function isEqual(cellId1: CellId, cellId2: CellId): boolean { 27 | return cellId1.toString() === cellId2.toString(); 28 | } 29 | 30 | /** 31 | * Create a new map maintaining the keys while mapping the values with the given mapping function 32 | */ 33 | export function mapCellValues( 34 | map: CellMap, 35 | mappingFn: (value: V, key: CellId) => U, 36 | ): CellMap { 37 | const mappedMap = new CellMap(); 38 | 39 | for (const [key, value] of map.entries()) { 40 | mappedMap.set(key, mappingFn(value, key)); 41 | } 42 | return mappedMap; 43 | } 44 | 45 | export function joinAsyncCellMap( 46 | map: CellMap>, 47 | joinOptions?: JoinAsyncOptions, 48 | ): AsyncResult> { 49 | const resultsArray = Array.from(map.entries()).map(([key, result]) => { 50 | if (result.status !== 'completed') return result; 51 | const value = [key, result.value] as [CellId, T]; 52 | return { 53 | status: 'completed', 54 | value, 55 | } as AsyncResult<[CellId, T]>; 56 | }); 57 | const arrayResult = joinAsync(resultsArray, joinOptions); 58 | 59 | if (arrayResult.status !== 'completed') return arrayResult; 60 | 61 | const value = new CellMap(arrayResult.value); 62 | return { 63 | status: 'completed', 64 | value, 65 | } as AsyncResult>; 66 | } 67 | -------------------------------------------------------------------------------- /packages/demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from '@rollup/plugin-node-resolve'; 2 | import babel from '@rollup/plugin-babel'; 3 | import html from '@web/rollup-plugin-html'; 4 | import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | export default { 8 | input: 'index.html', 9 | output: { 10 | entryFileNames: '[hash].js', 11 | chunkFileNames: '[hash].js', 12 | assetFileNames: '[hash][extname]', 13 | format: 'es', 14 | dir: 'dist', 15 | }, 16 | preserveEntrySignatures: false, 17 | 18 | plugins: [ 19 | /** Enable using HTML as rollup entrypoint */ 20 | html({ 21 | absoluteBaseUrl: 'https://holochain-playground.github.io', 22 | minify: true, 23 | }), 24 | /** Resolve bare module imports */ 25 | nodeResolve(), 26 | /** Minify JS */ 27 | terser(), 28 | /** Bundle assets references via import.meta.url */ 29 | importMetaAssets(), 30 | /** Compile JS to a lower language target */ 31 | babel({ 32 | babelHelpers: 'bundled', 33 | presets: [ 34 | [ 35 | require.resolve('@babel/preset-env'), 36 | { 37 | targets: [ 38 | 'last 3 Chrome major versions', 39 | 'last 3 Firefox major versions', 40 | 'last 3 Edge major versions', 41 | 'last 3 Safari major versions', 42 | ], 43 | modules: false, 44 | bugfixes: true, 45 | }, 46 | ], 47 | ], 48 | plugins: [ 49 | [ 50 | require.resolve('babel-plugin-template-html-minifier'), 51 | { 52 | modules: { lit: ['html', { name: 'css', encapsulation: 'style' }] }, 53 | failOnError: false, 54 | strictCSS: true, 55 | htmlMinifier: { 56 | collapseWhitespace: true, 57 | conservativeCollapse: true, 58 | removeComments: true, 59 | caseSensitive: true, 60 | minifyCSS: true, 61 | }, 62 | }, 63 | ], 64 | ], 65 | }), 66 | ], 67 | }; 68 | -------------------------------------------------------------------------------- /fixture/README.md: -------------------------------------------------------------------------------- 1 | # Forum 2 | 3 | ## Environment Setup 4 | 5 | > PREREQUISITE: set up the [holochain development environment](https://developer.holochain.org/docs/install/). 6 | 7 | Enter the nix shell by running this in the root folder of the repository: 8 | 9 | ```bash 10 | nix-shell 11 | npm install 12 | ``` 13 | 14 | **Run all the other instructions in this README from inside this nix-shell, otherwise they won't work**. 15 | 16 | ## Running 2 agents 17 | 18 | ```bash 19 | npm start 20 | ``` 21 | 22 | This will create a network of 2 nodes connected to each other and their respective UIs. 23 | It will also bring up the Holochain Playground for advanced introspection of the conductors. 24 | 25 | ## Running the backend tests 26 | 27 | ```bash 28 | npm test 29 | ``` 30 | 31 | ## Bootstrapping a network 32 | 33 | Create a custom network of nodes connected to each other and their respective UIs with: 34 | 35 | ```bash 36 | AGENTS=3 npm run network 37 | ``` 38 | 39 | Substitute the "3" for the number of nodes that you want to bootstrap in your network. 40 | This will also bring up the Holochain Playground for advanced introspection of the conductors. 41 | 42 | ## Packaging 43 | 44 | To package the web happ: 45 | ``` bash 46 | npm run package 47 | ``` 48 | 49 | You'll have the `forum.webhapp` in `workdir`. This is what you should distribute so that the Holochain Launcher can install it. 50 | You will also have its subcomponent `forum.happ` in the same folder`. 51 | 52 | ## Documentation 53 | 54 | This repository is using these tools: 55 | - [NPM Workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces/): npm v7's built-in monorepo capabilities. 56 | - [hc](https://github.com/holochain/holochain/tree/develop/crates/hc): Holochain CLI to easily manage Holochain development instances. 57 | - [@holochain/tryorama](https://www.npmjs.com/package/@holochain/tryorama): test framework. 58 | - [@holochain/client](https://www.npmjs.com/package/@holochain/client): client library to connect to Holochain from the UI. 59 | - [@holochain-playground/cli](https://www.npmjs.com/package/@holochain-playground/cli): introspection tooling to understand what's going on in the Holochain nodes. 60 | -------------------------------------------------------------------------------- /packages/elements/src/elements/select-active-dna/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DnaHash, 3 | decodeHashFromBase64, 4 | encodeHashToBase64, 5 | } from '@holochain/client'; 6 | import '@shoelace-style/shoelace/dist/components/card/card.js'; 7 | import '@shoelace-style/shoelace/dist/components/option/option.js'; 8 | import '@shoelace-style/shoelace/dist/components/select/select.js'; 9 | import SlSelect from '@shoelace-style/shoelace/dist/components/select/select.js'; 10 | import { css, html } from 'lit'; 11 | import { customElement } from 'lit/decorators.js'; 12 | 13 | import { PlaygroundElement } from '../../base/playground-element.js'; 14 | import { sharedStyles } from '../utils/shared-styles.js'; 15 | 16 | @customElement('select-active-dna') 17 | export class SelectActiveDna extends PlaygroundElement { 18 | render() { 19 | const allDnasResult = this.store.allDnas.get(); 20 | const activeDna = this.store.activeDna.get(); 21 | const allDnas = 22 | allDnasResult.status === 'completed' ? allDnasResult.value : []; 23 | return html` 24 | encodeHashToBase64(d) === encodeHashToBase64(activeDna), 28 | ) 29 | ? encodeHashToBase64(activeDna) 30 | : ''} 31 | @sl-change=${(e: any) => { 32 | const dna = decodeHashFromBase64( 33 | (e.target as SlSelect).value as string, 34 | ); 35 | this.store.activeDna.set(dna); 36 | }} 37 | style="flex: 1" 38 | > 39 | DNA 40 | ${allDnas.map( 41 | dna => html` 42 | ${encodeHashToBase64(dna)} 45 | `, 46 | )} 47 | ${activeDna 48 | ? html` 49 |
50 | 54 |
55 | ` 56 | : html``} 57 |
58 | `; 59 | } 60 | 61 | static get styles() { 62 | return [ 63 | css` 64 | :host { 65 | display: flex; 66 | } 67 | `, 68 | sharedStyles, 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/publish_dht_ops.ts: -------------------------------------------------------------------------------- 1 | import { DhtOp, HoloHash } from '@holochain/client'; 2 | import { DhtOpHash } from '@darksoil-studio/holochain-core-types'; 3 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 4 | 5 | import { getNonPublishedDhtOps } from '../source-chain/utils.js'; 6 | import { getDhtOpBasis } from '../utils.js'; 7 | import { 8 | Workflow, 9 | WorkflowReturn, 10 | WorkflowType, 11 | Workspace, 12 | } from './workflows.js'; 13 | 14 | // From https://github.com/holochain/holochain/blob/develop/crates/holochain/src/core/workflow/publish_dht_ops_workflow.rs 15 | export const publish_dht_ops = async ( 16 | workspace: Workspace, 17 | ): Promise> => { 18 | let workCompleted = true; 19 | const dhtOps = getNonPublishedDhtOps(workspace.state); 20 | 21 | const dhtOpsByBasis: HoloHashMap< 22 | HoloHash, 23 | HoloHashMap 24 | > = new HoloHashMap(); 25 | 26 | for (const [dhtOpHash, dhtOp] of dhtOps.entries()) { 27 | const basis = getDhtOpBasis(dhtOp); 28 | 29 | if (!dhtOpsByBasis.has(basis)) dhtOpsByBasis.set(basis, new HoloHashMap()); 30 | 31 | dhtOpsByBasis.get(basis).set(dhtOpHash, dhtOp); 32 | } 33 | 34 | const promises = Array.from(dhtOpsByBasis.entries()).map( 35 | async ([basis, dhtOps]) => { 36 | try { 37 | // Publish the operations 38 | await workspace.p2p.publish(basis, dhtOps); 39 | 40 | for (const dhtOpHash of dhtOps.keys()) { 41 | workspace.state.authoredDHTOps.get(dhtOpHash).last_publish_time = 42 | Date.now() * 1000; 43 | } 44 | } catch (e) { 45 | workCompleted = false; 46 | } 47 | }, 48 | ); 49 | 50 | await Promise.all(promises); 51 | 52 | const triggers = []; 53 | 54 | if (!workCompleted) { 55 | triggers.push(publish_dht_ops_task()); 56 | } 57 | 58 | return { 59 | result: undefined, 60 | triggers, 61 | }; 62 | }; 63 | 64 | export type PublishDhtOpsWorkflow = Workflow; 65 | 66 | export function publish_dht_ops_task(): PublishDhtOpsWorkflow { 67 | return { 68 | type: WorkflowType.PUBLISH_DHT_OPS, 69 | details: undefined, 70 | task: worskpace => publish_dht_ops(worskpace), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /fixture/ui/src/forum/posts/comments-for-post.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AppClient, 5 | EntryHash, 6 | InstalledCell, 7 | Link, 8 | Record, 9 | } from '@holochain/client'; 10 | import { consume } from '@lit-labs/context'; 11 | import { Task } from '@lit-labs/task'; 12 | import '@material/mwc-circular-progress'; 13 | import { LitElement, html } from 'lit'; 14 | import { customElement, property, state } from 'lit/decorators.js'; 15 | 16 | import { clientContext } from '../../contexts'; 17 | import './comment-detail'; 18 | 19 | @customElement('comments-for-post') 20 | export class CommentsForPost extends LitElement { 21 | @consume({ context: clientContext }) 22 | client!: AppClient; 23 | 24 | @property({ 25 | hasChanged: (newVal: ActionHash, oldVal: ActionHash) => 26 | newVal.toString() !== oldVal.toString(), 27 | }) 28 | postHash!: ActionHash; 29 | 30 | _fetchComments = new Task( 31 | this, 32 | ([postHash]) => 33 | this.client.callZome({ 34 | cap_secret: null, 35 | role_name: 'forum', 36 | zome_name: 'posts', 37 | fn_name: 'get_comments_for_post', 38 | payload: postHash, 39 | }) as Promise>, 40 | () => [this.postHash], 41 | ); 42 | 43 | firstUpdated() { 44 | if (this.postHash === undefined) { 45 | throw new Error( 46 | `The postHash property is required for the comments-for-post element`, 47 | ); 48 | } 49 | } 50 | 51 | renderList(links: Array) { 52 | if (links.length === 0) 53 | return html`No comments found for this post.`; 54 | 55 | return html` 56 |
57 | ${links.map( 58 | link => 59 | html``, 60 | )} 61 |
62 | `; 63 | } 64 | 65 | render() { 66 | return this._fetchComments.render({ 67 | pending: () => 68 | html`
71 | 72 |
`, 73 | complete: links => this.renderList(links), 74 | error: (e: any) => 75 | html`Error fetching comments: ${e.data.data}.`, 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/integrate_dht_ops.ts: -------------------------------------------------------------------------------- 1 | import { IntegratedDhtOpsValue, ValidationStatus } from '../state.js'; 2 | import { pullAllIntegrationLimboDhtOps } from '../dht/get.js'; 3 | import { 4 | putDhtOpData, 5 | putDhtOpMetadata, 6 | putDhtOpToIntegrated, 7 | } from '../dht/put.js'; 8 | import { Workflow, WorkflowReturn, WorkflowType, Workspace } from './workflows.js'; 9 | import { validation_receipt_task } from './validation_receipt.js'; 10 | 11 | // From https://github.com/holochain/holochain/blob/develop/crates/holochain/src/core/workflow/integrate_dht_ops_workflow.rs 12 | export const integrate_dht_ops = async ( 13 | worskpace: Workspace 14 | ): Promise> => { 15 | const opsToIntegrate = pullAllIntegrationLimboDhtOps(worskpace.state); 16 | let workComplete = Array.from(opsToIntegrate.keys()).length === 0; 17 | 18 | for (const [dhtOpHash, integrationLimboValue] of opsToIntegrate.entries()) { 19 | const dhtOp = integrationLimboValue.op; 20 | 21 | if (integrationLimboValue.validation_status === ValidationStatus.Valid) { 22 | putDhtOpData(dhtOp)(worskpace.state); 23 | putDhtOpMetadata(dhtOp)(worskpace.state); 24 | } else if ( 25 | integrationLimboValue.validation_status === ValidationStatus.Rejected 26 | ) { 27 | putDhtOpData(dhtOp)(worskpace.state); 28 | } 29 | 30 | const value: IntegratedDhtOpsValue = { 31 | op: dhtOp, 32 | validation_status: integrationLimboValue.validation_status, 33 | when_integrated: Date.now(), 34 | send_receipt: integrationLimboValue.send_receipt, 35 | }; 36 | 37 | putDhtOpToIntegrated(dhtOpHash, value)(worskpace.state); 38 | } 39 | const triggers = []; 40 | 41 | if (!workComplete) { 42 | triggers.push(integrate_dht_ops_task()); 43 | triggers.push(validation_receipt_task()); 44 | } 45 | 46 | return { 47 | result: undefined, 48 | triggers, 49 | }; 50 | }; 51 | 52 | export type IntegrateDhtOpsWorkflow = Workflow; 53 | 54 | export function integrate_dht_ops_task(): IntegrateDhtOpsWorkflow { 55 | return { 56 | type: WorkflowType.INTEGRATE_DHT_OPS, 57 | details: undefined, 58 | task: (worskpace) => integrate_dht_ops(worskpace), 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .cargo/ 107 | target/ 108 | *.dna 109 | *.happ 110 | .hc* 111 | 112 | .direnv 113 | result* 114 | -------------------------------------------------------------------------------- /packages/elements/src/elements/helpers/editable-field.ts: -------------------------------------------------------------------------------- 1 | import { mdiClose, mdiContentSave, mdiPencil } from '@mdi/js'; 2 | import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'; 3 | import { wrapPathInSvg } from '@darksoil-studio/holochain-elements'; 4 | import { LitElement, html } from 'lit'; 5 | import { customElement, property, state } from 'lit/decorators.js'; 6 | import { ref } from 'lit/directives/ref.js'; 7 | 8 | import { sharedStyles } from '../utils/shared-styles.js'; 9 | 10 | @customElement('editable-field') 11 | export class EditableField extends LitElement { 12 | @property() 13 | value: any; 14 | 15 | @state() 16 | _editing: boolean = false; 17 | @state() 18 | _newValue: any; 19 | @state() 20 | _valid: boolean = true; 21 | 22 | save() { 23 | this.dispatchEvent( 24 | new CustomEvent('field-saved', { detail: { value: this._newValue } }), 25 | ); 26 | this._editing = false; 27 | } 28 | 29 | cancel() { 30 | this._editing = false; 31 | this._newValue = this.value; 32 | } 33 | 34 | firstUpdated() { 35 | this._newValue = this.value; 36 | } 37 | 38 | setupField(fieldSlot: Element | undefined) { 39 | if (!fieldSlot) return; 40 | 41 | setTimeout(() => { 42 | const field = (fieldSlot as HTMLSlotElement).assignedNodes({ 43 | flatten: true, 44 | })[1] as HTMLInputElement; 45 | 46 | field.addEventListener('input', e => { 47 | field.reportValidity(); 48 | this._newValue = (field as any).value; 49 | this._valid = (field as any).validity.valid; 50 | }); 51 | }); 52 | } 53 | 54 | render() { 55 | return html`
56 | ${this._editing 57 | ? html` this.save()} 60 | .disabled=${!this._valid} 61 | .src=${wrapPathInSvg(mdiContentSave)} 62 | > this.cancel()} 65 | .src=${wrapPathInSvg(mdiClose)} 66 | >` 67 | : html`${this.value} (this._editing = true)} 71 | .src=${wrapPathInSvg(mdiPencil)} 72 | >`} 73 |
`; 74 | } 75 | 76 | static styles = [sharedStyles]; 77 | } 78 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/validation_receipt.ts: -------------------------------------------------------------------------------- 1 | import { ValidationReceipt } from '@darksoil-studio/holochain-core-types'; 2 | import uniq from 'lodash-es/uniq.js'; 3 | 4 | import { getBadAgents } from '../../network/utils.js'; 5 | import { getIntegratedDhtOpsWithoutReceipt } from '../dht/get.js'; 6 | import { putDhtOpToIntegrated, putValidationReceipt } from '../dht/put.js'; 7 | import { ValidationStatus } from '../state.js'; 8 | import { 9 | Workflow, 10 | WorkflowReturn, 11 | WorkflowType, 12 | Workspace, 13 | } from './workflows.js'; 14 | 15 | // From https://github.com/holochain/holochain/blob/develop/crates/holochain/src/core/workflow/integrate_dht_ops_workflow.rs 16 | export const validation_receipt = async ( 17 | workspace: Workspace, 18 | ): Promise> => { 19 | const integratedOpsWithoutReceipt = getIntegratedDhtOpsWithoutReceipt( 20 | workspace.state, 21 | ); 22 | const pretendIsValid = 23 | workspace.badAgentConfig && 24 | workspace.badAgentConfig.pretend_invalid_records_are_valid; 25 | 26 | for (const [ 27 | dhtOpHash, 28 | integratedValue, 29 | ] of integratedOpsWithoutReceipt.entries()) { 30 | const receipt: ValidationReceipt = { 31 | dht_op_hash: dhtOpHash, 32 | validation_status: pretendIsValid 33 | ? ValidationStatus.Valid 34 | : integratedValue.validation_status, 35 | validator: workspace.state.agentPubKey, 36 | when_integrated: Date.now() * 1000, 37 | }; 38 | 39 | putValidationReceipt(dhtOpHash, receipt)(workspace.state); 40 | 41 | const badAgents = getBadAgents(workspace.state); 42 | const beforeCount = workspace.state.badAgents.length; 43 | 44 | workspace.state.badAgents = uniq([ 45 | ...workspace.state.badAgents, 46 | ...badAgents, 47 | ]); 48 | 49 | if (beforeCount !== badAgents.length) { 50 | workspace.p2p.syncNeighbors(); 51 | } 52 | 53 | integratedValue.send_receipt = false; 54 | 55 | putDhtOpToIntegrated(dhtOpHash, integratedValue)(workspace.state); 56 | } 57 | 58 | return { 59 | result: undefined, 60 | triggers: [], 61 | }; 62 | }; 63 | 64 | export type ValidationReceiptWorkflow = Workflow; 65 | 66 | export function validation_receipt_task(): ValidationReceiptWorkflow { 67 | return { 68 | type: WorkflowType.VALIDATION_RECEIPT, 69 | details: undefined, 70 | task: worskpace => validation_receipt(worskpace), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /packages/elements/src/elements/utils/shared-styles.ts: -------------------------------------------------------------------------------- 1 | import { sharedStyles as nativeSharedStyles } from '@darksoil-studio/holochain-elements'; 2 | import { css, unsafeCSS } from 'lit'; 3 | 4 | export const sharedStyles = css` 5 | ${unsafeCSS(nativeSharedStyles)} 6 | :host { 7 | display: flex; 8 | } 9 | 10 | .center-content { 11 | align-items: center; 12 | justify-content: center; 13 | display: flex; 14 | } 15 | 16 | span { 17 | margin-block-start: 0; 18 | } 19 | 20 | .title { 21 | font-size: 20px; 22 | } 23 | 24 | .placeholder { 25 | color: rgba(0, 0, 0, 0.6); 26 | } 27 | .json-info { 28 | padding: 4px; 29 | max-width: 400px; 30 | } 31 | 32 | .block-title { 33 | font-size: 20px; 34 | } 35 | 36 | .horizontal-divider { 37 | background-color: grey; 38 | height: 1px; 39 | opacity: 0.3; 40 | margin-bottom: 0; 41 | width: 100%; 42 | } 43 | .vertical-divider { 44 | background-color: grey; 45 | width: 1px; 46 | height: 100%; 47 | opacity: 0.3; 48 | margin-bottom: 0; 49 | } 50 | sl-tab-group { 51 | display: flex; 52 | } 53 | sl-tab-group::part(base) { 54 | display: flex; 55 | flex: 1; 56 | } 57 | sl-tab-group::part(body) { 58 | display: flex; 59 | flex: 1; 60 | } 61 | sl-tab-panel::part(base) { 62 | display: flex; 63 | flex: 1; 64 | width: 100%; 65 | height: 100%; 66 | } 67 | sl-tab-panel { 68 | height: 100%; 69 | width: 100%; 70 | } 71 | 72 | json-viewer { 73 | --background-color: #transparent; 74 | --color: #333333; 75 | --string-color: #e03131; 76 | --number-color: #12b886; 77 | --boolean-color: #5f3dc4; 78 | --null-color: #808080; 79 | --property-color: #228be6; 80 | --preview-color: #bd5f1b; 81 | --highlight-color: #ff0000; 82 | --outline-color: #666968; 83 | --outline-width: 1px; 84 | --outline-style: dotted; 85 | 86 | --font-family: Nimbus Mono PS, Courier New, monospace; 87 | --font-size: 1rem; 88 | --line-height: 1.2rem; 89 | 90 | --indent-size: 0.5em; 91 | --indentguide-size: 1px; 92 | --indentguide-style: solid; 93 | --indentguide-color: #ccc; 94 | --indentguide-color-active: #999; 95 | --indentguide: var(--indentguide-size) var(--indentguide-style) 96 | var(--indentguide-color); 97 | --indentguide-active: var(--indentguide-size) var(--indentguide-style) 98 | var(--indentguide-color-active); 99 | } 100 | `; 101 | -------------------------------------------------------------------------------- /packages/simulator/src/core/hdk/host-fn/query.ts: -------------------------------------------------------------------------------- 1 | import { NewEntryAction, Record } from '@holochain/client'; 2 | import isEqual from 'lodash-es/isEqual.js'; 3 | 4 | import { areEqual } from '../../../processors/hash.js'; 5 | import { ChainQueryFilter } from '../../../types.js'; 6 | import { getAllAuthoredActions } from '../../cell/source-chain/get.js'; 7 | import { HostFn, HostFnWorkspace } from '../host-fn.js'; 8 | 9 | export type QueryFn = (filter: ChainQueryFilter) => Promise>; 10 | 11 | // Creates a new Create action and its entry in the source chain 12 | export const query: HostFn = 13 | (workspace: HostFnWorkspace): QueryFn => 14 | async (filter): Promise> => { 15 | let actions = getAllAuthoredActions(workspace.state); 16 | 17 | // TODO: filter by sequence_range 18 | 19 | if (filter.action_type) { 20 | actions = actions.filter(action => { 21 | const actionType = action.hashed.content.type; 22 | return filter.action_type!.find( 23 | wantedActionType => wantedActionType === actionType, 24 | ); 25 | }); 26 | } 27 | 28 | if (filter.entry_hashes) { 29 | actions = actions.filter(action => { 30 | const entryHash = (action.hashed.content as NewEntryAction).entry_hash; 31 | if (!entryHash) return false; 32 | return filter.entry_hashes!.find(wantedEntryHash => 33 | areEqual(wantedEntryHash, entryHash), 34 | ); 35 | }); 36 | } 37 | 38 | if (filter.entry_type) { 39 | actions = actions.filter(action => { 40 | const entryType = (action.hashed.content as NewEntryAction).entry_type; 41 | if (!entryType) return false; 42 | return filter.entry_type!.find(wantedEntryType => 43 | isEqual(wantedEntryType, entryType), 44 | ); 45 | }); 46 | } 47 | 48 | const records = actions.map(action => { 49 | let entry = undefined; 50 | 51 | if (filter.include_entries) { 52 | if ((action.hashed.content as NewEntryAction).entry_hash) { 53 | entry = workspace.state.CAS.get( 54 | (action.hashed.content as NewEntryAction).entry_hash, 55 | ); 56 | } 57 | } 58 | 59 | return { 60 | signed_action: action, 61 | entry, 62 | }; 63 | }); 64 | const sortedRecords = records.sort((r1, r2) => { 65 | const t1 = r1.signed_action.hashed.content.timestamp; 66 | const t2 = r2.signed_action.hashed.content.timestamp; 67 | return filter.order_descending ? t2 - t1 : t1 - t2; 68 | }); 69 | 70 | return sortedRecords; 71 | }; 72 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/incoming_dht_ops.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, DhtOp } from '@holochain/client'; 2 | import { DhtOpHash } from '@darksoil-studio/holochain-core-types'; 3 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 4 | 5 | import { hasDhtOpBeenProcessed } from '../dht/get.js'; 6 | import { putValidationLimboValue } from '../dht/put.js'; 7 | import { ValidationLimboStatus, ValidationLimboValue } from '../state.js'; 8 | import { getDhtOpBasis } from '../utils.js'; 9 | import { sys_validation_task } from './sys_validation.js'; 10 | import { 11 | Workflow, 12 | WorkflowReturn, 13 | WorkflowType, 14 | Workspace, 15 | } from './workflows.js'; 16 | 17 | // From https://github.com/holochain/holochain/blob/develop/crates/holochain/src/core/workflow/incoming_dht_ops_workflow.rs 18 | export const incoming_dht_ops = 19 | ( 20 | dhtOps: HoloHashMap, 21 | request_validation_receipt: boolean, 22 | from_agent: AgentPubKey | undefined, 23 | ) => 24 | async (workspace: Workspace): Promise> => { 25 | let sysValidate = false; 26 | 27 | for (const [dhtOpHash, dhtOp] of dhtOps.entries()) { 28 | if (!hasDhtOpBeenProcessed(workspace.state, dhtOpHash)) { 29 | const basis = getDhtOpBasis(dhtOp); 30 | 31 | const validationLimboValue: ValidationLimboValue = { 32 | basis, 33 | from_agent, 34 | last_try: undefined, 35 | num_tries: 0, 36 | op: dhtOp, 37 | status: ValidationLimboStatus.Pending, 38 | time_added: Date.now(), 39 | send_receipt: request_validation_receipt, 40 | }; 41 | 42 | putValidationLimboValue( 43 | dhtOpHash, 44 | validationLimboValue, 45 | )(workspace.state); 46 | 47 | sysValidate = true; 48 | } 49 | } 50 | 51 | return { 52 | result: undefined, 53 | triggers: sysValidate ? [sys_validation_task()] : [], 54 | }; 55 | }; 56 | 57 | export type IncomingDhtOpsWorkflow = Workflow< 58 | { from_agent: AgentPubKey; ops: HoloHashMap }, 59 | void 60 | >; 61 | 62 | export function incoming_dht_ops_task( 63 | from_agent: AgentPubKey, 64 | request_validation_receipt: boolean, 65 | ops: HoloHashMap, 66 | ): IncomingDhtOpsWorkflow { 67 | return { 68 | type: WorkflowType.INCOMING_DHT_OPS, 69 | details: { 70 | from_agent, 71 | ops, 72 | }, 73 | task: worskpace => 74 | incoming_dht_ops(ops, request_validation_receipt, from_agent)(worskpace), 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /packages/cli/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain-playground/cli", 3 | "version": "0.500.0", 4 | "description": "CLI tool that boots up the holochain playground to connect to a real running Holochain conductor", 5 | "author": "guillem.cordoba@gmail.com", 6 | "scripts": { 7 | "start": "pnpm build && concurrently \"nodemon --config ./nodemon.config.json\" --raw", 8 | "build": "rimraf dist && cross-env NODE_ENV=development webpack --config webpack.config.js && chmod +x dist/app.js", 9 | "build:release": "dist && cross-env NODE_ENV=production webpack --config webpack.config.js && chmod +x dist/app.js" 10 | }, 11 | "publishConfig": { 12 | "registry": "https://registry.npmjs.org/", 13 | "access": "public" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "main": "dist/app.js", 19 | "bin": { 20 | "holochain-playground": "./dist/app.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/darksoil-studio/holochain-playground" 25 | }, 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@babel/core": "^7.15.5", 29 | "@babel/preset-env": "^7.15.6", 30 | "@holochain-playground/cli-client": "workspace:^0.500.0", 31 | "@mdi/font": "5.9.55", 32 | "@types/body-parser": "^1.19.0", 33 | "@types/dotenv": "^8.2.0", 34 | "@types/express": "^4.17.7", 35 | "@types/node": "^22.7.5", 36 | "@types/prompts": "^2.0.14", 37 | "babel-loader": "^8.2.2", 38 | "babel-polyfill": "^6.26.0", 39 | "body-parser": "^1.19.0", 40 | "bufferutil": "^4.0.3", 41 | "chalk": "^4.1.2", 42 | "clear-npx-cache": "^1.0.1", 43 | "concurrently": "^5.1.0", 44 | "copy-webpack-plugin": "^6.4.1", 45 | "core-js": "^3.6.5", 46 | "cross-env": "^7.0.3", 47 | "deepmerge": "^4.2.2", 48 | "diff": "^5.0.0", 49 | "dotenv": "^8.6.0", 50 | "eslint": "^9.0.0", 51 | "express": "^4.17.1", 52 | "get-port-please": "^2.2.0", 53 | "node-fetch": "^3.0.0", 54 | "nodemon": "^2.0.4", 55 | "open": "^8.2.1", 56 | "path": "^0.12.7", 57 | "prettier": "^3.2.5", 58 | "prompts": "^2.4.1", 59 | "rimraf": "^3.0.2", 60 | "sass": "~1.32", 61 | "sass-loader": "^10.0.0", 62 | "semver": "^7.3.5", 63 | "socket.io": "^4.4.0", 64 | "stream": "^0.0.2", 65 | "ts-loader": "^8.0.0", 66 | "ts-node": "^8.10.2", 67 | "typescript": "^5.4.0", 68 | "utf-8-validate": "^5.0.5", 69 | "webpack": "^5.75.0", 70 | "webpack-cli": "^5.0.1", 71 | "webpack-path-resolve": "^0.0.3" 72 | } 73 | } -------------------------------------------------------------------------------- /packages/simulator/src/core/network/network.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, CellId, DnaHash } from '@holochain/client'; 2 | import { CellMap } from '@darksoil-studio/holochain-utils'; 3 | 4 | import { BootstrapService } from '../../bootstrap/bootstrap-service.js'; 5 | import { Cell } from '../cell/cell.js'; 6 | import { Conductor } from '../conductor.js'; 7 | import { P2pCell, P2pCellState } from '../network/p2p-cell.js'; 8 | import { KitsuneP2p } from './kitsune_p2p.js'; 9 | import { NetworkRequest } from './network-request.js'; 10 | 11 | export interface NetworkState { 12 | // P2pCellState by dna hash / agentPubKey 13 | p2pCellsState: CellMap; 14 | } 15 | 16 | export class Network { 17 | // P2pCells contained in this conductor 18 | p2pCells: CellMap; 19 | 20 | kitsune: KitsuneP2p; 21 | 22 | constructor( 23 | state: NetworkState, 24 | public conductor: Conductor, 25 | public bootstrapService: BootstrapService, 26 | ) { 27 | this.p2pCells = new CellMap(); 28 | for (const [cellId, p2pState] of state.p2pCellsState.entries()) { 29 | this.p2pCells.set( 30 | cellId, 31 | new P2pCell(p2pState, conductor.getCell(cellId) as Cell, this), 32 | ); 33 | } 34 | 35 | this.kitsune = new KitsuneP2p(this); 36 | } 37 | 38 | getState(): NetworkState { 39 | const p2pCellsState: CellMap = new CellMap(); 40 | 41 | for (const [cellId, p2pCell] of this.p2pCells.entries()) { 42 | p2pCellsState.set(cellId, p2pCell.getState()); 43 | } 44 | 45 | return { 46 | p2pCellsState, 47 | }; 48 | } 49 | 50 | getAllP2pCells(): P2pCell[] { 51 | return this.p2pCells.values(); 52 | } 53 | 54 | createP2pCell(cell: Cell): P2pCell { 55 | const cellId = cell.cellId; 56 | const dnaHash = cellId[0]; 57 | 58 | const state: P2pCellState = { 59 | neighbors: [], 60 | farKnownPeers: [], 61 | redundancyFactor: 3, 62 | neighborNumber: 6, 63 | badAgents: [], 64 | }; 65 | 66 | const p2pCell = new P2pCell(state, cell, this); 67 | 68 | this.p2pCells.set(cellId, p2pCell); 69 | 70 | return p2pCell; 71 | } 72 | 73 | removeP2pCell(cellId: CellId) { 74 | this.p2pCells.delete(cellId); 75 | } 76 | 77 | public sendRequest( 78 | dna: DnaHash, 79 | fromAgent: AgentPubKey, 80 | toAgent: AgentPubKey, 81 | request: NetworkRequest, 82 | ): Promise { 83 | const localCell = this.conductor.getCell([dna, toAgent]); 84 | 85 | if (localCell) return request(localCell); 86 | 87 | return request(this.bootstrapService.cells.get([dna, toAgent]) as Cell); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /fixture/tests/src/forum/posts/all-posts.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, test } from "vitest"; 2 | 3 | import { runScenario, dhtSync, CallableCell } from '@holochain/tryorama'; 4 | import { NewEntryAction, ActionHash, Record, AppBundleSource, fakeActionHash, fakeAgentPubKey, fakeEntryHash } from '@holochain/client'; 5 | import { decode } from '@msgpack/msgpack'; 6 | 7 | import { createPost } from './common.js'; 8 | 9 | test('create a Post and get all posts', async () => { 10 | await runScenario(async scenario => { 11 | // Construct proper paths for your app. 12 | // This assumes app bundle created by the `hc app pack` command. 13 | const testAppPath = process.cwd() + '/../workdir/forum.happ'; 14 | 15 | // Set up the app to be installed 16 | const appSource = { appBundleSource: { path: testAppPath } }; 17 | 18 | // Add 2 players with the test app to the Scenario. The returned players 19 | // can be destructured. 20 | const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]); 21 | 22 | // Shortcut peer discovery through gossip and register all agents in every 23 | // conductor of the scenario. 24 | await scenario.shareAllAgents(); 25 | 26 | // Bob gets all posts 27 | let collectionOutput: Link[] = await bob.cells[0].callZome({ 28 | zome_name: "posts", 29 | fn_name: "get_all_posts", 30 | payload: null 31 | }); 32 | assert.equal(collectionOutput.length, 0); 33 | 34 | // Alice creates a Post 35 | const createRecord: Record = await createPost(alice.cells[0]); 36 | assert.ok(createRecord); 37 | 38 | await dhtSync([alice, bob], alice.cells[0].cell_id[0]); 39 | 40 | // Bob gets all posts again 41 | collectionOutput = await bob.cells[0].callZome({ 42 | zome_name: "posts", 43 | fn_name: "get_all_posts", 44 | payload: null 45 | }); 46 | assert.equal(collectionOutput.length, 1); 47 | assert.deepEqual(createRecord.signed_action.hashed.hash, collectionOutput[0].target); 48 | 49 | // Alice deletes the Post 50 | await alice.cells[0].callZome({ 51 | zome_name: "posts", 52 | fn_name: "delete_post", 53 | payload: createRecord.signed_action.hashed.hash 54 | }); 55 | 56 | await dhtSync([alice, bob], alice.cells[0].cell_id[0]); 57 | 58 | // Bob gets all posts again 59 | collectionOutput = await bob.cells[0].callZome({ 60 | zome_name: "posts", 61 | fn_name: "get_all_posts", 62 | payload: null 63 | }); 64 | assert.equal(collectionOutput.length, 0); 65 | }); 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /fixture/ui/src/forum/posts/all-posts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AppClient, 5 | EntryHash, 6 | Link, 7 | NewEntryAction, 8 | Record, 9 | } from '@holochain/client'; 10 | import { consume } from '@lit-labs/context'; 11 | import { Task } from '@lit-labs/task'; 12 | import '@material/mwc-circular-progress'; 13 | import { LitElement, html } from 'lit'; 14 | import { customElement, property, state } from 'lit/decorators.js'; 15 | 16 | import { clientContext } from '../../contexts'; 17 | import './post-detail'; 18 | import { PostsSignal } from './types'; 19 | 20 | @customElement('all-posts') 21 | export class AllPosts extends LitElement { 22 | @consume({ context: clientContext }) 23 | client!: AppClient; 24 | 25 | @state() 26 | signaledHashes: Array = []; 27 | 28 | _fetchPosts = new Task( 29 | this, 30 | ([]) => 31 | this.client.callZome({ 32 | cap_secret: null, 33 | role_name: 'forum', 34 | zome_name: 'posts', 35 | fn_name: 'get_all_posts', 36 | payload: null, 37 | }) as Promise>, 38 | () => [], 39 | ); 40 | 41 | firstUpdated() { 42 | this.client.on('signal', signal => { 43 | if (signal.type !== 'app') return; 44 | if (signal.zome_name !== 'posts') return; 45 | const payload = signal.payload as PostsSignal; 46 | if (payload.type !== 'EntryCreated') return; 47 | if (payload.app_entry.type !== 'Post') return; 48 | this.signaledHashes = [ 49 | payload.action.hashed.hash, 50 | ...this.signaledHashes, 51 | ]; 52 | }); 53 | } 54 | 55 | renderList(hashes: Array) { 56 | if (hashes.length === 0) return html`No posts found.`; 57 | 58 | return html` 59 |
60 | ${hashes.map( 61 | hash => 62 | html` { 66 | this._fetchPosts.run(); 67 | this.signaledHashes = []; 68 | }} 69 | >`, 70 | )} 71 |
72 | `; 73 | } 74 | 75 | render() { 76 | return this._fetchPosts.render({ 77 | pending: () => 78 | html`
81 | 82 |
`, 83 | complete: links => 84 | this.renderList([...this.signaledHashes, ...links.map(l => l.target)]), 85 | error: (e: any) => 86 | html`Error fetching the posts: ${e.data.data}.`, 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/simulator/src/core/network/gossip/bloom/index.ts: -------------------------------------------------------------------------------- 1 | import { DhtOpHash } from '@darksoil-studio/holochain-core-types'; 2 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 3 | 4 | import { sleep } from '../../../../executor/delay-middleware.js'; 5 | import { getValidationReceipts } from '../../../cell/index.js'; 6 | import { P2pCell } from '../../p2p-cell.js'; 7 | import { getBadActions } from '../../utils.js'; 8 | import { GossipData, GossipDhtOpData } from '../types.js'; 9 | 10 | export const GOSSIP_INTERVAL_MS = 500; 11 | 12 | export class SimpleBloomMod { 13 | gossip_on: boolean = true; 14 | 15 | lastBadActions = 0; 16 | 17 | constructor(protected p2pCell: P2pCell) { 18 | this.loop(); 19 | } 20 | async loop() { 21 | while (true) { 22 | if (this.gossip_on) { 23 | try { 24 | await this.run_one_iteration(); 25 | } catch (e) { 26 | console.warn('Connection closed'); 27 | } 28 | } 29 | await sleep(GOSSIP_INTERVAL_MS); 30 | } 31 | } 32 | 33 | async run_one_iteration(): Promise { 34 | const localDhtOpsHashes = Array.from( 35 | this.p2pCell.cell._state.integratedDHTOps.keys(), 36 | ); 37 | const localDhtOps = 38 | this.p2pCell.cell.handle_fetch_op_hash_data(localDhtOpsHashes); 39 | 40 | const state = this.p2pCell.cell._state; 41 | 42 | const dhtOpData: HoloHashMap = 43 | new HoloHashMap(); 44 | 45 | for (const dhtOpHash of localDhtOps.keys()) { 46 | const receipts = getValidationReceipts(dhtOpHash)(state); 47 | dhtOpData.set(dhtOpHash, { 48 | op: localDhtOps.get(dhtOpHash), 49 | validation_receipts: receipts, 50 | }); 51 | } 52 | 53 | const pretendValid = 54 | this.p2pCell.cell.conductor.badAgent && 55 | this.p2pCell.cell.conductor.badAgent.config 56 | .pretend_invalid_records_are_valid; 57 | 58 | const badActions = pretendValid ? [] : getBadActions(state); 59 | 60 | const gossips: GossipData = { 61 | badActions, 62 | neighbors: [], 63 | validated_dht_ops: dhtOpData, 64 | }; 65 | 66 | let warrant = 67 | badActions.length > 0 && badActions.length !== this.lastBadActions; 68 | this.lastBadActions = badActions.length; 69 | 70 | if (warrant) { 71 | const promises = [ 72 | ...this.p2pCell.neighbors, 73 | ...this.p2pCell.farKnownPeers, 74 | ].map(peer => this.p2pCell.outgoing_gossip(peer, gossips, warrant)); 75 | 76 | await Promise.all(promises); 77 | } else { 78 | for (const neighbor of this.p2pCell.neighbors) { 79 | await this.p2pCell.outgoing_gossip(neighbor, gossips, warrant); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/simulator/src/core/network/kitsune_p2p.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, AnyDhtHash, DnaHash } from '@holochain/client'; 2 | 3 | import { Cell } from '../cell/cell.js'; 4 | import { NetworkRequest } from './network-request.js'; 5 | import { Network } from './network.js'; 6 | 7 | export class KitsuneP2p { 8 | discover: Discover; 9 | constructor(protected network: Network) { 10 | this.discover = new Discover(network); 11 | } 12 | 13 | async rpc_single( 14 | dna_hash: DnaHash, 15 | from_agent: AgentPubKey, 16 | to_agent: AgentPubKey, 17 | networkRequest: NetworkRequest, 18 | ): Promise { 19 | const peer = await this.discover.peer_discover( 20 | dna_hash, 21 | from_agent, 22 | to_agent, 23 | ); 24 | if (!peer) throw new Error('Peer does not exist anymore'); 25 | return networkRequest(peer); 26 | } 27 | 28 | async rpc_multi( 29 | dna_hash: DnaHash, 30 | from_agent: AgentPubKey, 31 | basis: AnyDhtHash, 32 | remote_agent_count: number, 33 | filtered_agents: AgentPubKey[], 34 | networkRequest: NetworkRequest, 35 | ): Promise> { 36 | // Discover neighbors 37 | return this.discover.message_neighborhood( 38 | dna_hash, 39 | from_agent, 40 | basis, 41 | remote_agent_count, 42 | filtered_agents, 43 | networkRequest, 44 | ); 45 | } 46 | } 47 | 48 | // From https://github.com/holochain/holochain/blob/develop/crates/kitsune_p2p/kitsune_p2p/src/spawn/actor/discover.rs 49 | export class Discover { 50 | constructor(protected network: Network) {} 51 | 52 | // TODO fix this 53 | async peer_discover( 54 | dna_hash: DnaHash, 55 | from_agent: AgentPubKey, 56 | to_agent: AgentPubKey, 57 | ): Promise { 58 | return this.network.bootstrapService.cells.get([dna_hash, to_agent]); 59 | } 60 | 61 | async message_neighborhood( 62 | dna_hash: DnaHash, 63 | from_agent: AgentPubKey, 64 | basis: AnyDhtHash, 65 | remote_agent_count: number, 66 | filtered_agents: AgentPubKey[], 67 | networkRequest: NetworkRequest, 68 | ): Promise> { 69 | const agents = await this.search_for_agents( 70 | dna_hash, 71 | basis, 72 | remote_agent_count, 73 | filtered_agents, 74 | ); 75 | 76 | const promises = agents.map(cell => networkRequest(cell)); 77 | return Promise.all(promises); 78 | } 79 | 80 | private async search_for_agents( 81 | dna_hash: DnaHash, 82 | basis: AnyDhtHash, 83 | remote_agent_count: number, 84 | filtered_agents: AgentPubKey[], 85 | ): Promise { 86 | return this.network.bootstrapService.getNeighborhood( 87 | dna_hash, 88 | basis, 89 | remote_agent_count, 90 | filtered_agents, 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/state/metadata.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | EntryHash, 5 | LinkType, 6 | NewEntryAction, 7 | Timestamp, 8 | } from '@holochain/client'; 9 | import { EntryDhtStatus } from '@darksoil-studio/holochain-core-types'; 10 | import { HoloHashMap } from '@darksoil-studio/holochain-utils'; 11 | 12 | // From https://github.com/holochain/holochain/blob/develop/crates/holochain/src/core/state/metadata.rs 13 | 14 | export interface Metadata { 15 | // Stores an array of actions indexed by entry hash 16 | system_meta: HoloHashMap; 17 | link_meta: Array<{ key: LinkMetaKey; value: LinkMetaVal }>; 18 | misc_meta: HoloHashMap; 19 | activity: HoloHashMap; 20 | } 21 | 22 | export type SysMetaVal = 23 | | { 24 | NewEntry: ActionHash; 25 | } 26 | | { 27 | Update: ActionHash; 28 | } 29 | | { 30 | Delete: ActionHash; 31 | } 32 | | { 33 | Activity: ActionHash; 34 | } 35 | | { 36 | DeleteLink: ActionHash; 37 | } 38 | | { 39 | CustomPackage: ActionHash; 40 | }; 41 | 42 | export function getSysMetaValActionHash( 43 | sys_meta_val: SysMetaVal, 44 | ): ActionHash | undefined { 45 | if ((sys_meta_val as { NewEntry: ActionHash }).NewEntry) 46 | return (sys_meta_val as { NewEntry: ActionHash }).NewEntry; 47 | if ((sys_meta_val as { Update: ActionHash }).Update) 48 | return (sys_meta_val as { Update: ActionHash }).Update; 49 | if ((sys_meta_val as { Delete: ActionHash }).Delete) 50 | return (sys_meta_val as { Delete: ActionHash }).Delete; 51 | if ((sys_meta_val as { Activity: ActionHash }).Activity) 52 | return (sys_meta_val as { Activity: ActionHash }).Activity; 53 | return undefined; 54 | } 55 | 56 | export interface LinkMetaKey { 57 | base: EntryHash; 58 | zome_index: number; 59 | tag: any; 60 | link_type: LinkType; 61 | action_hash: ActionHash; 62 | } 63 | 64 | export interface LinkMetaVal { 65 | link_add_hash: ActionHash; 66 | target: EntryHash; 67 | timestamp: Timestamp; 68 | zome_index: number; 69 | link_type: LinkType; 70 | tag: any; 71 | } 72 | 73 | export type MiscMetaVal = 74 | | { 75 | EntryStatus: EntryDhtStatus; 76 | } 77 | | 'StoreRecord' 78 | | { ChainItem: Timestamp } 79 | | { ChainObserved: HighestObserved } 80 | | { ChainStatus: ChainStatus }; 81 | 82 | export enum ChainStatus { 83 | Empty, 84 | Valid, 85 | Forked, 86 | Invalid, 87 | } 88 | 89 | export interface HighestObserved { 90 | action_seq: number; 91 | hash: ActionHash[]; 92 | } 93 | export interface CoreEntryDetails { 94 | actions: NewEntryAction[]; 95 | links: LinkMetaVal[]; 96 | dhtStatus: EntryDhtStatus; 97 | } 98 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/genesis.ts: -------------------------------------------------------------------------------- 1 | import { AgentPubKey, CellId, DnaHash, Entry } from '@holochain/client'; 2 | 3 | import { 4 | buildAgentValidationPkg, 5 | buildCreate, 6 | buildDna, 7 | buildShh, 8 | } from '../source-chain/builder-actions.js'; 9 | import { 10 | getSourceChainRecord, 11 | getSourceChainRecords, 12 | } from '../source-chain/get.js'; 13 | import { putRecord } from '../source-chain/put.js'; 14 | import { CellState } from '../state.js'; 15 | import { run_agent_validation_callback } from './app_validation.js'; 16 | import { produce_dht_ops_task } from './produce_dht_ops.js'; 17 | import { 18 | Workflow, 19 | WorkflowReturn, 20 | WorkflowType, 21 | Workspace, 22 | } from './workflows.js'; 23 | 24 | export const genesis = 25 | (agentId: AgentPubKey, dnaHash: DnaHash, membrane_proof: any) => 26 | async (worskpace: Workspace): Promise> => { 27 | const dna = buildDna(dnaHash, agentId); 28 | putRecord({ 29 | signed_action: buildShh(dna), 30 | entry: { NotApplicable: undefined }, 31 | })(worskpace.state); 32 | 33 | const pkg = buildAgentValidationPkg(worskpace.state, membrane_proof); 34 | putRecord({ 35 | signed_action: buildShh(pkg), 36 | entry: { NotApplicable: undefined }, 37 | })(worskpace.state); 38 | 39 | const entry: Entry = { 40 | entry: agentId, 41 | entry_type: 'Agent', 42 | }; 43 | const create_agent_pub_key_entry = buildCreate( 44 | worskpace.state, 45 | entry, 46 | 'Agent', 47 | ); 48 | putRecord({ 49 | signed_action: buildShh(create_agent_pub_key_entry), 50 | entry: { 51 | Present: entry, 52 | }, 53 | })(worskpace.state); 54 | 55 | if ( 56 | !( 57 | worskpace.badAgentConfig && 58 | worskpace.badAgentConfig.disable_validation_before_publish 59 | ) 60 | ) { 61 | const firstRecords = getSourceChainRecords(worskpace.state, 0, 3); 62 | const result = await run_agent_validation_callback( 63 | worskpace, 64 | firstRecords, 65 | ); 66 | if (!result.resolved) throw new Error('Unresolved in agent validate?'); 67 | else if (!result.valid) throw new Error('Agent is invalid in this Dna'); 68 | } 69 | 70 | return { 71 | result: undefined, 72 | triggers: [produce_dht_ops_task()], 73 | }; 74 | }; 75 | 76 | export type GenesisWorkflow = Workflow< 77 | { cellId: CellId; membrane_proof: any }, 78 | void 79 | >; 80 | 81 | export function genesis_task( 82 | cellId: CellId, 83 | membrane_proof: any, 84 | ): GenesisWorkflow { 85 | return { 86 | type: WorkflowType.GENESIS, 87 | details: { 88 | cellId, 89 | membrane_proof, 90 | }, 91 | task: worskpace => genesis(cellId[1], cellId[0], membrane_proof)(worskpace), 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /packages/simulator/src/processors/hash.ts: -------------------------------------------------------------------------------- 1 | import { HoloHash, HoloHashB64 } from '@holochain/client'; 2 | import blake from 'blakejs'; 3 | 4 | const hashLocationCache: Record = {}; 5 | 6 | export function location(bytesHash: HoloHash): number { 7 | let bytesStr = bytesHash.toString(); 8 | if (hashLocationCache[bytesStr]) { 9 | return hashLocationCache[bytesStr]; 10 | } 11 | const bytes = locationBytes(bytesHash); 12 | const view = new DataView(bytes.buffer, 0); 13 | const location = wrap(view.getUint32(0, false)); 14 | hashLocationCache[bytesStr] = location; 15 | 16 | return location; 17 | } 18 | 19 | function locationBytes(bytesHash: HoloHash): Uint8Array { 20 | const hash128: Uint8Array = blake.blake2b(bytesHash, undefined, 16); 21 | 22 | const out = [hash128[0], hash128[1], hash128[2], hash128[3]]; 23 | 24 | for (let i = 4; i < 16; i += 4) { 25 | out[0] ^= hash128[i]; 26 | out[1] ^= hash128[i + 1]; 27 | out[2] ^= hash128[i + 2]; 28 | out[3] ^= hash128[i + 3]; 29 | } 30 | return new Uint8Array(out); 31 | } 32 | 33 | const distanceCache: Record> = {}; 34 | 35 | // We return the distance as the shortest distance between two hashes in the circle 36 | export function distance(hash1: HoloHash, hash2: HoloHash): number { 37 | const hash1Str = hash1.toString(); 38 | const hash2Str = hash2.toString(); 39 | const firstHash = hash1Str > hash2Str ? hash1Str : hash2Str; 40 | const secondHash = hash1Str > hash2Str ? hash2Str : hash1Str; 41 | 42 | if (distanceCache[firstHash] && distanceCache[firstHash][secondHash]) { 43 | return distanceCache[firstHash][secondHash]; 44 | } 45 | 46 | const location1 = location(hash1); 47 | const location2 = location(hash2); 48 | 49 | const distance = shortest_arc_distance(location1, location2) + 1; 50 | 51 | if (!distanceCache[firstHash]) { 52 | distanceCache[firstHash] = {}; 53 | } 54 | distanceCache[firstHash][secondHash] = distance; 55 | 56 | return distance; 57 | } 58 | 59 | export function areEqual(b1: Uint8Array, b2: Uint8Array): boolean { 60 | return b1.toString() === b2.toString(); 61 | } 62 | 63 | export function hashToString(holoHash: HoloHash): string { 64 | return holoHash.toString(); 65 | } 66 | 67 | export function shortest_arc_distance( 68 | location1: number, 69 | location2: number, 70 | ): number { 71 | const distance1 = wrap(location1 - location2); 72 | const distance2 = wrap(location2 - location1); 73 | return Math.min(distance1, distance2); 74 | } 75 | 76 | const MAX_UINT = 4294967295; 77 | 78 | export function wrap(uint: number): number { 79 | if (uint < 0) return 1 + MAX_UINT + uint; 80 | if (uint > MAX_UINT) return uint - MAX_UINT; 81 | return uint; 82 | } 83 | -------------------------------------------------------------------------------- /packages/elements/README.md: -------------------------------------------------------------------------------- 1 | # Holochain Playground 2 | 3 | Visit the [playground](https://darksoil.studio/holochain-playground/). 4 | 5 | This is an experimental ongoing effort to build a holochain playground simulation. It's trying to follow as accurately as possible the internal mechanisms of holochain, displaying the DHT and enabling detailed inspection. 6 | 7 | This package is distributed as an [NPM package component library](https://npmjs.com/package/holochain-playground), in the form of a collection of web-components build with the [Custom Elements](https://developers.google.com/web/fundamentals/web-components/customelements) API. 8 | 9 | ## Library Usage 10 | 11 | 1. Install the package with `npm i @holochain-playground/elements` . 12 | 2. Import the `holochain-playground-provider` in your application like this: 13 | 14 | ``` js 15 | import "holochain-playground/elements/holochain-playground-provider"; 16 | ``` 17 | 18 | 3. Declare the `` element: 19 | 20 | ``` html 21 | 22 | 23 | 24 | ``` 25 | 26 | This is the fundamental element for the playground to work, as it provides the state for all other elements you declare inside it. 27 | 28 | 4. Import any elements you want from the library, and declare them inside the `holochain-playground-provider` : 29 | 30 | ``` js 31 | import 'holochain-playground/elements/holochain-playground-dht-graph' 32 | ``` 33 | 34 | ``` html 35 | 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | 5. Optionally, set the conductor urls to the nodes you want to bind the playground to: 43 | 44 | ``` html 45 | 46 | 47 | 48 | 54 | 55 | ``` 56 | 57 | ## Elements Library 58 | 59 | In the future, this will be shown with storybook. 60 | 61 | ### Technical data display 62 | 63 | - `holochain-playground-dht-graph` 64 | - `holochain-playground-dht-stats` 65 | - `holochain-playground-dht-shard` 66 | - `holochain-playground-entry-detail` 67 | - `holochain-playground-entry-graph` 68 | - `holochain-playground-source-chain` 69 | 70 | ### Utilities 71 | - `holochain-playground-conductor-detail` 72 | - `holochain-playground-create-entries` 73 | - `holochain-playground-import-export` 74 | - `holochain-playground-connect-to-nodes` 75 | - `holochain-playground-select-dna` 76 | -------------------------------------------------------------------------------- /packages/elements/src/elements/entry-contents/index.ts: -------------------------------------------------------------------------------- 1 | import '@alenaksu/json-viewer'; 2 | import '@shoelace-style/shoelace/dist/components/card/card.js'; 3 | import '@darksoil-studio/holochain-elements/dist/elements/holo-identicon.js'; 4 | import { css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { PlaygroundElement } from '../../base/playground-element.js'; 8 | import { shortenStrRec } from '../utils/hash.js'; 9 | import { sharedStyles } from '../utils/shared-styles.js'; 10 | import { getEntryContents } from '../utils/utils.js'; 11 | 12 | /** 13 | * @element entry-contents 14 | */ 15 | @customElement('entry-contents') 16 | export class EntryContents extends PlaygroundElement { 17 | @property({ type: Boolean, attribute: 'hide-header' }) 18 | hideHeader: boolean = false; 19 | 20 | render() { 21 | const activeDhtHash = this.store.activeDhtHash.get(); 22 | const activeContent = this.store.activeContent.get(); 23 | 24 | return html` 25 |
26 | ${this.hideHeader 27 | ? html`` 28 | : html` 29 | 33 | ${activeContent.status === 'completed' && 34 | activeContent.value && 35 | activeContent.value.type 36 | ? 'Action' 37 | : 'Entry'} 38 | Contents${activeDhtHash 39 | ? html` 43 | , with hash 44 | ` 49 | : html``} 51 | `} 52 | ${activeContent.status === 'completed' && activeContent.value 53 | ? html` 54 |
55 |
56 |
57 |
58 | 66 |
67 |
68 |
69 |
70 | ` 71 | : html` 72 |
73 | Select entry to inspect. 74 |
75 | `} 76 |
77 | `; 78 | } 79 | 80 | static styles = [ 81 | css` 82 | :host { 83 | display: flex; 84 | flex: 1; 85 | } 86 | `, 87 | sharedStyles, 88 | ]; 89 | } 90 | -------------------------------------------------------------------------------- /packages/simulator/src/dnas/simulated-dna.ts: -------------------------------------------------------------------------------- 1 | import { HashType, hash } from '@darksoil-studio/holochain-utils'; 2 | import { 3 | AgentPubKey, 4 | AgentPubKeyB64, 5 | CellId, 6 | DnaHash, 7 | DnaModifiers, 8 | EntryVisibility, 9 | HoloHash, 10 | Record, 11 | } from '@holochain/client'; 12 | import { encode } from '@msgpack/msgpack'; 13 | 14 | import { ValidationOutcome } from '../core/cell/sys_validate/types.js'; 15 | import { Conductor } from '../core/conductor.js'; 16 | import { 17 | SimulatedValidateFunctionContext, 18 | SimulatedZomeFunctionContext, 19 | } from '../core/hdk/index.js'; 20 | import { Dictionary } from '../types.js'; 21 | 22 | export interface SimulatedZomeFunctionArgument { 23 | name: string; 24 | type: string; 25 | } 26 | 27 | export interface SimulatedZomeFunction { 28 | call: ( 29 | context: SimulatedZomeFunctionContext, 30 | ) => (payload: any) => Promise; 31 | arguments: SimulatedZomeFunctionArgument[]; 32 | } 33 | 34 | export type SimulatedValidateFunction = ( 35 | context: SimulatedValidateFunctionContext, 36 | ) => (payload: any) => Promise; 37 | 38 | export interface SimulatedZome { 39 | name: string; 40 | entry_defs: Array; 41 | zome_functions: Dictionary; 42 | validate?: SimulatedValidateFunction; 43 | blocklyCode?: string; 44 | } 45 | 46 | export interface SimulatedDna { 47 | zomes: Array; 48 | properties: Dictionary; 49 | networkSeed: string; 50 | } 51 | 52 | export interface SimulatedDnaRole { 53 | dna: SimulatedDna; 54 | deferred: boolean; 55 | } 56 | export interface SimulatedHappBundle { 57 | name: string; 58 | description: string; 59 | roles: Dictionary; 60 | } 61 | 62 | export interface AppRole { 63 | base_cell_id: CellId; 64 | is_provisioned: boolean; 65 | clones: Dictionary; 66 | } 67 | 68 | export interface InstalledHapp { 69 | app_id: string; 70 | agent_pub_key: AgentPubKey; 71 | roles: Dictionary; 72 | installed_at: number; 73 | } 74 | 75 | export interface EntryDef { 76 | id: string; 77 | visibility: EntryVisibility; 78 | } 79 | 80 | export function hashDna(dna: SimulatedDna): HoloHash { 81 | const freeOfFunctionsDna = deepMap(dna, f => { 82 | if (typeof f !== 'function') return f; 83 | else return f.toString(); 84 | }); 85 | 86 | return hash(freeOfFunctionsDna, HashType.DNA); 87 | } 88 | 89 | function deepMap(obj: any, cb: (o: T, key: string) => R) { 90 | var out: any = {}; 91 | 92 | Object.keys(obj).forEach(function (k) { 93 | var val: any; 94 | 95 | if (obj[k] !== null && typeof obj[k] === 'object') { 96 | val = deepMap(obj[k], cb); 97 | } else { 98 | val = cb(obj[k], k); 99 | } 100 | 101 | out[k] = val as any; 102 | }); 103 | 104 | return out; 105 | } 106 | -------------------------------------------------------------------------------- /packages/simulator/src/core/cell/workflows/app_validation/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyDhtHash, 3 | ChainOp, 4 | ChainOpType, 5 | CreateLink, 6 | Delete, 7 | DeleteLink, 8 | Entry, 9 | NewEntryAction, 10 | Record, 11 | RecordEntry, 12 | SignedActionHashed, 13 | Update, 14 | encodeHashToBase64, 15 | } from '@holochain/client'; 16 | import { hashAction } from '@darksoil-studio/holochain-utils'; 17 | 18 | import { isPublic } from '../../source-chain/utils'; 19 | import { getDhtOpAction, getDhtOpSignature, getEntry } from '../../utils'; 20 | 21 | export class MissingDependenciesError extends Error { 22 | constructor(public missingDepsHashes: AnyDhtHash[]) { 23 | super( 24 | `Missing depencencies: ${missingDepsHashes.map(encodeHashToBase64).join(',')}`, 25 | ); 26 | } 27 | } 28 | export function chainOpToRecord(op: ChainOp): Record { 29 | const action = getDhtOpAction(op); 30 | const actionHash = hashAction(action); 31 | let entry: RecordEntry = { 32 | NotApplicable: undefined, 33 | }; 34 | if ((action as NewEntryAction).entry_hash) { 35 | const e = getEntry({ ChainOp: op }); 36 | const publicEntryType = isPublic((action as NewEntryAction).entry_type); 37 | entry = e 38 | ? { 39 | Present: e, 40 | } 41 | : publicEntryType 42 | ? { 43 | NotStored: undefined, 44 | } 45 | : { 46 | Hidden: undefined, 47 | }; 48 | } 49 | 50 | return { 51 | entry, 52 | signed_action: { 53 | hashed: { 54 | content: action, 55 | hash: actionHash, 56 | }, 57 | signature: getDhtOpSignature(op), 58 | }, 59 | }; 60 | } 61 | 62 | export type Op = 63 | | { 64 | StoreRecord: StoreRecord; 65 | } 66 | | { 67 | StoreEntry: StoreEntry; 68 | } 69 | | { 70 | RegisterUpdate: RegisterUpdate; 71 | } 72 | | { 73 | RegisterDelete: RegisterDelete; 74 | } 75 | | { 76 | RegisterAgentActivity: RegisterAgentActivity; 77 | } 78 | | { 79 | RegisterCreateLink: RegisterCreateLink; 80 | } 81 | | { 82 | RegisterDeleteLink: RegisterDeleteLink; 83 | }; 84 | export interface StoreRecord { 85 | record: Record; 86 | } 87 | export interface StoreEntry { 88 | action: SignedActionHashed; 89 | entry: Entry; 90 | } 91 | 92 | export interface RegisterUpdate { 93 | update: SignedActionHashed; 94 | new_entry: Entry | undefined; 95 | } 96 | 97 | export interface RegisterDelete { 98 | delete: SignedActionHashed; 99 | } 100 | 101 | export interface RegisterAgentActivity { 102 | action: SignedActionHashed; 103 | cached_entry: Entry | undefined; 104 | } 105 | export interface RegisterCreateLink { 106 | create_link: SignedActionHashed; 107 | } 108 | export interface RegisterDeleteLink { 109 | delete_link: SignedActionHashed; 110 | create_link: CreateLink; 111 | } 112 | -------------------------------------------------------------------------------- /fixture/ui/src/forum/posts/create-comment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionHash, 3 | AgentPubKey, 4 | AppClient, 5 | DnaHash, 6 | EntryHash, 7 | InstalledCell, 8 | Record, 9 | } from '@holochain/client'; 10 | import { consume } from '@lit-labs/context'; 11 | import '@material/mwc-button'; 12 | import '@material/mwc-snackbar'; 13 | import { Snackbar } from '@material/mwc-snackbar'; 14 | import '@material/mwc-textarea'; 15 | import { LitElement, html } from 'lit'; 16 | import { customElement, property, state } from 'lit/decorators.js'; 17 | 18 | import { clientContext } from '../../contexts'; 19 | import { Comment } from './types'; 20 | 21 | @customElement('create-comment') 22 | export class CreateComment extends LitElement { 23 | @consume({ context: clientContext }) 24 | client!: AppClient; 25 | 26 | @property() 27 | postHash!: ActionHash; 28 | 29 | @state() 30 | _comment: string = ''; 31 | 32 | firstUpdated() { 33 | if (this.postHash === undefined) { 34 | throw new Error( 35 | `The postHash input is required for the create-comment element`, 36 | ); 37 | } 38 | } 39 | 40 | isCommentValid() { 41 | return true && this._comment !== ''; 42 | } 43 | 44 | async createComment() { 45 | const comment: Comment = { 46 | comment: this._comment, 47 | post_hash: this.postHash, 48 | }; 49 | 50 | try { 51 | const record: Record = await this.client.callZome({ 52 | cap_secret: null, 53 | role_name: 'forum', 54 | zome_name: 'posts', 55 | fn_name: 'create_comment', 56 | payload: comment, 57 | }); 58 | 59 | this.dispatchEvent( 60 | new CustomEvent('comment-created', { 61 | composed: true, 62 | bubbles: true, 63 | detail: { 64 | commentHash: record.signed_action.hashed.hash, 65 | }, 66 | }), 67 | ); 68 | } catch (e: any) { 69 | const errorSnackbar = this.shadowRoot?.getElementById( 70 | 'create-error', 71 | ) as Snackbar; 72 | errorSnackbar.labelText = `Error creating the comment: ${e.data.data}`; 73 | errorSnackbar.show(); 74 | } 75 | } 76 | 77 | render() { 78 | return html` 79 | 80 |
81 | Create Comment 82 | 83 |
84 | { 89 | this._comment = (e.target as any).value; 90 | }} 91 | required 92 | > 93 |
94 | 95 | this.createComment()} 100 | > 101 |
`; 102 | } 103 | } 104 | --------------------------------------------------------------------------------