├── .name ├── web ├── static │ ├── robots.txt │ ├── preview.png │ ├── images │ │ └── wallets │ │ │ ├── frame.png │ │ │ ├── web3-default.png │ │ │ ├── status.svg │ │ │ ├── walletconnect.svg │ │ │ ├── portis.svg │ │ │ └── metamask.svg │ └── maskable_icon_512x512.png ├── src │ ├── lib │ │ ├── utils │ │ │ ├── data.ts │ │ │ ├── time.ts │ │ │ ├── path.ts │ │ │ ├── localCache.ts │ │ │ ├── debug.ts │ │ │ └── url.ts │ │ ├── components │ │ │ ├── web │ │ │ │ ├── NoInstallPrompt.svelte │ │ │ │ ├── NewVersionNotification.svelte │ │ │ │ └── Install.svelte │ │ │ ├── alert │ │ │ │ ├── InlineInfo.svelte │ │ │ │ ├── AlertWithSlot.svelte │ │ │ │ └── Alert.svelte │ │ │ ├── navigation │ │ │ │ └── PageLink.svelte │ │ │ ├── ethereum │ │ │ │ ├── ImgBlockie.svelte │ │ │ │ └── CanvasBlockie.svelte │ │ │ ├── daisyui │ │ │ │ ├── NavTabs.svelte │ │ │ │ ├── ThemeSwitcher.svelte │ │ │ │ ├── themes.ts │ │ │ │ └── ThemeChanger.svelte │ │ │ ├── utilities │ │ │ │ ├── Executor.svelte │ │ │ │ ├── CopyBlock.svelte │ │ │ │ ├── clipboard.ts │ │ │ │ ├── focusTrap.ts │ │ │ │ └── click-outside.ts │ │ │ └── modals │ │ │ │ ├── types.ts │ │ │ │ ├── stores.ts │ │ │ │ ├── Modal.svelte │ │ │ │ ├── ModalContent.svelte │ │ │ │ └── Modals.svelte │ │ ├── web3 │ │ │ ├── Web3ConnectionUI.svelte │ │ │ ├── AccountSignIn.svelte │ │ │ ├── Web3Connection.svelte │ │ │ ├── Web3AccountInfo.svelte │ │ │ ├── Web3PendingActions.svelte │ │ │ ├── Web3Executing.svelte │ │ │ ├── Web3WalletSelection.svelte │ │ │ ├── ConnectButton.svelte │ │ │ └── Web3ConnectionError.svelte │ │ ├── blockchain │ │ │ ├── networks.ts │ │ │ └── state │ │ │ │ ├── State.ts │ │ │ │ └── ViewState.ts │ │ ├── web │ │ │ ├── serviceWorker.ts │ │ │ └── notifications.ts │ │ ├── account │ │ │ ├── types.ts │ │ │ ├── account-data.ts │ │ │ └── base.ts │ │ ├── config.ts │ │ └── time.ts │ ├── routes │ │ ├── +layout.ts │ │ ├── _redirects │ │ │ └── +server.ts │ │ ├── debug │ │ │ ├── +page.svelte │ │ │ ├── transactions │ │ │ │ └── +page.svelte │ │ │ ├── contracts │ │ │ │ └── +page.svelte │ │ │ ├── +layout.svelte │ │ │ ├── indexer │ │ │ │ └── +page.svelte │ │ │ └── utilities │ │ │ │ └── +page.svelte │ │ ├── version │ │ │ └── +server.ts │ │ ├── 404.html │ │ │ └── +page.svelte │ │ ├── about │ │ │ └── +page.svelte │ │ ├── +page.svelte │ │ ├── +layout.svelte │ │ └── demo │ │ │ └── +page.svelte │ ├── app.d.ts │ ├── web-config.json │ ├── app.postcss │ ├── data │ │ └── networks.json │ ├── service-worker-handler.ts │ ├── app.html │ └── service-worker.ts ├── vite.config.ts ├── .prettierrc ├── postcss.config.cjs ├── .gitignore ├── tsconfig.json ├── .prettierignore ├── .env ├── tailwind.config.cjs ├── README.md ├── svelte.config.js └── package.json ├── .gitattributes ├── common ├── .gitignore ├── src │ ├── index.ts │ ├── types.ts │ ├── bn.ts │ └── Registry.ts ├── .prettierrc ├── .env ├── tsup.config.ts ├── tsconfig.json ├── .prettierignore └── package.json ├── docs ├── guide │ └── getting-started │ │ └── index.md ├── .gitignore ├── index.md ├── .vitepress │ └── config.mts └── contracts │ └── Registry.md ├── .env ├── contracts ├── remappings.txt ├── deployments │ └── sepolia │ │ └── .chain ├── test │ ├── utils │ │ └── viem-matchers.ts │ ├── foundry │ │ └── GreetingsRegistry.t.sol │ └── GreetingsRegistry.test.ts ├── vitest.config.ts ├── zellij.kdl ├── deploy │ ├── _context.ts │ └── 00_deploy_GreetingsRegistry.ts ├── foundry.toml ├── .prettierrc ├── .prettierignore ├── .gitignore ├── .env ├── scripts │ ├── readMessage.ts │ └── setMessage.ts ├── docs_templates │ └── {{contracts}}.hbs ├── hardhat.config.cjs ├── utils │ └── connection.ts ├── package.json ├── README.md └── src │ └── GreetingsRegistry.sol ├── pnpm-workspace.yaml ├── .gitignore ├── .prettierrc ├── indexer ├── .prettierrc ├── .env ├── .gitignore ├── tsconfig.json ├── .prettierignore ├── version.cjs ├── tsup.config.ts ├── package.json └── src │ └── index.ts ├── dev ├── docker-compose-ipfs.yml ├── zellij-attach.kdl ├── zellij-remote.kdl ├── zellij.kdl ├── docker-compose.yml ├── wezterm.lua └── docker-compose-geth.yml ├── .prettierignore ├── jolly-roger.code-workspace ├── README.md └── package.json /.name: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/static/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /common/.gitignore: -------------------------------------------------------------------------------- 1 | .env*.local 2 | node_modules 3 | /dist -------------------------------------------------------------------------------- /docs/guide/getting-started/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BLOCK_TIME=5 2 | ETH_NODE_URI_localhost=http://localhost:8545 -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .vitepress/* 2 | !.vitepress/theme 3 | !.vitepress/config.mts 4 | node_modules -------------------------------------------------------------------------------- /contracts/remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'common' 3 | - 'contracts' 4 | - 'indexer' 5 | - 'web' 6 | -------------------------------------------------------------------------------- /web/static/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wighawag/jolly-roger/HEAD/web/static/preview.png -------------------------------------------------------------------------------- /common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bn'; 2 | export * from './Registry'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # environment variables 2 | .env*.local 3 | 4 | # node modules 5 | node_modules/ 6 | 7 | .dev.vars -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /web/static/images/wallets/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wighawag/jolly-roger/HEAD/web/static/images/wallets/frame.png -------------------------------------------------------------------------------- /web/static/maskable_icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wighawag/jolly-roger/HEAD/web/static/maskable_icon_512x512.png -------------------------------------------------------------------------------- /common/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /indexer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /contracts/deployments/sepolia/.chain: -------------------------------------------------------------------------------- 1 | {"chainId":"11155111","genesisHash":"0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9"} -------------------------------------------------------------------------------- /indexer/.env: -------------------------------------------------------------------------------- 1 | # this let ldenv know to load the parent foler .env too (the child folder's vars takes precedence though) 2 | ENV_ROOT_FOLDER=.. -------------------------------------------------------------------------------- /web/src/lib/utils/data.ts: -------------------------------------------------------------------------------- 1 | import * as lz from 'lz-string'; 2 | 3 | export const {compressToUint8Array, decompressFromUint8Array} = lz; 4 | -------------------------------------------------------------------------------- /web/static/images/wallets/web3-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wighawag/jolly-roger/HEAD/web/static/images/wallets/web3-default.png -------------------------------------------------------------------------------- /common/.env: -------------------------------------------------------------------------------- 1 | # this let ldenv know to load the parent foler .env too (the child folder's vars takes precedence though) 2 | ENV_ROOT_FOLDER=.. 3 | -------------------------------------------------------------------------------- /common/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup'; 2 | export default defineConfig({ 3 | outDir: 'dist', 4 | sourcemap: true, 5 | }); 6 | -------------------------------------------------------------------------------- /indexer/.gitignore: -------------------------------------------------------------------------------- 1 | .env*.local 2 | node_modules 3 | 4 | # data generated 5 | /dist 6 | /ethereum-indexer-data 7 | /src/contracts.ts 8 | /indexed/ -------------------------------------------------------------------------------- /web/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import '../service-worker-handler'; 2 | 3 | export const prerender = true; 4 | export const trailingSlash = 'always'; 5 | -------------------------------------------------------------------------------- /contracts/test/utils/viem-matchers.ts: -------------------------------------------------------------------------------- 1 | import {chai} from 'vitest'; 2 | import {viemChaiMatchers} from 'viem-chai-matchers'; 3 | chai.use(viemChaiMatchers); 4 | -------------------------------------------------------------------------------- /common/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Greeting = {account: `0x${string}`; message: string; pending: boolean}; 2 | export type RegistryState = { 3 | greetings: Greeting[]; 4 | }; 5 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {sveltekit} from '@sveltejs/kit/vite'; 2 | import {defineConfig} from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | }); 7 | -------------------------------------------------------------------------------- /web/src/lib/utils/time.ts: -------------------------------------------------------------------------------- 1 | export function wait(numSeconds: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, numSeconds * 1000); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /dev/docker-compose-ipfs.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ipfs: 4 | image: ipfs/go-ipfs:v0.4.23 5 | ports: 6 | - '6651:5001' 7 | - '6680:8080' 8 | environment: 9 | IPFS_PATH: /tmp 10 | -------------------------------------------------------------------------------- /common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2020", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "strict": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /indexer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2020", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "strict": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/src/lib/components/web/NoInstallPrompt.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /common/.prettierignore: -------------------------------------------------------------------------------- 1 | # ########################################### 2 | # .gitignore 3 | ############################################# 4 | .env*.local 5 | node_modules 6 | /dist 7 | ############################################# 8 | package.json 9 | pnpm-lock.yaml -------------------------------------------------------------------------------- /contracts/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | provider: 'custom', 7 | customProviderModule: 'vitest-solidity-coverage', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /web/src/routes/_redirects/+server.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | export const GET = async () => { 3 | // see: https://docs.ipfs.tech/how-to/websites-on-ipfs/redirects-and-custom-404s/#examples 4 | return new Response(`/* /404.html/index.html 404`); 5 | }; 6 | -------------------------------------------------------------------------------- /web/src/routes/debug/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "bracketSpacing": false, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [ 8 | {"files": "*.svelte", "options": {"parser": "svelte"}}, 9 | {"files": "*.ts", "options": {"parser": "typescript"}} 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /web/src/routes/version/+server.ts: -------------------------------------------------------------------------------- 1 | import {version} from '$app/environment'; 2 | export const prerender = true; 3 | // This allow to easily verify that the IPFS folder served is of the correct version 4 | // ti will create a file with the version in it 5 | export const GET = async () => { 6 | return new Response(version); 7 | }; 8 | -------------------------------------------------------------------------------- /contracts/zellij.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane { 3 | pane name="shell" command="bash" { 4 | args "-c" "${SHELL-bash}" 5 | } 6 | pane split_direction="vertical" { 7 | pane name="live test" command="pnpm" { 8 | args "test" 9 | } 10 | pane name="local node" command="pnpm" { 11 | args "local_node" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /indexer/.prettierignore: -------------------------------------------------------------------------------- 1 | # ########################################### 2 | # .gitignore 3 | ############################################# 4 | .env*.local 5 | node_modules 6 | 7 | # data generated 8 | /dist 9 | /ethereum-indexer-data 10 | /src/contracts.ts 11 | /indexed/ 12 | 13 | ############################################# 14 | package.json 15 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # ########################################### 2 | # .gitignore 3 | ############################################# 4 | # environment variables 5 | .env*.local 6 | 7 | # node modules 8 | node_modules/ 9 | ############################################# 10 | 11 | # Ignore npm files 12 | pnpm-lock.yaml 13 | package.json 14 | 15 | # ignore folders 16 | **/ 17 | -------------------------------------------------------------------------------- /contracts/deploy/_context.ts: -------------------------------------------------------------------------------- 1 | /// Typed Context 2 | /// This file is used by deploy script to get access 3 | /// to typed artifacts as well as account names 4 | 5 | import artifacts from '../generated/artifacts'; 6 | import 'rocketh-signer'; 7 | 8 | export const context = { 9 | accounts: { 10 | deployer: { 11 | default: 0, 12 | }, 13 | }, 14 | artifacts, 15 | } as const; 16 | -------------------------------------------------------------------------------- /web/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer, 10 | ], 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /web/src/lib/web3/Web3ConnectionUI.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "forge-artifacts" 4 | libs = ["node_modules", "lib"] 5 | remappings = [ 6 | "hardhat/=node_modules/hardhat/", 7 | "solidity-proxy/=node_modules/solidity-proxy/" 8 | ] 9 | test = 'test/foundry' 10 | cache_path = 'forge-cache' 11 | 12 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 13 | -------------------------------------------------------------------------------- /web/src/routes/debug/transactions/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | {#each transactions as transaction} 10 |
  • {transaction.hash}: {transaction.transaction.inclusion}
  • 11 | {/each} 12 |
13 | -------------------------------------------------------------------------------- /web/src/lib/components/alert/InlineInfo.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /contracts/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "bracketSpacing": false, 6 | 7 | "plugins": ["prettier-plugin-solidity"], 8 | "overrides": [ 9 | { 10 | "files": "*.sol", 11 | "options": { 12 | "parser": "solidity-parse", 13 | "printWidth": 120, 14 | "tabWidth": 4, 15 | "useTabs": false, 16 | "singleQuote": false, 17 | "bracketSpacing": false 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /dev/zellij-attach.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane { 3 | pane {} 4 | pane split_direction="vertical" { 5 | pane name="INDEXER" command="bash" { 6 | args "-c" "pnpm indexer:dev; cd indexer; ${SHELL-bash}" 7 | } 8 | pane name="COMMON" command="bash" { 9 | args "-c" "pnpm common:dev; cd common; ${SHELL-bash}" 10 | } 11 | pane name="WEB" command="bash" { 12 | args "-c" "pnpm web:dev; cd web; ${SHELL-bash}" 13 | } 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /web/src/routes/debug/contracts/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if contractList} 10 |
    11 | {#each contractList as contract} 12 |
  • {contract.name}: {contract.contract.address}
  • 13 | {/each} 14 |
15 | {/if} 16 | -------------------------------------------------------------------------------- /contracts/.prettierignore: -------------------------------------------------------------------------------- 1 | ############################################ 2 | # .gitignore 3 | ############################################# 4 | node_modules 5 | .vscode/* 6 | !*.default 7 | *.local 8 | 9 | # rocketh 10 | generated 11 | deployments/localhost 12 | 13 | # hardhat 14 | /coverage 15 | /coverage.json 16 | /coverage-data.json 17 | cache 18 | artifacts 19 | 20 | docs 21 | *.hbs 22 | 23 | ############################################# 24 | deployments 25 | package.json 26 | pnpm-lock.yaml 27 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # environment variables 2 | .env*.local 3 | 4 | # node modules 5 | node_modules 6 | 7 | # vscode 8 | .vscode/* 9 | !.vscode/settings.json.default 10 | !.vscode/launch.json.default 11 | !.vscode/extensions.json.default 12 | 13 | # data generated 14 | /build 15 | /.svelte-kit 16 | /package 17 | vite.config.js.timestamp-* 18 | vite.config.ts.timestamp-* 19 | 20 | # pwag 21 | static/pwa 22 | 23 | # generated from contracts 24 | src/data/contracts.ts 25 | static/indexed-state-*.json 26 | -------------------------------------------------------------------------------- /web/static/images/wallets/status.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /common/src/bn.ts: -------------------------------------------------------------------------------- 1 | // TODO share with ethereum-indexer db-utils etc... 2 | export function bnReplacer(k: string, v: any): any { 3 | if (typeof v === 'bigint') { 4 | return v.toString() + 'n'; 5 | } 6 | return v; 7 | } 8 | 9 | export function bnReviver(k: string, v: any): any { 10 | if ( 11 | typeof v === 'string' && 12 | (v.startsWith('-') ? !isNaN(parseInt(v.charAt(1))) : !isNaN(parseInt(v.charAt(0)))) && 13 | v.charAt(v.length - 1) === 'n' 14 | ) { 15 | return BigInt(v.slice(0, -1)); 16 | } 17 | return v; 18 | } 19 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode/* 3 | !*.default 4 | *.local 5 | 6 | # rocketh 7 | generated 8 | deployments/localhost 9 | deployments/lan 10 | _metadata 11 | 12 | # hardhat 13 | /coverage 14 | /coverage.json 15 | /coverage-data.json 16 | cache 17 | artifacts 18 | docs 19 | 20 | # foundry 21 | forge-cache/ 22 | forge-artifacts/ 23 | 24 | # Note: by default we ignore lib/forge-std as we do not use submodules 25 | # install it via `git clone --recursive https://github.com/foundry-rs/forge-std.git lib/forge-std` 26 | lib/forge-std 27 | 28 | -------------------------------------------------------------------------------- /contracts/deploy/00_deploy_GreetingsRegistry.ts: -------------------------------------------------------------------------------- 1 | import {execute} from 'rocketh'; 2 | import 'rocketh-deploy-proxy'; 3 | import {context} from './_context'; 4 | 5 | export default execute( 6 | context, 7 | async ({deployViaProxy, accounts, artifacts}) => { 8 | await deployViaProxy( 9 | 'Registry', 10 | { 11 | account: accounts.deployer, 12 | artifact: artifacts.GreetingsRegistry, 13 | args: [''], 14 | }, 15 | { 16 | owner: accounts.deployer, 17 | }, 18 | ); 19 | }, 20 | {tags: ['Registry', 'Registry_deploy']}, 21 | ); 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Jolly-Roger" 7 | text: "Build and Deploy for Eternity." 8 | tagline: "Jolly Roger is a production-ready template for decentralised applications." 9 | image: 10 | src: /icon.svg 11 | width: 512 12 | height: 512 13 | alt: Logo 14 | actions: 15 | - theme: brand 16 | text: Guide 17 | link: /guide/getting-started/ 18 | - theme: alt 19 | text: Contracts 20 | link: /contracts/Registry/ 21 | --- 22 | -------------------------------------------------------------------------------- /web/src/web-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jolly Roger", 3 | "title": "Jolly Roger - Template for Decentralised Applications.", 4 | "description": "Build and Deploy for Eternity. Jolly Roger is a production-ready template for decentralised applications.", 5 | "canonicalURL": "https://jolly-roger.eth.limo", 6 | "ENSName": "jolly-roger.eth", 7 | "themeColor": "#000000", 8 | "appleStatusBarStyle": "black-translucent", 9 | "icon": "static/icon.svg", 10 | "maskableIcons": [ 11 | { 12 | "src": "../maskable_icon_512x512.png", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /contracts/test/foundry/GreetingsRegistry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {GreetingsRegistry} from "../../src/GreetingsRegistry.sol"; 6 | 7 | contract GreetingsRegistryTest is Test { 8 | GreetingsRegistry public registry; 9 | 10 | function setUp() public { 11 | registry = new GreetingsRegistry(""); 12 | } 13 | 14 | function test_setMessage() public { 15 | registry.setMessage("hello", 0); 16 | assertEq(registry.lastGreetingOf(address(this)), "hello"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/lib/components/navigation/PageLink.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /web/src/lib/blockchain/networks.ts: -------------------------------------------------------------------------------- 1 | import networks from '$data/networks.json'; 2 | 3 | export type NetworkWalletData = { 4 | rpcUrls?: string[]; 5 | blockExplorerUrls?: string[]; 6 | chainName?: string; 7 | iconUrls?: string[]; 8 | nativeCurrency?: { 9 | name: string; 10 | symbol: string; 11 | decimals: number; 12 | }; 13 | }; 14 | export type NetworkData = { 15 | config: NetworkWalletData; 16 | finality: number; 17 | averageBblockTime: number; 18 | }; 19 | 20 | export function getNetworkConfig(chainId: string) { 21 | const network = (networks as any)[chainId] as NetworkData | undefined; 22 | return network?.config; 23 | } 24 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "outDir": "build" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /web/src/lib/components/ethereum/ImgBlockie.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {address} 14 | 15 | 23 | -------------------------------------------------------------------------------- /web/src/lib/components/daisyui/NavTabs.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#each pages as page} 15 | {page.title} 16 | {/each} 17 |
18 | -------------------------------------------------------------------------------- /web/src/routes/404.html/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 11 | 12 |
13 |
14 |

15 | Could not find the page {$page.params.notfound || ''} 16 |

17 |
18 |
19 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jolly-roger-common", 3 | "version": "0.0.0", 4 | "description": "Common lib", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "dependencies": { 10 | "named-logs": "^0.2.2" 11 | }, 12 | "devDependencies": { 13 | "ldenv": "^0.3.9", 14 | "prettier": "^3.2.4", 15 | "tsup": "^8.0.1", 16 | "typescript": "^5.3.3" 17 | }, 18 | "scripts": { 19 | "build": "tsup src/index.ts --dts --format esm,cjs", 20 | "dev": "tsup src/index.ts --dts --format esm,cjs --watch", 21 | "format:check": "prettier --check .", 22 | "format": "prettier --write ." 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/.prettierignore: -------------------------------------------------------------------------------- 1 | # ########################################### 2 | # .gitignore 3 | ############################################# 4 | # environment variables 5 | .env*.local 6 | 7 | # node modules 8 | node_modules 9 | 10 | # vscode 11 | .vscode/* 12 | !.vscode/settings.json.default 13 | !.vscode/launch.json.default 14 | !.vscode/extensions.json.default 15 | 16 | # data generated 17 | /build 18 | /.svelte-kit 19 | /package 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | 23 | # pwag 24 | static/pwa 25 | static/robots.txt 26 | 27 | # generated from contracts 28 | src/data/contracts.ts 29 | static/indexed-state-*.json 30 | 31 | ############################################# 32 | package.json 33 | pnpm-lock.yaml 34 | static/ -------------------------------------------------------------------------------- /web/src/lib/components/utilities/Executor.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#if $execution.error} 15 | {$execution.error} 16 | 17 | {:else} 18 | 22 | {/if} 23 |
24 | -------------------------------------------------------------------------------- /dev/zellij-remote.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane { 3 | pane {} 4 | pane split_direction="vertical" { 5 | pane name="COMPILE" command="bash" { 6 | args "-c" "pnpm contracts:compile:watch; cd contracts; ${SHELL-bash}" 7 | } 8 | pane name="DEPLOY" command="bash" { 9 | args "-c" "pnpm contracts:deploy:watch ${MODE}; cd contracts; ${SHELL-bash}" 10 | } 11 | } 12 | pane split_direction="vertical" { 13 | pane name="INDEXER" command="bash" { 14 | args "-c" "pnpm indexer:dev; cd indexer; ${SHELL-bash}" 15 | } 16 | pane name="COMMON" command="bash" { 17 | args "-c" "pnpm common:dev; cd common; ${SHELL-bash}" 18 | } 19 | pane name="WEB" command="bash" { 20 | args "-c" "pnpm web:dev; cd web; ${SHELL-bash}" 21 | } 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /web/.env: -------------------------------------------------------------------------------- 1 | # this let ldenv know to load the parent foler .env too (the child folder's vars takes precedence though) 2 | ENV_ROOT_FOLDER=.. 3 | 4 | # only used for development, this does not replace PUBLIC_ETH_NODE_URI_LOCALHOST 5 | # Use it only for dev-related queries 6 | PUBLIC_DEV_NODE_URI=http://localhost:8545 7 | 8 | # This set a fallback node (centralised node) to let the app get read access to the blockchain 9 | # Disabled by default: User need a wallet to read state 10 | PUBLIC_ETH_NODE_URI= 11 | PUBLIC_ETH_NODE_URI_LOCALHOST= 12 | # for localhost, you can also set it to be the same as PUBLIC_DEV_NODE_URI 13 | # PUBLIC_ETH_NODE_URI_LOCALHOST=${PUBLIC_DEV_NODE_URI} 14 | 15 | # Can specify a block time for localhost 16 | PUBLIC_LOCALHOST_BLOCK_TIME=${BLOCK_TIME} 17 | 18 | PUBLIC_SYNC_URI= -------------------------------------------------------------------------------- /web/src/lib/components/modals/types.ts: -------------------------------------------------------------------------------- 1 | export type ModalCancelationMode = 'clickOutside' | 'esc'; 2 | export type ModalSettings = 3 | | { 4 | element: HTMLElement; 5 | response?: (r: any, mode?: ModalCancelationMode) => boolean; 6 | } 7 | | { 8 | content: ModalContentSettings; 9 | response?: (r: any, mode?: ModalCancelationMode) => boolean; 10 | }; 11 | 12 | export type ModalContentSettings = 13 | | { 14 | type: 'info'; 15 | title?: string; 16 | message: string; 17 | } 18 | | { 19 | type: 'confirm'; 20 | title?: string; 21 | message: string; 22 | }; 23 | 24 | export type ModalCancellationOptions = {button: boolean; clickOutside?: boolean} | {cancelable: false}; 25 | 26 | export type ModalResponseCallback = (response: boolean) => boolean | undefined | void; 27 | -------------------------------------------------------------------------------- /indexer/version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const crypto = require('crypto'); 3 | 4 | const args = process.argv.slice(2); 5 | const filepath = args[0]; 6 | 7 | function transform(filepath, versionHash) { 8 | try { 9 | const content = fs.readFileSync(filepath, 'utf-8'); 10 | const hash = versionHash || crypto.createHash('sha256').update(content, 'utf8').digest('hex'); 11 | const newContent = content.replace('__VERSION_HASH__', hash); 12 | fs.writeFileSync(filepath, newContent); 13 | return hash; 14 | } catch (e) { 15 | console.error(e.message); 16 | } 17 | } 18 | 19 | if (fs.statSync(filepath).isDirectory) { 20 | const hash = transform(`${filepath}/index.js`); 21 | transform(`${filepath}/index.cjs`, hash); 22 | transform(`${filepath}/index.d.ts`, hash); 23 | } else { 24 | transform(filepath); 25 | } 26 | -------------------------------------------------------------------------------- /web/src/lib/web/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | import {get, writable} from 'svelte/store'; 2 | 3 | export type ServiceWorkerState = { 4 | registration?: ServiceWorkerRegistration; 5 | updateAvailable: boolean; 6 | }; 7 | 8 | const store = writable({ 9 | registration: undefined, 10 | updateAvailable: false, 11 | }); 12 | export const serviceWorker = { 13 | ...store, 14 | get registration(): ServiceWorkerRegistration | undefined { 15 | return get(store).registration; 16 | }, 17 | get updateAvailable(): boolean { 18 | return get(store).updateAvailable; 19 | }, 20 | }; 21 | 22 | // allow to test service worker notifcation by executing the following in the console: 23 | // serviceWorker.update(v => {v.updateAvailable = true; v.registration = "anything"; return v}); 24 | (globalThis as any).serviceWorker = store; 25 | -------------------------------------------------------------------------------- /dev/zellij.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane { 3 | pane {} 4 | pane split_direction="vertical" { 5 | pane name="NODE" command="bash" { 6 | args "-c" "pnpm local_node; ${SHELL-bash}" 7 | } 8 | pane name="COMPILE" command="bash" { 9 | args "-c" "pnpm contracts:compile:watch; cd contracts; ${SHELL-bash}" 10 | } 11 | pane name="DEPLOY" command="bash" { 12 | args "-c" "pnpm contracts:deploy:watch; cd contracts; ${SHELL-bash}" 13 | } 14 | } 15 | pane split_direction="vertical" { 16 | pane name="INDEXER" command="bash" { 17 | args "-c" "pnpm indexer:dev; cd indexer; ${SHELL-bash}" 18 | } 19 | pane name="COMMON" command="bash" { 20 | args "-c" "pnpm common:dev; cd common; ${SHELL-bash}" 21 | } 22 | pane name="WEB" command="bash" { 23 | args "-c" "pnpm web:dev; cd web; ${SHELL-bash}" 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /web/src/routes/debug/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 19 | 20 |
21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /contracts/.env: -------------------------------------------------------------------------------- 1 | # this let ldenv know to load the parent foler .env too (the child folder's vars takes precedence though) 2 | ENV_ROOT_FOLDER=.. 3 | 4 | # this let ldenv know to load the parent foler .env too (the child folder's vars takes precedence though) 5 | ENV_ROOT_FOLDER=.. 6 | 7 | # if you want to deploy on a specific network and not expose your api key 8 | # you just create a .env.local (or .env..local) with the following 9 | # ETH_NODE_URI_= 10 | # MNEMONIC_=">" 11 | # you can also add the etherscan api key for verification 12 | # ETHERSCAN_API_KEY= 13 | 14 | ETH_NODE_URI_localhost="http://127.0.0.1:8545" 15 | MNEMONIC_localhost="test test test test test test test test test test test junk" 16 | 17 | ETH_NODE_URI_lan="http://ethereum.local:8545" 18 | MNEMONIC_lan="test test test test test test test test test test test junk" 19 | -------------------------------------------------------------------------------- /contracts/scripts/readMessage.ts: -------------------------------------------------------------------------------- 1 | import {Deployment, loadEnvironment} from 'rocketh'; 2 | import {context} from '../deploy/_context'; 3 | import hre from 'hardhat'; 4 | import {EIP1193ProviderWithoutEvents} from 'eip-1193'; 5 | import {fetchContract} from '../utils/connection'; 6 | 7 | async function main() { 8 | const env = await loadEnvironment( 9 | { 10 | provider: hre.network.provider as EIP1193ProviderWithoutEvents, 11 | networkName: hre.network.name, 12 | }, 13 | context, 14 | ); 15 | 16 | const args = process.argv.slice(2); 17 | const account = (args[0] || process.env.ACCOUNT) as `0x${string}`; 18 | const Registry = env.deployments.Registry as Deployment; 19 | const RegistryContract = await fetchContract(Registry); 20 | const message = await RegistryContract.read.messages([account]); 21 | 22 | console.log({account, message}); 23 | } 24 | main(); 25 | -------------------------------------------------------------------------------- /indexer/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup'; 2 | // @ts-ignore 3 | import * as fs from 'fs'; 4 | // @ts-ignore 5 | import * as crypto from 'crypto'; 6 | 7 | const outDir = 'dist'; 8 | function transform(filepath: string, versionHash?: string): string { 9 | const content = fs.readFileSync(filepath, 'utf-8'); 10 | const hash = versionHash || crypto.createHash('sha256').update(content, 'utf8').digest('hex'); 11 | const newContent = content.replace('__VERSION_HASH__', hash); 12 | fs.writeFileSync(filepath, newContent); 13 | return hash; 14 | } 15 | 16 | export default defineConfig({ 17 | outDir, 18 | async onSuccess() { 19 | const hash = transform(`${outDir}/index.js`); 20 | const cjs = `${outDir}/index.cjs`; 21 | const dts = `${outDir}/index.d.ts`; 22 | if (fs.existsSync(cjs)) { 23 | transform(cjs, hash); 24 | } 25 | if (fs.existsSync(dts)) { 26 | transform(dts, hash); 27 | } 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /common/src/Registry.ts: -------------------------------------------------------------------------------- 1 | import {RegistryState} from './types'; 2 | 3 | export class Registry { 4 | protected state!: RegistryState; 5 | public initialState: RegistryState; 6 | protected pending: boolean = false; 7 | 8 | constructor() { 9 | this.initialState = {greetings: []}; 10 | } 11 | 12 | handle(state: RegistryState, pending: boolean = false) { 13 | this.pending = pending; 14 | this.state = state; 15 | } 16 | 17 | setMessageFor(account: `0x${string}`, message: string, dayTimeInSeconds: number) { 18 | const findIndex = this.state.greetings.findIndex((v) => v.account === account); 19 | if (findIndex === -1) { 20 | try { 21 | this.state.greetings.push({ 22 | account, 23 | message, 24 | pending: this.pending, 25 | }); 26 | } catch (err) { 27 | console.error(err); 28 | } 29 | } else { 30 | this.state.greetings[findIndex].message = message; 31 | this.state.greetings[findIndex].pending = this.pending; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /web/src/lib/components/modals/stores.ts: -------------------------------------------------------------------------------- 1 | // Modal Store Queue 2 | 3 | import {writable} from 'svelte/store'; 4 | import type {ModalSettings} from './types'; 5 | 6 | function modalService() { 7 | const {subscribe, set, update} = writable([]); 8 | return { 9 | subscribe, 10 | set, 11 | update, 12 | /** Append to end of queue. */ 13 | trigger: (modal: ModalSettings) => { 14 | update((mStore) => { 15 | mStore.unshift(modal); 16 | return mStore; 17 | }); 18 | }, 19 | 20 | /** Remove first item in queue. */ 21 | close: () => { 22 | update((mStore) => { 23 | if (mStore.length > 0) mStore.shift(); 24 | return mStore; 25 | }); 26 | }, 27 | /** Remove all items from queue. */ 28 | clear: () => set([]), 29 | }; 30 | } 31 | 32 | export const modalStore = modalService(); 33 | 34 | // TODO remove, or better add option to auto inject stores 35 | if (typeof window !== 'undefined') { 36 | (window as any).modalStore = modalStore; 37 | } 38 | -------------------------------------------------------------------------------- /web/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const daisyui = require('daisyui'); 2 | const typography = require('@tailwindcss/typography'); 3 | const forms = require('@tailwindcss/forms'); 4 | 5 | const config = { 6 | content: ['./src/**/*.{html,js,svelte,ts}'], 7 | 8 | theme: { 9 | extend: {}, 10 | }, 11 | 12 | plugins: [forms, typography, daisyui], 13 | daisyui: { 14 | logs: false, 15 | darkTheme: 'dracula', 16 | themes: [ 17 | 'light', 18 | 'dark', 19 | 'cupcake', 20 | 'bumblebee', 21 | 'emerald', 22 | 'corporate', 23 | 'synthwave', 24 | 'retro', 25 | 'cyberpunk', 26 | 'valentine', 27 | 'halloween', 28 | 'garden', 29 | 'forest', 30 | 'aqua', 31 | 'lofi', 32 | 'pastel', 33 | 'fantasy', 34 | 'wireframe', 35 | 'black', 36 | 'luxury', 37 | 'dracula', 38 | 'cmyk', 39 | 'autumn', 40 | 'business', 41 | 'acid', 42 | 'lemonade', 43 | 'night', 44 | 'coffee', 45 | 'winter', 46 | ], 47 | }, 48 | }; 49 | 50 | module.exports = config; 51 | -------------------------------------------------------------------------------- /jolly-roger.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "common", 5 | "path": "./common" 6 | }, 7 | { 8 | "name": "contracts", 9 | "path": "./contracts" 10 | }, 11 | { 12 | "name": "indexer", 13 | "path": "./indexer" 14 | }, 15 | { 16 | "name": "web", 17 | "path": "./web" 18 | }, 19 | { 20 | "name": "docs", 21 | "path": "./docs" 22 | }, 23 | { 24 | "name": "ROOT", 25 | "path": "./" 26 | } 27 | ], 28 | "settings": { 29 | "editor.formatOnSave": true, 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | "[svelte]": { 32 | "editor.defaultFormatter": "svelte.svelte-vscode" 33 | }, 34 | "[solidity]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "prettier.documentSelectors": ["**/*.sol", "**/*.ts", "**/*.svelte"], 38 | "solidity.formatter": "prettier", 39 | "npm.exclude": "**/lib/**" 40 | }, 41 | "extensions": { 42 | "recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode", "NomicFoundation.hardhat-solidity"] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Jolly Roger Frontend 2 | 3 | The frontend is build on svelte-kit 4 | 5 | It makes use of STORES (object with a suscribe method) whenever possible. 6 | 7 | This let it be easily integrated in svelte files (`$` syntax) but also make it ultimately agnositc to teh frontend framework you use if any. 8 | 9 | For onchain app and games there is a common pattern: 10 | 11 | - get data from indexer 12 | - apply local data 13 | - apply pending tx 14 | 15 | Furthermore, it is often the case that some local data need to be kept hidden 16 | 17 | ## Architecture 18 | 19 | Jolly-Roger supports all of this and is architect around this same patterns 20 | 21 | The blockchain folder handle the indexed State. it make of [ethereum-indexer] to have the indexing run in the browser. 22 | 23 | This is handled in [src/lib/blockchain/state/State.ts] by providing few STORES. 24 | 25 | One simply called `state` represent the onchain state already included in the blockchain. 26 | 27 | This State is then merge witj both local data and pending tx to create the ViewState 28 | -------------------------------------------------------------------------------- /web/src/lib/web3/AccountSignIn.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |

Welcome to Jolly-Roger

11 |

12 | In order to continue and get a safe place to save data, you'll need to sign a message. Be carefull and only sign 13 | this message on trusted frontend. 14 |

15 |
16 | 20 |
21 | 25 |
26 | -------------------------------------------------------------------------------- /contracts/scripts/setMessage.ts: -------------------------------------------------------------------------------- 1 | import {Deployment, loadEnvironment} from 'rocketh'; 2 | import {context} from '../deploy/_context'; 3 | import hre from 'hardhat'; 4 | import {EIP1193ProviderWithoutEvents} from 'eip-1193'; 5 | import {fetchContract, getConnection} from '../utils/connection'; 6 | 7 | async function main() { 8 | const env = await loadEnvironment( 9 | { 10 | provider: hre.network.provider as EIP1193ProviderWithoutEvents, 11 | networkName: hre.network.name, 12 | }, 13 | context, 14 | ); 15 | 16 | const {walletClient} = await getConnection(); 17 | 18 | const args = process.argv.slice(2); 19 | const message = (args[0] || process.env.MESSAGE) as `0x${string}`; 20 | 21 | const [address] = await walletClient.getAddresses(); 22 | const Registry = env.deployments.Registry as Deployment; 23 | const RegistryContract = await fetchContract(Registry); 24 | const hash = await RegistryContract.write.setMessage([message, 1], {account: address}); 25 | 26 | console.log({hash, account: address}); 27 | } 28 | main(); 29 | -------------------------------------------------------------------------------- /web/src/lib/web3/Web3Connection.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/src/lib/account/types.ts: -------------------------------------------------------------------------------- 1 | import type {EIP1193TransactionWithMetadata, PendingTransaction} from 'ethereum-tx-observer'; 2 | 3 | export type AccountInfo = { 4 | address: `0x${string}`; 5 | chainId: string; 6 | genesisHash: string; 7 | localKey?: `0x${string}`; 8 | doNotEncryptLocally?: boolean; 9 | }; 10 | 11 | export type SyncInfo = {uri: string; enabled?: boolean}; 12 | 13 | export type MergeFunction = ( 14 | localData?: T, 15 | remoteData?: T, 16 | ) => {newData: T; newDataOnLocal: boolean; newDataOnRemote: boolean}; 17 | 18 | export type OnPendingTransaction = ( 19 | event: 20 | | { 21 | name: 'newTx'; 22 | txs: PendingTransaction[]; 23 | } 24 | | { 25 | name: 'clear'; 26 | }, 27 | ) => void; 28 | 29 | export type AccountHandler = { 30 | updateTx(pendingTransaction: PendingTransaction): void; 31 | on(f: OnPendingTransaction): void; 32 | off: (func: OnPendingTransaction) => void; 33 | onTxSent(tx: EIP1193TransactionWithMetadata, hash: `0x${string}`): void; 34 | load: (info: AccountInfo, syncInfo?: SyncInfo) => Promise; 35 | unload(): Promise; 36 | }; 37 | -------------------------------------------------------------------------------- /web/src/lib/components/alert/AlertWithSlot.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 29 | -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: postgres:9.6.17 5 | # ports: 6 | # - "5432:5432" 7 | command: ['postgres', '-cshared_preload_libraries=pg_stat_statements', '-cmax_connections=200'] 8 | healthcheck: 9 | test: ['CMD-SHELL', 'pg_isready -d explorer -U $${POSTGRES_USER}'] 10 | interval: 10s 11 | timeout: 5s 12 | retries: 5 13 | environment: 14 | - POSTGRES_USER=dbowner 15 | - POSTGRES_PASSWORD=let-me-in 16 | - PGDATA=/tmp 17 | 18 | blockscout: 19 | image: wighawag/blockscout:v0.0.1 20 | depends_on: 21 | postgres: 22 | condition: service_healthy 23 | ports: 24 | - '4000:4000' 25 | extra_hosts: 26 | - host.docker.internal:host-gateway 27 | environment: 28 | ETHEREUM_JSONRPC_HTTP_URL: http://host.docker.internal:8545 29 | ETHEREUM_JSONRPC_TRACE_URL: http://host.docker.internal:8545 30 | #ETHEREUM_JSONRPC_WS_URL: ws://host.docker.internal:8545 31 | ETHEREUM_JSONRPC_VARIANT: ganache 32 | DATABASE_URL: postgresql://dbowner:let-me-in@postgres:5432/explorer?ssl=false 33 | entrypoint: '/bin/sh -c "mix do ecto.create, ecto.migrate; mix phx.server"' 34 | -------------------------------------------------------------------------------- /web/src/lib/utils/path.ts: -------------------------------------------------------------------------------- 1 | import {base} from '$app/paths'; 2 | import {params, globalQueryParams} from '$lib/config'; 3 | import {getParamsFromURL, queryStringifyNoArray} from './url'; 4 | 5 | export function route(p: string, hash?: string) { 6 | if (!p.endsWith('/')) { 7 | p += '/'; 8 | } 9 | let path = `${base}${p}${getQueryStringToKeep(p)}${hash ? `#${hash}` : ''}`; 10 | return path; 11 | } 12 | 13 | function getQueryStringToKeep(p: string): string { 14 | if (globalQueryParams && globalQueryParams.length > 0) { 15 | const {params: paramFromPath} = getParamsFromURL(p); 16 | for (const queryParam of globalQueryParams) { 17 | if (typeof params[queryParam] != 'undefined' && typeof paramFromPath[queryParam] === 'undefined') { 18 | paramFromPath[queryParam] = params[queryParam]; 19 | } 20 | } 21 | return queryStringifyNoArray(paramFromPath); 22 | } else { 23 | return ''; 24 | } 25 | } 26 | 27 | export function url(p: string, hash?: string) { 28 | return `${base}${p}`; 29 | } 30 | 31 | export function isSameRoute(a: string, b: string): boolean { 32 | return a === b || a === route(b); 33 | } 34 | 35 | export function isParentRoute(a: string, b: string): boolean { 36 | return a.startsWith(b) || a.startsWith(route(b)); 37 | } 38 | -------------------------------------------------------------------------------- /web/src/lib/utils/localCache.ts: -------------------------------------------------------------------------------- 1 | import {base} from '$app/paths'; 2 | 3 | class LocalCache { 4 | private _prefix: string; 5 | constructor(version?: string) { 6 | this._prefix = base.startsWith('/ipfs/') || base.startsWith('/ipns/') ? base.slice(6) : ''; // ensure local storage is not conflicting across web3w-based apps on ipfs gateways (require encryption for sensitive data) 7 | 8 | if (version) { 9 | const lastVersion = this.getItem('_version'); 10 | if (lastVersion !== version) { 11 | this.clear(); 12 | if (version) { 13 | this.setItem('_version', version); 14 | } 15 | } 16 | } 17 | } 18 | setItem(key: string, value: string): void { 19 | try { 20 | localStorage.setItem(this._prefix + key, value); 21 | } catch (e) { 22 | // 23 | } 24 | } 25 | 26 | getItem(key: string): string | null { 27 | try { 28 | return localStorage.getItem(this._prefix + key); 29 | } catch (e) { 30 | return null; 31 | } 32 | } 33 | 34 | removeItem(key: string) { 35 | try { 36 | localStorage.removeItem(this._prefix + key); 37 | } catch (e) { 38 | // 39 | } 40 | } 41 | 42 | clear(): void { 43 | try { 44 | localStorage.clear(); 45 | } catch (e) { 46 | // 47 | } 48 | } 49 | } 50 | 51 | // can force version change 52 | export default new LocalCache(); 53 | -------------------------------------------------------------------------------- /web/src/app.postcss: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in PostCSS syntax */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | .bg-reverse-primary { 7 | background-color: hsl(var(--pc)); 8 | } 9 | 10 | .text-base-100 { 11 | --tw-text-opacity: 0.55; 12 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 13 | } 14 | .text-base-200 { 15 | --tw-text-opacity: 0.6; 16 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 17 | } 18 | .text-base-300 { 19 | --tw-text-opacity: 0.65; 20 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 21 | } 22 | .text-base-400 { 23 | --tw-text-opacity: 0.7; 24 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 25 | } 26 | .text-base-500 { 27 | --tw-text-opacity: 0.75; 28 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 29 | } 30 | .text-base-600 { 31 | --tw-text-opacity: 0.8; 32 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 33 | } 34 | .text-base-700 { 35 | --tw-text-opacity: 0.85; 36 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 37 | } 38 | .text-base-800 { 39 | --tw-text-opacity: 0.9; 40 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 41 | } 42 | .text-base-900 { 43 | --tw-text-opacity: 0.95; 44 | color: hsla(var(--bc) / var(--tw-text-opacity, 1)); 45 | } 46 | 47 | .text-title { 48 | font-size: 3.75rem !important; 49 | } 50 | -------------------------------------------------------------------------------- /web/src/lib/components/utilities/CopyBlock.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 |
37 | {#if copyState} 38 | Copied ✓ 39 | {:else} 40 | {text} 41 | {/if} 42 |
43 | -------------------------------------------------------------------------------- /web/src/lib/components/alert/Alert.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | -------------------------------------------------------------------------------- /indexer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jolly-roger-indexer", 3 | "version": "0.1.0", 4 | "description": "jolly-roger indexer function to generate the state of all the registry messages", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "dependencies": { 10 | "ethereum-indexer-js-processor": "^0.6.27", 11 | "jolly-roger-common": "workspace:*", 12 | "named-logs": "^0.2.2" 13 | }, 14 | "devDependencies": { 15 | "ethereum-indexer-cli": "^0.6.24", 16 | "ethereum-indexer-server": "^0.6.27", 17 | "ldenv": "^0.3.9", 18 | "prettier": "^3.2.4", 19 | "tsup": "^8.0.1", 20 | "typescript": "^5.3.3", 21 | "wait-on": "^7.2.0" 22 | }, 23 | "scripts": { 24 | "eis": "eis", 25 | "serve": "ldenv eis run -n @@ETH_NODE_URI_:MODE,ETH_NODE_URI -p ./dist/index.cjs --disable-cache --deployments ../contracts/deployments/@@MODE @@", 26 | "index": "ldenv ei -n @@ETH_NODE_URI_:MODE,ETH_NODE_URI -p ./dist/index.cjs -d src/contracts.ts -f indexed/@@MODE@:.json @@", 27 | "index-to-file": "ei -p ./dist/index.cjs", 28 | "build": "tsup src/index.ts --dts --format esm,cjs", 29 | "dev": "echo 'waiting for src/contracts.ts...'; wait-on src/contracts.ts && tsup src/index.ts --dts --format esm,cjs --watch", 30 | "format:check": "prettier --check .", 31 | "format": "prettier --write ." 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/src/lib/web3/Web3AccountInfo.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if $account.unlocking} 9 | { 11 | account.cancelUnlock(); 12 | return true; 13 | }} 14 | settings={{type: 'info', message: 'Please unlock'}} 15 | cancelation={{clickOutside: false, button: true}} 16 | /> 17 | {/if} 18 | 19 | {#if $account.loadingStep} 20 | {#if $account.loadingStep.id == 'SIGNING'} 21 | 22 |

Welcome to Jolly-Roger

23 |

Sign the message to access to your data.

24 | 27 |
28 | {:else if $account.loadingStep.id == 'WELCOME'} 29 | 30 | {:else} 31 | 32 |

{$account.loadingStep.id}

33 |

{$account.loadingStep.id}

34 | 38 |
39 | {/if} 40 | {/if} 41 | -------------------------------------------------------------------------------- /contracts/docs_templates/{{contracts}}.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | # {{{name}}} 5 | 6 | 7 | {{#if notice}} 8 | ### **Description** 9 | 10 | {{{notice}}} 11 | {{/if}} 12 | 13 | {{#if methods.length}} 14 | ## Functions 15 | 16 | {{#each methods}} 17 | ### **{{{name}}}** 18 | 19 | {{{notice}}} 20 | 21 | *sig hash*: `{{{bytes4}}}` 22 | 23 | *Signature*: {{{signature}}} 24 | 25 | {{{fullFormat}}} 26 | 27 | {{#if params.length}} 28 | | Name | Description 29 | | ---- | ----------- 30 | {{#each params}} 31 | {{#if description}} 32 | | {{{name}}} | {{{description}}} 33 | {{/if}} 34 | {{/each}} 35 | 36 | {{/if}} 37 | {{/each}} 38 | {{/if}} 39 | 40 | {{#if events.length}} 41 | ## Events 42 | 43 | {{#each events}} 44 | ### **{{{name}}}** 45 | 46 | {{{notice}}} 47 | 48 | {{{fullFormat}}} 49 | 50 | {{#if params.length}} 51 | | Name | Description 52 | | ---- | ----------- 53 | {{#each params}} 54 | {{#if description}} 55 | | {{{name}}} | {{{description}}} 56 | {{/if}} 57 | {{/each}} 58 | 59 | {{/if}} 60 | {{/each}} 61 | {{/if}} 62 | 63 | {{#if errors.length}} 64 | ## Errors 65 | 66 | {{#each errors}} 67 | ### **{{{name}}}** 68 | 69 | {{#each notice}} 70 | {{{this}}} 71 | 72 | {{/each}} 73 | 74 | {{{fullFormat}}} 75 | 76 | {{#if params.length}} 77 | | Name | Description 78 | | ---- | ----------- 79 | {{#each params}} 80 | {{#if description}} 81 | | {{{name}}} | {{{description}}} 82 | {{/if}} 83 | {{/each}} 84 | 85 | {{/if}} 86 | {{/each}} 87 | {{/if}} -------------------------------------------------------------------------------- /web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import adapter from '@sveltejs/adapter-static'; 3 | import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'; 4 | import {execSync} from 'child_process'; 5 | 6 | export function getVersion() { 7 | try { 8 | return execSync('git rev-parse --short HEAD').toString().trim(); 9 | } catch { 10 | const timestamp = Date.now().toString(); 11 | console.error(`could not get commit-hash to set a version id, falling back on timestamp ${timestamp}`); 12 | return timestamp; 13 | } 14 | } 15 | const VERSION = getVersion(); 16 | 17 | /** @type {import('@sveltejs/kit').Config} */ 18 | const config = { 19 | preprocess: [ 20 | vitePreprocess(), 21 | preprocess({ 22 | // postcss make use of tailwind 23 | // we ensure it get processed, see postcss.config.cjs 24 | postcss: true, 25 | }), 26 | ], 27 | 28 | kit: { 29 | adapter: adapter(), 30 | version: { 31 | // we create a dertemrinistic building using a derterministic version (via git commit, see above) 32 | name: VERSION, 33 | }, 34 | alias: { 35 | // alias for web-config 36 | 'web-config': './src/web-config.json', 37 | $data: './src/data', 38 | $external: './src/external', 39 | }, 40 | serviceWorker: { 41 | // we handle it ourselves here : src/service-worker-handler.ts 42 | register: false, 43 | }, 44 | paths: { 45 | // this is to make it work on ipfs (on an unknown path) 46 | relative: true, 47 | }, 48 | }, 49 | }; 50 | 51 | export default config; 52 | -------------------------------------------------------------------------------- /web/static/images/wallets/walletconnect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/lib/web3/Web3PendingActions.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if $pendingActions.list.length > 0} 9 | { 11 | // in case the tx is rejected while showing that confirmation modal 12 | // we need to close it 13 | // TODO have modal id to ensure we close the right one 14 | const unsubscribe = pendingActions.subscribe((p) => { 15 | if (p.list.length === 0) { 16 | modalStore.close(); 17 | } 18 | }); 19 | modalStore.trigger({ 20 | response: (yes) => { 21 | unsubscribe(); 22 | if (yes) { 23 | pendingActions.skip(); 24 | } 25 | return true; 26 | }, 27 | content: { 28 | type: 'confirm', 29 | message: 'Are you sure?', 30 | }, 31 | }); 32 | }} 33 | cancelation={{button: true, clickOutside: false}} 34 | > 35 | {#if $pendingActions.list[0].item.metadata && $pendingActions.list[0].item.metadata.title} 36 |

37 | {$pendingActions.list[0].item.metadata.title} 38 |

39 | {/if} 40 |

41 | {#if $pendingActions.list[0].item.metadata && $pendingActions.list[0].item.metadata.description} 42 | {$pendingActions.list[0].item.metadata.description} 43 | {:else} 44 | 'Please confirm or reject the request on your wallet.' 45 | {/if} 46 |

47 |
48 | {/if} 49 | -------------------------------------------------------------------------------- /dev/wezterm.lua: -------------------------------------------------------------------------------- 1 | local wezterm = require 'wezterm' 2 | local mux = wezterm.mux 3 | 4 | wezterm.on('gui-startup', function(cmd) 5 | local tab, pane, window 6 | tab, pane, window = mux.spawn_window { 7 | args = {'bash', '-i', '-c' , 'cd '.. cmd.args[1] .. '; bash'}, 8 | 9 | 10 | } 11 | tab:set_title 'jolly-roger' 12 | window:set_title 'jolly-roger' 13 | 14 | local pane_indexer = pane:split { 15 | args = {'bash', '-i', '-c', 'cd '.. cmd.args[1] .. '; sleep 1; pnpm indexer:dev; bash'}, 16 | direction = 'Bottom' 17 | } 18 | local pane_web = pane_indexer:split { 19 | args = {'bash', '-i', '-c', 'cd '.. cmd.args[1] .. '; sleep 1; pnpm web:dev; bash'}, 20 | direction = 'Right' 21 | } 22 | local pane_common = pane_indexer:split { 23 | args = {'bash', '-i', '-c', 'cd '.. cmd.args[1] .. '; sleep 1; pnpm common:dev; bash'}, 24 | direction = 'Right' 25 | } 26 | 27 | 28 | local pane_local_node = pane:split { 29 | args = {'bash', '-i', '-c', 'cd '.. cmd.args[1] .. '; sleep 1; pnpm local_node; bash'}, 30 | direction = 'Bottom' 31 | } 32 | 33 | 34 | local pane_contracts_compile = pane_local_node:split { 35 | args = {'bash', '-i', '-c', 'cd '.. cmd.args[1] .. '; sleep 1; pnpm contracts:compile:watch; bash'}, 36 | direction = 'Right' 37 | } 38 | 39 | local pane_contracts_deploy = pane_contracts_compile:split { 40 | args = {'bash', '-i', '-c', 'cd '.. cmd.args[1] .. '; sleep 1; pnpm contracts:deploy:watch; bash'}, 41 | direction = 'Bottom' 42 | } 43 | 44 | end) 45 | 46 | 47 | config = {} 48 | 49 | -- fix windows in virtualbox 50 | config.prefer_egl=true 51 | 52 | return config 53 | -------------------------------------------------------------------------------- /web/src/lib/components/daisyui/ThemeSwitcher.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /web/src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |

10 | {name} is a template for building decentralised applications. 11 |

12 |

13 | It can be used to quickly get a dev environment for hackathon or quick prototypes. 14 |

15 |

16 | But it is also a good starting point for long term projects as it contains a set of best practises for web and 17 | ipfs deployment. 18 |

19 |

20 | {name} with its in-browser indexer, is also a great ally to ensure your application remains fully decentralised. 21 |

22 |
23 | 24 |
25 |

Use it:

26 | 34 |

35 | Find out more on 36 | github 39 |

40 |
41 |
42 | -------------------------------------------------------------------------------- /contracts/hardhat.config.cjs: -------------------------------------------------------------------------------- 1 | const {loadEnv} = require('ldenv'); 2 | loadEnv(); 3 | require('@nomicfoundation/hardhat-network-helpers'); 4 | const {addForkConfiguration, addNetworksFromEnv} = require('hardhat-rocketh'); 5 | require('vitest-solidity-coverage/hardhat'); 6 | 7 | const {task} = require('hardhat/config'); 8 | // we override the node task to set up our block time interval 9 | // setting it up in the config below would also set it in tests and we do not want that 10 | task('node').setAction(async (args, hre, runSuper) => { 11 | hre.config.networks.hardhat.mining.interval = process.env['BLOCK_TIME'] 12 | ? parseInt(process.env['BLOCK_TIME']) * 1000 13 | : undefined; 14 | return runSuper(args); 15 | }); 16 | 17 | const defaultVersion = '0.8.20'; 18 | const defaultSettings = { 19 | optimizer: { 20 | enabled: true, 21 | runs: 999999, 22 | }, 23 | outputSelection: { 24 | '*': { 25 | '*': ['evm.methodIdentifiers'], 26 | }, 27 | }, 28 | }; 29 | 30 | const config = { 31 | solidity: { 32 | compilers: [ 33 | { 34 | version: defaultVersion, 35 | settings: {...defaultSettings}, 36 | }, 37 | ], 38 | }, 39 | networks: 40 | // this setup forking for netwoirk if env var HARDHAT_FORK is set 41 | addForkConfiguration( 42 | // this add network for each respective env var found (ETH_NODE_URI_) 43 | addNetworksFromEnv({ 44 | hardhat: { 45 | initialBaseFeePerGas: 0, 46 | mining: { 47 | auto: true, // TODO 48 | interval: process.env['BLOCK_TIME'] ? parseInt(process.env['BLOCK_TIME']) * 1000 : undefined, 49 | }, 50 | }, 51 | }), 52 | ), 53 | paths: { 54 | sources: 'src', 55 | }, 56 | docgen: { 57 | templates: 'docs_templates', 58 | pages: 'files', 59 | }, 60 | mocha: { 61 | require: 'named-logs-console', 62 | }, 63 | }; 64 | 65 | module.exports = config; 66 | -------------------------------------------------------------------------------- /web/src/lib/web/notifications.ts: -------------------------------------------------------------------------------- 1 | import {writable} from 'svelte/store'; 2 | 3 | type Notification = { 4 | id?: string; 5 | delay: number; 6 | onAcknowledge?: () => void; 7 | title: string; 8 | text: string; 9 | type: 'error' | 'success' | 'info' | 'warning'; 10 | }; 11 | 12 | type CurrentNotification = { 13 | current?: Notification; 14 | }; 15 | 16 | const createStore = () => { 17 | const recorded: Record = {}; 18 | const pending: Notification[] = []; 19 | const data: CurrentNotification = { 20 | current: undefined, 21 | }; 22 | 23 | let timeout: NodeJS.Timeout; 24 | 25 | const {subscribe, set} = writable(data); 26 | 27 | function setCurrent(current: Notification | undefined) { 28 | data.current = current; 29 | set(data); 30 | if (current && current.delay) { 31 | if (current.onAcknowledge) { 32 | current.onAcknowledge(); // TODO delay? 33 | } 34 | timeout = setTimeout(acknowledge, current.delay * 1000); 35 | } 36 | } 37 | 38 | function acknowledge() { 39 | const current = data.current; 40 | if (current && !current.delay && current.onAcknowledge) { 41 | current.onAcknowledge(); 42 | } 43 | const next = pending.shift(); 44 | clearTimeout(timeout); 45 | setCurrent(next); 46 | } 47 | 48 | function queue(notification: Notification) { 49 | if (notification.id) { 50 | if (recorded[notification.id]) { 51 | return; 52 | } 53 | recorded[notification.id] = true; 54 | } 55 | if (pending.length > 0 || data.current) { 56 | pending.push(notification); 57 | } else { 58 | setCurrent(notification); 59 | } 60 | } 61 | 62 | return { 63 | subscribe, 64 | queue, 65 | acknowledge, 66 | clear: () => { 67 | pending.splice(0, pending.length); 68 | acknowledge(); 69 | }, 70 | }; 71 | }; 72 | 73 | export const notifications = createStore(); 74 | -------------------------------------------------------------------------------- /web/src/lib/components/modals/Modal.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/src/lib/components/utilities/clipboard.ts: -------------------------------------------------------------------------------- 1 | // based on https://github.com/skeletonlabs/skeleton/blob/58d9780dafd4a7ca04b1086a30aac8c0dc3ce416/src/lib/actions/Clipboard/clipboard.ts 2 | // Action: Clipboard 3 | 4 | import type {ActionReturn} from 'svelte/action'; 5 | 6 | type Attributes = { 7 | newprop?: string; 8 | 'on:copied': (e: CustomEvent) => void; 9 | }; 10 | 11 | type Parameter = string | {element: string} | {input: string}; 12 | 13 | export function clipboard(node: HTMLElement, args: Parameter): ActionReturn { 14 | const onClick = () => { 15 | // Handle `data-clipboard` target based on object key 16 | if (typeof args === 'object') { 17 | // Element Inner HTML 18 | if ('element' in args) { 19 | const element: HTMLElement | null = document.querySelector(`[data-clipboard="${args.element}"]`); 20 | if (element?.innerHTML) { 21 | copyToClipboard(element?.innerHTML); 22 | node.dispatchEvent(new CustomEvent('copied', {detail: element?.innerHTML})); 23 | } 24 | return; 25 | } 26 | // Form Input Value 27 | if ('input' in args) { 28 | const input: HTMLInputElement | null = document.querySelector(`[data-clipboard="${args.input}"]`); 29 | if (input?.value) { 30 | copyToClipboard(input?.value); 31 | node.dispatchEvent(new CustomEvent('copied', {detail: input?.value})); 32 | } 33 | return; 34 | } 35 | } 36 | // Handle everything else. 37 | copyToClipboard(args); 38 | node.dispatchEvent(new CustomEvent('copied', {detail: args})); 39 | }; 40 | // Event Listner 41 | node.addEventListener('click', onClick); 42 | // Lifecycle 43 | return { 44 | update(newArgs: any) { 45 | args = newArgs; 46 | }, 47 | destroy() { 48 | node.removeEventListener('click', onClick); 49 | }, 50 | }; 51 | } 52 | 53 | // Shared copy method 54 | function copyToClipboard(data: any): void { 55 | navigator.clipboard.writeText(String(data)); 56 | } 57 | -------------------------------------------------------------------------------- /web/src/lib/components/utilities/focusTrap.ts: -------------------------------------------------------------------------------- 1 | // from https://github.com/skeletonlabs/skeleton/ 2 | // Action: Focus Trap 3 | 4 | export function focusTrap(node: HTMLElement, enabled: boolean) { 5 | const elemWhitelist = 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'; 6 | let elemFirst: HTMLElement; 7 | let elemLast: HTMLElement; 8 | 9 | // When the first element is selected, shift+tab pressed, jump to the last selectable item. 10 | function onFirstElemKeydown(e: KeyboardEvent): void { 11 | if (e.shiftKey && e.code === 'Tab') { 12 | e.preventDefault(); 13 | elemLast.focus(); 14 | } 15 | } 16 | 17 | // When the last item selected, tab pressed, jump to the first selectable item. 18 | function onLastElemKeydown(e: KeyboardEvent): void { 19 | if (!e.shiftKey && e.code === 'Tab') { 20 | e.preventDefault(); 21 | elemFirst.focus(); 22 | } 23 | } 24 | 25 | const onInit = () => { 26 | if (enabled === false) return; 27 | // Gather all focusable elements 28 | const focusableElems: HTMLElement[] = Array.from(node.querySelectorAll(elemWhitelist)); 29 | if (focusableElems.length) { 30 | // Set first/last focusable elements 31 | elemFirst = focusableElems[0]; 32 | elemLast = focusableElems[focusableElems.length - 1]; 33 | // Auto-focus first focusable element 34 | elemFirst.focus(); 35 | // Listen for keydown on first & last element 36 | elemFirst.addEventListener('keydown', onFirstElemKeydown); 37 | elemLast.addEventListener('keydown', onLastElemKeydown); 38 | } 39 | }; 40 | onInit(); 41 | 42 | function onDestory(): void { 43 | if (elemFirst) elemFirst.removeEventListener('keydown', onFirstElemKeydown); 44 | if (elemLast) elemLast.removeEventListener('keydown', onLastElemKeydown); 45 | } 46 | 47 | // Lifecycle 48 | return { 49 | update(newArgs: boolean) { 50 | enabled = newArgs; 51 | newArgs ? onInit() : onDestory(); 52 | }, 53 | destroy() { 54 | onDestory(); 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | {name} 16 |

19 | {name} 20 |

21 |
22 |

23 | Build 26 |

27 |

and

28 |

29 | Deploy 32 |

33 |

for

34 |

35 | Eternity. 38 |

39 |
40 |

Welcome to your !

41 |
42 |
43 | Demo 44 |
45 |
46 |
47 |
48 | 49 | 69 | -------------------------------------------------------------------------------- /web/src/data/networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "31337": { 3 | "config": { 4 | "rpcUrls": ["http://localhost:8545"], 5 | "blockExplorerUrls": ["http://localhost:4000"], 6 | "chainName": "Hardhat", 7 | "nativeCurrency": { 8 | "name": "Ether", 9 | "symbol": "ETH", 10 | "decimals": 18 11 | } 12 | }, 13 | "finality": 12 14 | }, 15 | "1": { 16 | "config": { 17 | "blockExplorerUrls": ["https://etherscan.io"], 18 | "chainName": "Mainnet", 19 | "nativeCurrency": { 20 | "name": "Ether", 21 | "symbol": "ETH", 22 | "decimals": 18 23 | } 24 | }, 25 | "finality": 12, 26 | "averageBblockTime": 12 27 | }, 28 | "11155111": { 29 | "config": { 30 | "rpcUrls": [ 31 | "https://rpc.sepolia.org", 32 | "https://rpc2.sepolia.org", 33 | "https://rpc.sepolia.online", 34 | "https://www.sepoliarpc.space", 35 | "https://rpc-sepolia.rockx.com", 36 | "https://rpc.bordel.wtf/sepolia" 37 | ], 38 | "blockExplorerUrls": ["https://sepolia.etherscan.io"], 39 | "chainName": "Sepolia Testnet", 40 | "iconUrls": null, 41 | "nativeCurrency": { 42 | "name": "Sepolia Ether", 43 | "symbol": "SepETH", 44 | "decimals": 18 45 | } 46 | }, 47 | "finality": 12, 48 | "averageBblockTime": 12 49 | }, 50 | "100": { 51 | "config": { 52 | "rpcUrls": ["https://rpc.gnosischain.com"], 53 | "blockExplorerUrls": ["https://gnosisscan.io/"], 54 | "chainName": "Gnosis Chain", 55 | "nativeCurrency": { 56 | "name": "XDai", 57 | "symbol": "XDAI", 58 | "decimals": 18 59 | } 60 | }, 61 | "finality": 12, 62 | "averageBblockTime": 5 63 | }, 64 | "10200": { 65 | "config": { 66 | "rpcUrls": ["https://rpc.chiadochain.net"], 67 | "blockExplorerUrls": ["https://blockscout.com/gnosis/chiado"], 68 | "chainName": "Chiado (Gnosis Testnet)", 69 | "nativeCurrency": { 70 | "name": "Chiado xDAI", 71 | "symbol": "ChXDAI", 72 | "decimals": 18 73 | } 74 | }, 75 | "finality": 12, 76 | "averageBblockTime": 5 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /web/src/lib/components/daisyui/themes.ts: -------------------------------------------------------------------------------- 1 | export let themes = [ 2 | { 3 | name: '🌝  light', 4 | id: 'light', 5 | }, 6 | { 7 | name: '🌚  dark', 8 | id: 'dark', 9 | }, 10 | { 11 | name: '🧁  cupcake', 12 | id: 'cupcake', 13 | }, 14 | { 15 | name: '🐝  bumblebee', 16 | id: 'bumblebee', 17 | }, 18 | { 19 | name: '✳️  Emerald', 20 | id: 'emerald', 21 | }, 22 | { 23 | name: '🏢  Corporate', 24 | id: 'corporate', 25 | }, 26 | { 27 | name: '🌃  synthwave', 28 | id: 'synthwave', 29 | }, 30 | { 31 | name: '👴  retro', 32 | id: 'retro', 33 | }, 34 | { 35 | name: '🤖  cyberpunk', 36 | id: 'cyberpunk', 37 | }, 38 | { 39 | name: '🌸  valentine', 40 | id: 'valentine', 41 | }, 42 | { 43 | name: '🎃  halloween', 44 | id: 'halloween', 45 | }, 46 | { 47 | name: '🌷  garden', 48 | id: 'garden', 49 | }, 50 | { 51 | name: '🌲  forest', 52 | id: 'forest', 53 | }, 54 | { 55 | name: '🐟  aqua', 56 | id: 'aqua', 57 | }, 58 | { 59 | name: '👓  lofi', 60 | id: 'lofi', 61 | }, 62 | { 63 | name: '🖍  pastel', 64 | id: 'pastel', 65 | }, 66 | { 67 | name: '🧚‍♀️  fantasy', 68 | id: 'fantasy', 69 | }, 70 | { 71 | name: '📝  Wireframe', 72 | id: 'wireframe', 73 | }, 74 | { 75 | name: '🏴  black', 76 | id: 'black', 77 | }, 78 | { 79 | name: '💎  luxury', 80 | id: 'luxury', 81 | }, 82 | { 83 | name: '🧛‍♂️  dracula', 84 | id: 'dracula', 85 | }, 86 | { 87 | name: '🖨  CMYK', 88 | id: 'cmyk', 89 | }, 90 | { 91 | name: '🍁  Autumn', 92 | id: 'autumn', 93 | }, 94 | { 95 | name: '💼  Business', 96 | id: 'business', 97 | }, 98 | { 99 | name: '💊  Acid', 100 | id: 'acid', 101 | }, 102 | { 103 | name: '🍋  Lemonade', 104 | id: 'lemonade', 105 | }, 106 | { 107 | name: '🌙  Night', 108 | id: 'night', 109 | }, 110 | { 111 | name: '☕️  Coffee', 112 | id: 'coffee', 113 | }, 114 | { 115 | name: '❄️  Winter', 116 | id: 'winter', 117 | }, 118 | ]; 119 | -------------------------------------------------------------------------------- /web/src/routes/debug/indexer/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
27 | {$syncing.lastSync?.syncPercentage 28 | ? $syncing.lastSync?.syncPercentage === 100 29 | ? $status.state 30 | : `${$syncing.lastSync?.syncPercentage}%` 31 | : '0%'} 32 |
33 | 34 | 35 | 36 |

status: {$status.state}

37 |

catchingUp: {$syncing.catchingUp}

38 |

autoIndexing: {$syncing.autoIndexing}

39 |

fetchingLogs: {$syncing.fetchingLogs}

40 |

processingFetchedLogs: {$syncing.processingFetchedLogs}

41 | 42 | {#if $syncing.numRequests !== undefined} 43 |

requests sent: {$syncing.numRequests}

44 | {/if} 45 |

46 | block processed: {$syncing.lastSync?.numBlocksProcessedSoFar?.toLocaleString() || 0} 47 |

48 | 49 |

50 | latestBlock: {$syncing.lastSync?.latestBlock || 0} 51 |

52 | 53 |
State
54 | 55 | {#if $state} 56 | 57 | {JSON.stringify(stateDisplayed, (key, value) => (typeof value === 'bigint' ? value.toString() : value), 2)} 58 | 59 | {:else} 60 | {JSON.stringify($syncing)} 61 | {/if} 62 | -------------------------------------------------------------------------------- /web/src/lib/utils/debug.ts: -------------------------------------------------------------------------------- 1 | import {devProvider} from '$lib/web3'; 2 | import {readable, writable, type Readable} from 'svelte/store'; 3 | 4 | export async function increaseBlockTime(numSeconds: number) { 5 | if (!devProvider) { 6 | throw new Error(`no dev provider`); 7 | } 8 | const block = await devProvider.request({ 9 | method: 'eth_getBlockByNumber', 10 | params: ['latest', false], 11 | }); 12 | if (!block) { 13 | throw new Error(`no block can be fetched`); 14 | } 15 | const old_timestamp = parseInt(block.timestamp.slice(2), 16); 16 | await devProvider.request({ 17 | method: 'evm_setNextBlockTimestamp', 18 | params: [`0x` + BigInt(old_timestamp + numSeconds).toString(16)], 19 | } as any); 20 | await devProvider.request({ 21 | method: 'evm_mine', 22 | params: [], 23 | } as any); 24 | } 25 | 26 | export async function enableAnvilLogging() { 27 | if (!devProvider) { 28 | throw new Error(`no dev provider`); 29 | } 30 | await devProvider.request({ 31 | method: 'anvil_setLoggingEnabled', 32 | params: [true], 33 | } as any); 34 | } 35 | 36 | // TODO move in promise utitilies 37 | export type Execution = {executing: boolean; error?: any; result?: T}; 38 | export function createExecutor Promise>( 39 | func: F, 40 | ): Readable> & { 41 | execute: F; 42 | acknowledgeError: () => void; 43 | } { 44 | const executing = writable>({executing: false}); 45 | 46 | const execute = ((...args: any[]) => { 47 | executing.set({executing: true, error: undefined, result: undefined}); 48 | return func(...args) 49 | .then((v) => { 50 | try { 51 | executing.set({executing: false, result: v}); 52 | } catch {} 53 | return v; 54 | }) 55 | .catch((err) => { 56 | try { 57 | executing.set({executing: false, error: err}); 58 | } catch {} 59 | throw err; 60 | }); 61 | }) as F; 62 | return { 63 | subscribe: executing.subscribe, 64 | acknowledgeError() { 65 | executing.set({executing: false, error: undefined, result: undefined}); 66 | }, 67 | execute, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /contracts/utils/connection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abi, 3 | CustomTransport, 4 | PublicClient, 5 | WalletClient, 6 | createPublicClient, 7 | createWalletClient, 8 | custom, 9 | defineChain, 10 | getContract, 11 | } from 'viem'; 12 | 13 | import hre from 'hardhat'; 14 | import type {EIP1193ProviderWithoutEvents} from 'eip-1193'; 15 | import {Chain} from 'viem'; 16 | 17 | export type Connection = { 18 | walletClient: WalletClient; 19 | publicClient: PublicClient; 20 | accounts: `0x${string}`[]; 21 | provider: EIP1193ProviderWithoutEvents; 22 | }; 23 | 24 | const cache: {connection?: Connection} = {}; 25 | export async function getConnection(): Promise { 26 | if (cache.connection) { 27 | return cache.connection; 28 | } 29 | const provider = hre.network.provider as EIP1193ProviderWithoutEvents; 30 | 31 | const chainIdAsHex = await provider.request({method: 'eth_chainId'}); 32 | const chainIdAsNumber = parseInt(chainIdAsHex.slice(2), 16); 33 | const chain = defineChain({ 34 | id: chainIdAsNumber, 35 | name: hre.network.name, 36 | network: hre.network.name, 37 | nativeCurrency: { 38 | decimals: 18, 39 | name: 'Ether', 40 | symbol: 'ETH', 41 | }, 42 | rpcUrls: { 43 | default: {http: []}, 44 | public: {http: []}, 45 | }, 46 | } as const); 47 | 48 | const walletClient = createWalletClient({ 49 | chain, 50 | transport: custom(provider), 51 | }); 52 | const publicClient = createPublicClient({ 53 | chain, 54 | transport: custom(provider), 55 | }); 56 | return (cache.connection = { 57 | walletClient, 58 | publicClient, 59 | accounts: await walletClient.getAddresses(), 60 | provider, 61 | }); 62 | } 63 | 64 | export async function fetchContract(contractInfo: {address: `0x${string}`; abi: TAbi}) { 65 | const {walletClient, publicClient} = await getConnection(); 66 | return getContract({ 67 | ...contractInfo, 68 | client: {wallet: walletClient, public: publicClient}, 69 | }); 70 | } 71 | 72 | export type ContractWithViemClient = Awaited>>; 73 | -------------------------------------------------------------------------------- /web/src/lib/web3/Web3Executing.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if $execution.executing} 14 | {#if $network.notSupported} 15 | 16 |

You are connected to unsupported network

17 |

18 | Proceed to switch to {getNetworkConfig($contractsInfos.chainId)?.chainName || 19 | `the network with chainID: ${$contractsInfos.chainId}`}. 20 |

21 | 33 |
34 | {:else if $account.isLoadingData} 35 | 36 | 37 | {:else if $account.state === 'Disconnected' && !$account.unlocking} 38 | 39 |

To proceed, you need to connect to a wallet.

40 | 53 |
54 | {/if} 55 | {/if} 56 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jolly-roger-web", 3 | "version": "0.0.1", 4 | "description": "Frontend for jolly-roger.", 5 | "type": "module", 6 | "private": true, 7 | "devDependencies": { 8 | "@sveltejs/adapter-static": "^3.0.0", 9 | "@sveltejs/kit": "^2.0.0", 10 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 11 | "@tailwindcss/forms": "^0.5.7", 12 | "@tailwindcss/typography": "^0.5.10", 13 | "abitype": "^0.10.3", 14 | "autoprefixer": "^10.4.16", 15 | "daisyui": "^4.6.0", 16 | "eip-1193": "^0.4.7", 17 | "ipfs-gateway-emulator": "4.2.1-ipfs.2", 18 | "postcss": "^8.4.33", 19 | "postcss-load-config": "^5.0.2", 20 | "prettier": "^3.2.4", 21 | "prettier-plugin-svelte": "^3.1.2", 22 | "pwag": "0.2.0", 23 | "svelte": "^4.2.9", 24 | "svelte-check": "^3.6.3", 25 | "svelte-preprocess": "^5.1.3", 26 | "tailwindcss": "^3.4.1", 27 | "tslib": "^2.6.2", 28 | "typescript": "^5.3.3", 29 | "vite": "^5.0.0", 30 | "wait-on": "^7.2.0" 31 | }, 32 | "dependencies": { 33 | "@noble/ciphers": "^0.4.1", 34 | "@scure/base": "^1.1.5", 35 | "@types/lodash-es": "^4.17.12", 36 | "ethereum-indexer-browser": "^0.6.28", 37 | "ethereum-tx-observer": "^0.0.3", 38 | "immer": "^10.0.3", 39 | "jolly-roger-common": "workspace:*", 40 | "jolly-roger-indexer": "workspace:*", 41 | "lodash-es": "^4.17.21", 42 | "lz-string": "^1.5.0", 43 | "named-logs": "^0.2.2", 44 | "radiate": "^0.0.2", 45 | "theme-change": "^2.5.0", 46 | "viem": "^2.1.1", 47 | "web3-connection": "^0.1.12", 48 | "web3-connection-viem": "^0.1.3" 49 | }, 50 | "scripts": { 51 | "prepare": "pwag static/icon.svg src/web-config.json", 52 | "dev": "echo 'waiting for src/data/contracts.ts...'; wait-on src/data/contracts.ts && ldenv vite dev", 53 | "build": "pnpm run prepare && ldenv vite build", 54 | "preview": "ldenv vite preview", 55 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 56 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 57 | "format:check": "prettier --check .", 58 | "format": "prettier --write .", 59 | "svelte-check": "svelte-check", 60 | "serve": "ipfs-emulator --only -d build -p 8080" 61 | } 62 | } -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | const mainContract = 'Registry'; 8 | 9 | const contractFilenames = fs.readdirSync(path.join(__dirname, '../contracts')); 10 | const contractNames = contractFilenames.map((filename) => path.basename(filename, '.md')); 11 | const firstContractName = contractNames.indexOf(mainContract) == -1 ? contractNames[0] : mainContract; 12 | 13 | const contracts = contractNames.sort((a,b) => a === firstContractName ? -1 : b === firstContractName ? 1 : (a > b ? 1: a { 15 | return { 16 | text: name, link: `/contracts/${name}/` 17 | }; 18 | }); 19 | 20 | const isRunningOnVercel = !!process.env.VERCEL; 21 | 22 | // https://vitepress.dev/reference/site-config 23 | export default defineConfig({ 24 | base: isRunningOnVercel ? '/' : '/jolly-roger/', 25 | title: "Jolly Roger", 26 | description: "Build and Deploy for Eternity. Jolly Roger is a production-ready template for decentralised applications.", 27 | head: [ 28 | 29 | ], 30 | themeConfig: { 31 | logo: "/icon.svg", 32 | // https://vitepress.dev/reference/default-theme-config 33 | nav: [ 34 | { text: 'Home', link: '/' }, 35 | { text: 'Getting Started', link: '/guide/getting-started/' }, 36 | { text: 'Contracts', link: `/contracts/${firstContractName}/` } 37 | ], 38 | 39 | sidebar: [ 40 | { 41 | text: 'Documentation', 42 | items: [ 43 | { text: 'Getting Started', link: '/guide/getting-started/' }, 44 | { text: 'Contracts', items: contracts} 45 | ] 46 | } 47 | ], 48 | 49 | socialLinks: [ 50 | { icon: 'github', link: 'https://github.com/wighawag/jolly-roger#readme' }, 51 | { icon: 'twitter', link: 'https://twitter.com/jollyroger_eth' }, 52 | ], 53 | 54 | search: { 55 | provider: 'local' 56 | }, 57 | 58 | footer: { 59 | message: 'Released under the MIT License.', 60 | copyright: 'Copyright © 2019-present Ronan Sandford' 61 | } 62 | }, 63 | rewrites: { 64 | 'contracts/:pkg.md': 'contracts/:pkg/index.md' 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /web/src/lib/components/utilities/click-outside.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * taken from : https://github.com/svelteuidev/svelteui/blob/822fedc4c08c1ba777b4d9a5780a737225601184/packages/svelteui-composables/src/actions/use-click-outside/use-click-outside.ts 3 | * With the `use-click-outside` action, a callback function will be fired whenever the user clicks outside of the dom node the action is applied to. 4 | * 5 | * ```tsx 6 | * 11 | * 12 | *
open = false }}> 13 | * 14 | * 15 | * 16 | * {#if open} 17 | *
18 | * This is a modal 19 | *
20 | * {:else if !open} 21 | *
22 | * There is no modal 23 | *
24 | * {/if} 25 | *
26 | * ``` 27 | * @param params - Object that contains two properties {enabled: boolean, callback: (any) => unknown} 28 | * @see https://svelteui.org/actions/use-click-outside 29 | */ 30 | 31 | export type Action = ( 32 | node: HTMLElement, 33 | parameters?: T, 34 | ) => { 35 | update?: (parameters: T) => any | void; 36 | destroy?: () => void; 37 | }; 38 | 39 | export function clickoutside( 40 | node: HTMLElement, 41 | params: {enabled: boolean; callback: (node: HTMLElement) => unknown}, 42 | ): ReturnType { 43 | const {enabled: initialEnabled, callback} = params; 44 | 45 | const handleOutsideClick = ({target}: MouseEvent) => { 46 | if (!node.contains(target as HTMLElement)) { 47 | callback(node); 48 | } 49 | }; 50 | 51 | let currentEnabledStatus = initialEnabled; 52 | function update({enabled}: {enabled: boolean}) { 53 | if (enabled) { 54 | currentEnabledStatus = true; 55 | setTimeout(() => { 56 | if (currentEnabledStatus) { 57 | window.addEventListener('click', handleOutsideClick); 58 | } 59 | }, 1); 60 | } else { 61 | currentEnabledStatus = false; 62 | window.removeEventListener('click', handleOutsideClick); 63 | } 64 | } 65 | update({enabled: initialEnabled}); 66 | return { 67 | update, 68 | destroy() { 69 | window.removeEventListener('click', handleOutsideClick); 70 | }, 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jolly-roger-contracts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "devDependencies": { 7 | "@nomicfoundation/hardhat-network-helpers": "^1.0.10", 8 | "as-soon": "^0.0.4", 9 | "eip-1193": "^0.4.7", 10 | "hardhat": "^2.19.4", 11 | "hardhat-rocketh": "^1.0.4", 12 | "jolly-roger-common": "workspace:*", 13 | "ldenv": "^0.3.9", 14 | "prettier": "^3.2.4", 15 | "prettier-plugin-solidity": "^1.3.1", 16 | "rocketh": "^0.7.4", 17 | "rocketh-deploy": "^1.0.5", 18 | "rocketh-deploy-proxy": "^1.0.5", 19 | "rocketh-doc": "^1.0.4", 20 | "rocketh-export": "^1.0.4", 21 | "rocketh-signer": "^1.0.4", 22 | "rocketh-verifier": "^0.7.4", 23 | "set-defaults": "^0.0.2", 24 | "solidity-coverage": "^0.8.5", 25 | "solidity-proxy": "^0.2.4", 26 | "tsx": "^4.7.0", 27 | "typescript": "^5.3.3", 28 | "viem": "^2.1.1", 29 | "viem-chai-matchers": "^0.0.4", 30 | "vitest": "^1.2.0", 31 | "vitest-solidity-coverage": "^0.1.5" 32 | }, 33 | "scripts": { 34 | "prepare": "pnpm compile", 35 | "docgen": "ldenv -m localhost pnpm rocketh-doc -n @@MODE --except-suffix _Implementation,_Proxy,_Router,_Route -t docs_templates/{{contracts}}.hbs @@", 36 | "local_node": "hardhat node", 37 | "compile": "hardhat compile", 38 | "compile:watch": "as-soon -w src pnpm compile", 39 | "execute": "ROCKETH_SKIP_ESBUILD=true ldenv -n HARDHAT_NETWORK -m localhost tsx @@", 40 | "deploy": "ldenv hardhat --network @@MODE deploy @@", 41 | "deploy:watch": "as-soon -w generated -w deploy pnpm run deploy", 42 | "verify": "ldenv rocketh-verify -n @@MODE @@", 43 | "test": "vitest", 44 | "coverage:compile": "hardhat compile-for-coverage", 45 | "coverage:watch:compile": "as-soon -w src pnpm coverage:compile", 46 | "coverage:watch": "pnpm coverage:compile && vitest --coverage --no-threads", 47 | "coverage": "pnpm coverage:compile && vitest run --coverage || pnpm compile", 48 | "export": "rocketh-export", 49 | "start": "zellij --layout zellij.kdl a $npm_package_name || zellij --layout zellij.kdl -s $npm_package_name", 50 | "stop": "zellij kill-session $npm_package_name", 51 | "format:check": "prettier --check .", 52 | "format": "prettier --write ." 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/src/lib/components/modals/ModalContent.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | {#if showCancelButton} 29 | 33 | {/if} 34 | {#if settings?.type && settings.type !== 'custom'} 35 | {#if settings.type === 'info'} 36 | {#if settings.title} 37 |

{settings.title}

38 | {/if} 39 |

40 | {settings.message} 41 |

42 | {:else if settings.type === 'confirm'} 43 | {#if settings.title} 44 |

{settings.title}

45 | {/if} 46 |

47 | {settings.message} 48 |

49 | 57 | {:else} 58 | 59 | 60 | {/if} 61 | {:else} 62 | 63 | {/if} 64 |
65 | -------------------------------------------------------------------------------- /web/src/lib/account/account-data.ts: -------------------------------------------------------------------------------- 1 | import type {EIP1193TransactionWithMetadata} from 'web3-connection'; 2 | import type {PendingTransaction} from 'ethereum-tx-observer'; 3 | import {BaseAccountHandler, type OnChainAction, type OnChainActions} from './base'; 4 | 5 | export type SendMessageMetadata = { 6 | type: 'message'; 7 | message: string; 8 | }; 9 | export type JollyRogerMetadata = SendMessageMetadata; 10 | 11 | export type JollyRogerTransaction = EIP1193TransactionWithMetadata & { 12 | metadata?: JollyRogerMetadata; 13 | }; 14 | 15 | export type AccountData = { 16 | onchainActions: OnChainActions; 17 | }; 18 | 19 | function fromOnChainActionToPendingTransaction( 20 | hash: `0x${string}`, 21 | onchainAction: OnChainAction, 22 | ): PendingTransaction { 23 | return { 24 | hash, 25 | request: onchainAction.tx, 26 | final: onchainAction.final, 27 | inclusion: onchainAction.inclusion, 28 | status: onchainAction.status, 29 | } as PendingTransaction; 30 | } 31 | 32 | export class JollyRogerAccountData extends BaseAccountHandler { 33 | constructor() { 34 | super( 35 | 'jolly-roger', 36 | () => ({ 37 | onchainActions: {}, 38 | }), 39 | fromOnChainActionToPendingTransaction, 40 | ); 41 | } 42 | 43 | _merge( 44 | localData?: AccountData | undefined, 45 | remoteData?: AccountData | undefined, 46 | ): {newData: AccountData; newDataOnLocal: boolean; newDataOnRemote: boolean} { 47 | let newDataOnLocal = false; 48 | let newDataOnRemote = false; 49 | 50 | let newData: AccountData; 51 | if (!localData) { 52 | newData = { 53 | onchainActions: {}, 54 | }; 55 | } else { 56 | newData = localData; 57 | } 58 | 59 | // hmm check if valid 60 | if (!remoteData || !remoteData.onchainActions) { 61 | remoteData = { 62 | onchainActions: {}, 63 | }; 64 | } 65 | 66 | for (const key of Object.keys(remoteData.onchainActions)) { 67 | const txHash = key as `0x${string}`; 68 | if (!newData.onchainActions[txHash]) { 69 | newData.onchainActions[txHash] = remoteData.onchainActions[txHash]; 70 | newDataOnRemote = true; 71 | } 72 | } 73 | 74 | for (const key of Object.keys(newData.onchainActions)) { 75 | const txHash = key as `0x${string}`; 76 | if (!remoteData.onchainActions[txHash]) { 77 | newDataOnLocal = true; 78 | break; 79 | } 80 | } 81 | 82 | return { 83 | newData, 84 | newDataOnLocal, 85 | newDataOnRemote, 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /web/src/lib/web3/Web3WalletSelection.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 | {#if $connection.requireSelection} 44 | connection.cancel()}> 45 |
46 |

How do you want to connect ?

47 |
48 |
49 | {#each options as option} 50 | 51 | 53 | {`Login connection.select(option.id)} 58 | /> 59 | {/each} 60 |
61 | {#if builtinNeedInstalation} 62 |
OR
63 | 73 | {/if} 74 |
75 | {/if} 76 | -------------------------------------------------------------------------------- /contracts/README.md: -------------------------------------------------------------------------------- 1 | ## How to use? 2 | 3 | ### Compile your contracts 4 | 5 | ```bash 6 | pnpm compile 7 | ``` 8 | 9 | ### Test your contracts 10 | 11 | There is 2 flavors of test 12 | 13 | 1. Using hardhat 14 | 15 | ```bash 16 | pnpm test 17 | ``` 18 | 19 | 2. Using foundry 20 | 21 | ```bash 22 | forge test 23 | ``` 24 | 25 | This assumes you have `forge` installed and that you added forge-std in via the following command 26 | 27 | ```bash 28 | git clone --recursive https://github.com/foundry-rs/forge-std.git lib/forge-std 29 | ``` 30 | 31 | You can also add it as a submodule if you prefers 32 | 33 | ### watch for changes and rebuild automatically 34 | 35 | ```bash 36 | pnpm watch_compile 37 | ``` 38 | 39 | ### deploy your contract 40 | 41 | - on localhost 42 | 43 | This assume you have a local node running : `pnpm local_node` 44 | 45 | ```bash 46 | pnpm run deploy localhost 47 | ``` 48 | 49 | - on a network of your choice 50 | 51 | Just make sure you have your .env.local setup, see [.env](.env) 52 | 53 | ```bash 54 | pnpm run deploy 55 | ``` 56 | 57 | ### execute scripts 58 | 59 | The setup currently use hardhat run and so to pass argument we use env variables 60 | 61 | ```bash 62 | MESSAGE="hello earth" pnpm hardhat --network localhost run scripts/setMessage.ts 63 | ``` 64 | 65 | ```bash 66 | ACCOUNT=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 pnpm hardhat --network localhost run scripts/readMessage.ts 67 | ``` 68 | 69 | ### zellij 70 | 71 | [zellij](https://zellij.dev/) is a useful multiplexer (think tmux) for which we have included a [layout file](./zellij.kdl) to get started 72 | 73 | Once installed simply run 74 | 75 | ```bash 76 | pnpm start 77 | ``` 78 | 79 | And you'll have anvil running as well as watch process executing tests on changes 80 | 81 | if you want to try zellij without install try this : 82 | 83 | ```bash 84 | bash <(curl -L zellij.dev/launch) --layout zellij.kdl 85 | ``` 86 | 87 | In the shell in the upper pane, you can deploy your contract via 88 | 89 | ```bash 90 | pnpm run deploy 91 | ``` 92 | 93 | ## Initial Setup 94 | 95 | You need to have these installed 96 | 97 | - [nodejs](https://nodejs.org/en) 98 | 99 | - [pnpm](https://pnpm.io/) 100 | 101 | ```bash 102 | npm i -g pnpm 103 | ``` 104 | 105 | Then you need to install the local dependencies with the following command: 106 | 107 | ```bash 108 | pnpm i 109 | ``` 110 | 111 | We also recommend to install [zellij](https://zellij.dev/) to have your dev env setup in one go via `pnpm start` 112 | -------------------------------------------------------------------------------- /web/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import {readable} from 'svelte/store'; 2 | import {version} from '$app/environment'; 3 | 4 | import {getParamsFromLocation, getHashParamsFromLocation} from '$lib/utils/url'; 5 | import { 6 | PUBLIC_ETH_NODE_URI_LOCALHOST, 7 | PUBLIC_ETH_NODE_URI, 8 | PUBLIC_LOCALHOST_BLOCK_TIME, 9 | PUBLIC_DEV_NODE_URI, 10 | PUBLIC_SYNC_URI, 11 | } from '$env/static/public'; 12 | 13 | import _contractsInfos from '$data/contracts'; 14 | export type NetworkConfig = typeof _contractsInfos; 15 | 16 | export const initialContractsInfos = _contractsInfos; 17 | 18 | export const globalQueryParams = ['debug', 'log', 'ethnode', '_d_eruda']; 19 | 20 | export const hashParams = getHashParamsFromLocation(); 21 | export const {params} = getParamsFromLocation(); 22 | 23 | const contractsChainId = initialContractsInfos.chainId as string; 24 | let defaultRPCURL: string | undefined = params['ethnode']; 25 | 26 | let blockTime: number | undefined = undefined; 27 | 28 | let isUsingLocalDevNetwork = false; 29 | if (contractsChainId === '1337' || contractsChainId === '31337') { 30 | isUsingLocalDevNetwork = true; 31 | if (!defaultRPCURL) { 32 | const url = PUBLIC_ETH_NODE_URI_LOCALHOST as string; 33 | if (url && url !== '') { 34 | defaultRPCURL = url; 35 | } 36 | } 37 | blockTime = PUBLIC_LOCALHOST_BLOCK_TIME ? parseInt(PUBLIC_LOCALHOST_BLOCK_TIME) : undefined; 38 | } 39 | if (!defaultRPCURL) { 40 | const url = PUBLIC_ETH_NODE_URI as string; 41 | if (url && url !== '') { 42 | defaultRPCURL = url; 43 | } 44 | } 45 | 46 | const localRPC = 47 | isUsingLocalDevNetwork && PUBLIC_DEV_NODE_URI ? {chainId: contractsChainId, url: PUBLIC_DEV_NODE_URI} : undefined; 48 | 49 | const defaultRPC = defaultRPCURL ? {chainId: contractsChainId, url: defaultRPCURL} : undefined; 50 | 51 | // This allow to debug what is written to local storage 52 | // Disable this if the data should remains private 53 | export const doNotEncryptLocally = true; 54 | 55 | const syncInfo = PUBLIC_SYNC_URI 56 | ? { 57 | uri: PUBLIC_SYNC_URI, 58 | } 59 | : undefined; 60 | 61 | export {defaultRPC, isUsingLocalDevNetwork, localRPC, blockTime, syncInfo}; 62 | 63 | let _setContractsInfos: any; 64 | export const contractsInfos = readable(_contractsInfos, (set) => { 65 | _setContractsInfos = set; 66 | }); 67 | 68 | export function _asNewModule(set: any) { 69 | _setContractsInfos = set; 70 | } 71 | 72 | if (import.meta.hot) { 73 | import.meta.hot.accept((newModule) => { 74 | newModule?._asNewModule(_setContractsInfos); 75 | _setContractsInfos(newModule?.initialContractsInfos); 76 | }); 77 | } 78 | 79 | console.log(`VERSION: ${version}`); 80 | -------------------------------------------------------------------------------- /dev/docker-compose-geth.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ethereum: 4 | image: ethereum/client-go:v1.11.6 5 | ports: 6 | - '8545:8545' 7 | - '30303:30303' 8 | entrypoint: ['sh', '-c'] 9 | command: 10 | [ 11 | 'mkdir -p $HOME/.geth-dev/keystore; echo ''testtesttest'' > $HOME/.geth-dev/password ;echo ''{"address":"f39fd6e51aad88f6f4ce6ab8827279cfffb92266","crypto":{"cipher":"aes-128-ctr","ciphertext":"a5982ef69eb74b9e2983682f4e9ee5e319e5ad34490896ee495e34bad72a8357","cipherparams":{"iv":"c2ca22a6fac1d3ab309d1e7e482409a5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"53b601aff894a272fe60ae729229400462f9027ac665ed8dd08634f130413c01"},"mac":"35c6a45bee206fe273cf4a5edb5aa374d3373176090995119250a1d961d3ae34"},"id":"beff5fef-c7bd-4c32-97ce-133018c4d5d7","version":3}'' > $HOME/.geth-dev/keystore/UTC--2023-05-04T10-27-54.443489969Z--f39fd6e51aad88f6f4ce6ab8827279cfffb92266; echo ''{"config": {"chainId": 31337,"homesteadBlock": 0,"eip150Block": 0,"eip155Block": 0,"eip158Block": 0,"byzantiumBlock": 0,"constantinopleBlock": 0,"petersburgBlock": 0,"istanbulBlock": 0,"berlinBlock": 0,"londonBlock": 0,"parisBlock": 0,"clique": {"period": 5,"epoch": 30000}},"difficulty": "1","gasLimit": "8000000","extradata": "0x0000000000000000000000000000000000000000000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb922660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","alloc": {"f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { "balance": "100000000000000000000" },"0x70997970C51812dc3A010C7d01b50e0d17dc79C8": { "balance": "1000000000000000000" },"3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": { "balance": "1000000000000000000" },"0x90F79bf6EB2c4f870365E785982E1f101E93b906": { "balance": "1000000000000000000" },"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65": { "balance": "1000000000000000000" },"0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc": { "balance": "1000000000000000000" },"0x976EA74026E726554dB657fA54763abd0C3a0aa9": { "balance": "1000000000000000000" },"0x14dC79964da2C08b23698B3D3cc7Ca32193d9955": { "balance": "1000000000000000000" },"0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": { "balance": "1000000000000000000" },"0xa0Ee7A142d267C1f36714E4a8F75612F20a79720": { "balance": "1000000000000000000" }}}'' > $HOME/.geth-dev/genesis.json; geth --datadir $HOME/.geth-dev init $HOME/.geth-dev/genesis.json; geth --dev --nousb --ws --ws.addr "0.0.0.0" --ws.origins "*" --http --http.vhosts "*" --http.addr "0.0.0.0" --http.corsdomain "*" --miner.gasprice 1000000000 --dev.period ${BLOCK_TIME:-5} --rpc.allow-unprotected-txs --datadir $HOME/.geth-dev --password $HOME/.geth-dev/password', 12 | ] 13 | -------------------------------------------------------------------------------- /web/src/routes/debug/utilities/+page.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 |

{date.toLocaleDateString() + ` ` + date.toLocaleTimeString()}

40 | 41 | {#if error} 42 | {error.message} 43 | 44 | {:else} 45 | 46 | 49 |
50 |