├── .gitignore ├── README.md ├── images ├── cover.jpg ├── current-polygon-flow.jpg ├── decentralized-submitters.jpg ├── hyperscale-for-data.jpg ├── logo.png ├── momoka-explorer.jpg ├── momoka-flow.jpg ├── momoka-network.jpg ├── submitter-flow.jpg └── tech-comparison.jpg ├── momoka-node ├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.stable ├── LICENSE.txt ├── README.md ├── client │ └── package.json ├── codegen.yaml ├── jest.config.js ├── package.json ├── playground-browser │ ├── .gitignore │ ├── README.md │ ├── config-overrides.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── render.yaml ├── src │ ├── __TESTS__ │ │ ├── comment.e2e.test.ts │ │ ├── config │ │ │ └── jest.setup.js │ │ ├── db.test.ts │ │ ├── mirror.e2e.test.ts │ │ ├── mocks │ │ │ ├── comment │ │ │ │ ├── comment-created-delegate-arweave-response.mock.ts │ │ │ │ └── comment-created-without-delegate-arweave-response.mock.ts │ │ │ ├── mirror │ │ │ │ ├── mirror-created-delegate-comment-arweave-response.mock.ts │ │ │ │ ├── mirror-created-delegate-post-arweave-response.mock.ts │ │ │ │ ├── mirror-created-without-delegate-comment-arweave-response.mock.ts │ │ │ │ └── mirror-created-without-delegate-post-arweave-response.mock.ts │ │ │ ├── post │ │ │ │ ├── post-created-delegate-arweave-response.mock.ts │ │ │ │ └── post-created-without-delegate-arweave-response.mock.ts │ │ │ └── shared.mock.ts │ │ ├── post.e2e.test.ts │ │ ├── publications │ │ │ └── publication.base.test.ts │ │ ├── random.test.ts │ │ └── submitters.test.ts │ ├── bin │ │ └── cli.ts │ ├── client │ │ ├── axios-provider.ts │ │ ├── check-da-proof-client.ts │ │ ├── client-da-proof-gateway.ts │ │ ├── client-da-proof-verifier.ts │ │ └── index.ts │ ├── common │ │ ├── environment.ts │ │ ├── helpers.ts │ │ ├── in-memory-store.ts │ │ └── logger.ts │ ├── data-availability-models │ │ ├── da-result.ts │ │ ├── data-availability-action-types.ts │ │ ├── data-availability-provider.ts │ │ ├── data-availability-structure-base.ts │ │ ├── data-availability-timestamp-proofs.ts │ │ ├── data-availability-typed-data.ts │ │ ├── publications │ │ │ ├── data-availability-publication-typed-data.ts │ │ │ ├── data-availability-structure-publication.ts │ │ │ └── data-availability-structure-publications-events.ts │ │ └── validator-errors.ts │ ├── evm │ │ ├── abi-types │ │ │ ├── LensHubV1.ts │ │ │ ├── LensHubV1Events.ts │ │ │ ├── LensHubV2.ts │ │ │ └── LensHubV2Events.ts │ │ ├── anvil.ts │ │ ├── contract-lens │ │ │ ├── lens-hub-v1-contract-abi.ts │ │ │ └── lens-hub-v2-contract-abi.ts │ │ ├── ethereum.ts │ │ ├── gateway │ │ │ ├── LensHubV1Gateway.ts │ │ │ └── LensHubV2Gateway.ts │ │ └── jsonrpc-methods.ts │ ├── graphql │ │ ├── data-availability-transactions.graphql │ │ ├── generated.ts │ │ └── urql.client.ts │ ├── index.ts │ ├── input-output │ │ ├── arweave │ │ │ └── get-arweave-by-id.api.ts │ │ ├── bundlr │ │ │ ├── bundlr-config.ts │ │ │ ├── get-bundlr-bulk-txs.api.ts │ │ │ ├── get-bundlr-by-id.api.ts │ │ │ ├── get-data-availability-transactions.api.ts │ │ │ └── get-owner-of-transaction.api.ts │ │ ├── common.ts │ │ ├── db.ts │ │ ├── fetch-with-timeout.ts │ │ ├── json-rpc-with-timeout.ts │ │ ├── lib-curl-provider.ts │ │ ├── paths.ts │ │ ├── post-with-timeout.ts │ │ └── tx-validated-results.ts │ ├── proofs │ │ ├── check-da-proof.ts │ │ ├── check-da-proofs-batch.ts │ │ ├── da-proof-checker.ts │ │ ├── da-proof-gateway.ts │ │ ├── da-proof-verifier.ts │ │ ├── models │ │ │ └── check-da-submisson-options.ts │ │ ├── publications │ │ │ ├── comment │ │ │ │ ├── da-comment-verifier-v1.ts │ │ │ │ ├── da-comment-verifier-v2.ts │ │ │ │ └── index.ts │ │ │ ├── create-da-publication-verifier.ts │ │ │ ├── da-publication-verifier-v1.ts │ │ │ ├── da-publication-verifier-v2.ts │ │ │ ├── mirror │ │ │ │ ├── da-mirror-verifier-v1.ts │ │ │ │ ├── da-mirror-verifier-v2.ts │ │ │ │ └── index.ts │ │ │ ├── post │ │ │ │ ├── da-post-verifier-v1.ts │ │ │ │ ├── da-post-verifier-v2.ts │ │ │ │ └── index.ts │ │ │ ├── publication.base.ts │ │ │ └── quote │ │ │ │ ├── da-quote-verifier-v2.ts │ │ │ │ └── index.ts │ │ └── utils.ts │ ├── queue │ │ ├── base.queue.ts │ │ ├── known.queue.ts │ │ ├── process-failed-da-proof.queue.ts │ │ ├── process-retry-check-da-proofs.queue.ts │ │ └── startup.queue.ts │ ├── runnable │ │ ├── da-verifier-node.runnable.ts │ │ ├── ethereum-node-instance.ts │ │ └── failed-submissons.runnable.ts │ ├── submitters.ts │ ├── utils │ │ ├── arrays-equal.ts │ │ ├── invariant.ts │ │ └── number-to-hex.ts │ ├── watchers │ │ ├── failed-submissons.watcher.ts │ │ ├── models │ │ │ ├── start-da-verifier-node-options.ts │ │ │ └── stream.type.ts │ │ ├── trusting-indexing.watcher.ts │ │ └── verifier.watcher.ts │ └── workers │ │ ├── handler-communication.worker.ts │ │ ├── message-handlers │ │ ├── bundlr-verify-receipt.worker.ts │ │ ├── evm-verify-message.worker.ts │ │ └── index.ts │ │ └── worker-pool.ts └── tsconfig.json └── momoka-rs ├── .env ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── abi └── lens_hub_contract_abi.json ├── bundlr ├── api.rs ├── mod.rs └── verify.rs ├── cache.rs ├── contracts ├── lens_hub.rs └── mod.rs ├── environment.rs ├── evm.rs ├── http.rs ├── logger.rs ├── main.rs ├── submitter ├── mod.rs └── state.rs ├── types ├── chain_proofs.rs ├── eip721.rs ├── evm_event.rs ├── hex.rs ├── mod.rs ├── profile_id.rs ├── publication_id.rs ├── transaction.rs └── verifier_error.rs ├── utils.rs └── verifier ├── mod.rs ├── proof.rs └── transactions ├── comment.rs ├── common.rs ├── mirror.rs ├── mod.rs └── post.rs /.gitignore: -------------------------------------------------------------------------------- 1 | momoka-node/node_modules 2 | momoka-node/.env 3 | momoka-node/coverage 4 | momoka-node/coverage.json 5 | momoka-node/lib/ 6 | momoka-node/database/* 7 | momoka-node/da-verifier-database/* 8 | .DS_Store 9 | momoka-node/cache/* 10 | momoka-node/src/__PLAYGROUND__/* 11 | momoka-node/lens__da/* 12 | .vscode 13 | .idea 14 | momoka-node/.env.test 15 | momoka-rs/target/* 16 | -------------------------------------------------------------------------------- /images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/cover.jpg -------------------------------------------------------------------------------- /images/current-polygon-flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/current-polygon-flow.jpg -------------------------------------------------------------------------------- /images/decentralized-submitters.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/decentralized-submitters.jpg -------------------------------------------------------------------------------- /images/hyperscale-for-data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/hyperscale-for-data.jpg -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/logo.png -------------------------------------------------------------------------------- /images/momoka-explorer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/momoka-explorer.jpg -------------------------------------------------------------------------------- /images/momoka-flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/momoka-flow.jpg -------------------------------------------------------------------------------- /images/momoka-network.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/momoka-network.jpg -------------------------------------------------------------------------------- /images/submitter-flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/submitter-flow.jpg -------------------------------------------------------------------------------- /images/tech-comparison.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/images/tech-comparison.jpg -------------------------------------------------------------------------------- /momoka-node/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | npm-debug.log 3 | Dockerfile 4 | .gitignore 5 | jest.config.js 6 | .git 7 | node_modules 8 | -------------------------------------------------------------------------------- /momoka-node/.env.example: -------------------------------------------------------------------------------- 1 | ETHEREUM_NETWORK=MUMBAI|POLYGON 2 | NODE_URL=YOUR_ARCHIVE_NODE_URL 3 | CONCURRENCY=3 -------------------------------------------------------------------------------- /momoka-node/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | ignorePatterns: ['.eslintrc.js', '*.json', '*.test.ts', '*.mock.ts'], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:import/recommended', 11 | 'plugin:import/typescript', 12 | 'plugin:prettier/recommended', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 12, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['@typescript-eslint', 'prettier', 'prefer-arrow'], 20 | settings: { 21 | 'import/resolver': { 22 | typescript: true, 23 | node: true, 24 | }, 25 | 'import/internal-regex': '^@lens/', 26 | }, 27 | rules: { 28 | '@typescript-eslint/comma-dangle': [ 29 | 'error', 30 | { 31 | arrays: 'always-multiline', 32 | objects: 'always-multiline', 33 | imports: 'always-multiline', 34 | exports: 'always-multiline', 35 | enums: 'always-multiline', 36 | functions: 'never', 37 | }, 38 | ], 39 | '@typescript-eslint/ban-types': [ 40 | 'error', 41 | { types: { Function: false, Boolean: false }, extendDefaults: true }, 42 | ], 43 | '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], 44 | '@typescript-eslint/no-empty-interface': ['warn'], 45 | '@typescript-eslint/no-empty-function': 'warn', 46 | '@typescript-eslint/no-explicit-any': ['warn'], 47 | '@typescript-eslint/no-shadow': ['error'], 48 | '@typescript-eslint/no-namespace': 'off', 49 | '@typescript-eslint/no-non-null-assertion': 'off', 50 | '@typescript-eslint/no-unused-vars': [ 51 | 'warn', 52 | { 53 | argsIgnorePattern: '^_', 54 | }, 55 | ], 56 | '@typescript-eslint/no-var-requires': 'off', 57 | '@typescript-eslint/ban-ts-comment': ['warn'], 58 | // '@typescript-eslint/naming-convention': [ 59 | // 'error', 60 | // { 61 | // selector: 'default', 62 | // format: ['camelCase', 'PascalCase'], 63 | // leadingUnderscore: 'allow', 64 | // }, 65 | // { 66 | // selector: 'typeProperty', 67 | // format: null, 68 | // }, 69 | // { 70 | // selector: ['interface', 'typeLike'], 71 | // format: ['PascalCase'], 72 | // }, 73 | // { 74 | // selector: 'variable', 75 | // modifiers: ['const'], 76 | // format: ['camelCase', 'UPPER_CASE'], 77 | // leadingUnderscore: 'allow', 78 | // }, 79 | // { 80 | // selector: 'enumMember', 81 | // format: ['UPPER_CASE'], 82 | // }, 83 | // ], 84 | 'require-await': ['error'], 85 | 'capitalized-comments': 'off', 86 | 'no-control-regex': 'off', 87 | 'no-empty': 'warn', 88 | 'no-shadow': 'off', 89 | 'prefer-arrow/prefer-arrow-functions': [ 90 | 'warn', 91 | { 92 | disallowPrototype: true, 93 | singleReturnOnly: false, 94 | classPropertiesAllowed: false, 95 | }, 96 | ], 97 | // 'array-element-newline': ['error', 'consistent'], 98 | 'arrow-body-style': 'off', 99 | // 'max-len': [ 100 | // 'error', 101 | // { 102 | // code: 120, 103 | // ignoreComments: true, 104 | // ignorePattern: '^import .*$', 105 | // ignoreStrings: true, 106 | // ignoreTemplateLiterals: true, 107 | // ignoreRegExpLiterals: true, 108 | // }, 109 | // ], 110 | 'max-statements-per-line': 'error', 111 | 'no-case-declarations': 'off', 112 | 'no-constant-condition': 'warn', 113 | }, 114 | }; 115 | -------------------------------------------------------------------------------- /momoka-node/.nvmrc: -------------------------------------------------------------------------------- 1 | v18.12.1 2 | -------------------------------------------------------------------------------- /momoka-node/.prettierignore: -------------------------------------------------------------------------------- 1 | *.yml 2 | *.yaml 3 | *.md 4 | -------------------------------------------------------------------------------- /momoka-node/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /momoka-node/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unreleased - fill in any changes you do here! 2 | 3 | ## Features 4 | 5 | ## Bug fixes 6 | 7 | ## Breaking changes 8 | 9 | ################################################################### 10 | 11 | # 2.0.0 - 18th December 2023 12 | 13 | # Features 14 | 15 | - feat: add support for lens v2 publications 16 | - fix: export MomokaProvider and MomokaActionTypes 17 | 18 | # 1.1.2 - 24th May 2023 19 | 20 | ## Bug fixes 21 | 22 | - fix: uninstall lib-curl 23 | 24 | # 1.1.1 - 24th May 2023 25 | 26 | # Bug fixes 27 | 28 | - fix: remove lib-curl and use axios instead on node server to avoid some OS issues 29 | 30 | # 1.0.0>1.1.0 - 26th April 2023 31 | 32 | ## Features 33 | 34 | - feat: Initial release 1.0.0 35 | 36 | ## Fix 37 | 38 | - fix: Avoid installing `only-allow` during `preinstall` step 39 | -------------------------------------------------------------------------------- /momoka-node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/typescript-node:18 as base 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | COPY tsconfig.json ./ 7 | COPY src ./src 8 | COPY .env ./ 9 | 10 | # Enable corepack which would install correct version of pnpm 11 | RUN corepack enable 12 | 13 | # Install project dependencies using pnpm 14 | RUN npx pnpm install 15 | 16 | RUN chown -R node:node /usr/src/app 17 | 18 | USER node 19 | 20 | # Build the project 21 | RUN npm run build 22 | 23 | EXPOSE 3008 24 | 25 | CMD ["npm", "run", "start"] 26 | -------------------------------------------------------------------------------- /momoka-node/Dockerfile.stable: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | # Install pnpm through corepack 4 | RUN apk update 5 | RUN apk add --no-cache libc6-compat 6 | RUN corepack enable 7 | RUN corepack prepare pnpm@latest --activate 8 | 9 | # Specify a path for pnpm global root 10 | ENV PNPM_HOME=/usr/local/bin 11 | 12 | FROM base as runtime 13 | 14 | # Install stable version of momoka 15 | RUN pnpm add -g @lens-protocol/momoka 16 | 17 | # Run using the default shell, this is important to infer the environment variables 18 | CMD ["sh", "-c", "momoka --node $NODE_URL --environment=$ENVIRONMENT --concurrency=$CONCURRENCY --fromHead=true"] -------------------------------------------------------------------------------- /momoka-node/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Aave companies 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /momoka-node/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../lib/client/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /momoka-node/codegen.yaml: -------------------------------------------------------------------------------- 1 | schema: https://lens.bundlr.network/graphql 2 | documents: './src/graphql/*.graphql' 3 | generates: 4 | ./src/graphql/generated.ts: 5 | plugins: 6 | - typescript 7 | - typescript-operations 8 | - typed-document-node 9 | config: 10 | fetcher: fetch -------------------------------------------------------------------------------- /momoka-node/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | collectCoverage: true, 6 | coverageDirectory: 'coverage', 7 | setupFiles: ['/src/__TESTS__/config/jest.setup.js'], 8 | coveragePathIgnorePatterns: [ 9 | 'node_modules', 10 | '/src/__TESTS__', 11 | '.mock.ts', 12 | '/src/logger.ts', 13 | '/src/arweave', 14 | '/src/bundlr', 15 | ], 16 | testPathIgnorePatterns: ['/node_modules/', '/lib/', '/playground-browser/'], 17 | verbose: true, 18 | }; 19 | -------------------------------------------------------------------------------- /momoka-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lens-protocol/momoka", 3 | "version": "2.1.1", 4 | "description": "Momoka node for the Lens protocol", 5 | "main": "lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "exports": { 8 | ".": "./lib/index.js", 9 | "./client": "./lib/client/index.js", 10 | "./package.json": "./package.json" 11 | }, 12 | "files": [ 13 | "lib/**/*", 14 | "client/**/*", 15 | "images/**/*", 16 | "README.md" 17 | ], 18 | "scripts": { 19 | "build": "tsc", 20 | "build:watch": "tsc --watch", 21 | "eslint:fix": "eslint \"src/**/*.ts\" --quiet --fix", 22 | "eslint": "eslint \"src/**/*.ts\" --quiet", 23 | "start:fork": "REQ_TIMEOUT=100000 anvil --fork-url NODE_URL --silent", 24 | "start": "env-cmd -f .env node lib/runnable/da-verifier-node.runnable.js", 25 | "start:failed": "env-cmd -f .env ts-node src/failed-submissons.runnable.ts", 26 | "debug:playground": "npm run build && env-cmd -f .env node lib/__PLAYGROUND__/index.js", 27 | "generate": "graphql-codegen", 28 | "test": "npm run build && env-cmd -f .env.test jest", 29 | "lint": "pnpm run prettier && pnpm run tsc", 30 | "lint:fix": "pnpm run prettier:fix && pnpm run eslint", 31 | "prettier:fix": "prettier --write .", 32 | "prettier": "prettier --check .", 33 | "tsc": "tsc --noEmit", 34 | "prepublishOnly": "pnpm run build", 35 | "publish": "pnpm publish --access public" 36 | }, 37 | "devDependencies": { 38 | "@graphql-codegen/cli": "2.11.3", 39 | "@graphql-codegen/typed-document-node": "2.3.2", 40 | "@graphql-codegen/typescript": "2.7.2", 41 | "@graphql-codegen/typescript-operations": "2.5.2", 42 | "@types/bluebird": "^3.5.38", 43 | "@types/jest": "^29.2.6", 44 | "@types/yargs": "^17.0.22", 45 | "@typescript-eslint/eslint-plugin": "^5.49.0", 46 | "@typescript-eslint/parser": "^5.49.0", 47 | "env-cmd": "^10.1.0", 48 | "eslint": "^7.28.0", 49 | "eslint-config-prettier": "^8.5.0", 50 | "eslint-import-resolver-typescript": "^3.5.3", 51 | "eslint-plugin-import": "npm:eslint-plugin-i@^2.26.0-2", 52 | "eslint-plugin-prefer-arrow": "^1.2.3", 53 | "eslint-plugin-prettier": "^3.4.0", 54 | "ethereum-abi-types-generator": "^1.3.2", 55 | "graphql-codegen": "^0.4.0", 56 | "jest": "^29.4.0", 57 | "ts-jest": "^29.0.5", 58 | "ts-node": "10.9.1", 59 | "typescript": "^4.6.4" 60 | }, 61 | "dependencies": { 62 | "@ethersproject/abstract-provider": "^5.7.0", 63 | "@ethersproject/bytes": "^5.7.0", 64 | "@graphql-typed-document-node/core": "^3.1.2", 65 | "@urql/core": "^3.0.5", 66 | "arweave": "^1.13.3", 67 | "axios": "^1.3.4", 68 | "bluebird": "^3.7.2", 69 | "ethereum-multicall": "^2.24.0", 70 | "ethers": "^5.7.2", 71 | "graphql": "^16.6.0", 72 | "level": "^8.0.0", 73 | "yargs": "^17.7.1" 74 | }, 75 | "license": "MIT", 76 | "bin": { 77 | "momoka": "./lib/bin/cli.js" 78 | }, 79 | "repository": { 80 | "type": "git", 81 | "url": "git+https://github.com/lens-protocol/momoka.git" 82 | }, 83 | "packageManager": "pnpm@8.4.0" 84 | } 85 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = function override(config) { 2 | // required to support .cjs extension which is used by axios inside the verifier library 3 | // see https://github.com/facebook/create-react-app/pull/12021 4 | // TODO: Build verifier client as a ES module 5 | config.module.rules = config.module.rules.map((rule) => { 6 | if (rule.oneOf instanceof Array) { 7 | rule.oneOf[rule.oneOf.length - 1].exclude = [ 8 | /\.(js|mjs|jsx|cjs|ts|tsx)$/, 9 | /\.html$/, 10 | /\.json$/, 11 | ]; 12 | } 13 | return rule; 14 | }); 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-browser", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.18.16", 11 | "@types/react": "^18.0.28", 12 | "@types/react-dom": "^18.0.11", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-scripts": "5.0.1", 16 | "typescript": "^4.9.5", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "devDependencies": { 20 | "react-app-rewired": "^2.2.1" 21 | }, 22 | "scripts": { 23 | "start": "react-app-rewired start", 24 | "build": "react-app-rewired build", 25 | "test": "react-app-rewired test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/momoka-node/playground-browser/public/favicon.ico -------------------------------------------------------------------------------- /momoka-node/playground-browser/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | React App 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/momoka-node/playground-browser/public/logo192.png -------------------------------------------------------------------------------- /momoka-node/playground-browser/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lens-protocol/momoka/00056b8227ddcc80ed55bf10cf9e4b17d90d794a/momoka-node/playground-browser/public/logo512.png -------------------------------------------------------------------------------- /momoka-node/playground-browser/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | checkDAProof, 3 | Deployment, 4 | Environment, 5 | EthereumNode, 6 | } from '@lens-protocol/data-availability-verifier/client'; 7 | import './App.css'; 8 | 9 | const ethereumNode: EthereumNode = { 10 | environment: Environment.AMOY, 11 | nodeUrl: 'INSERT_NODE_URL_HERE', 12 | deployment: Deployment.STAGING, 13 | }; 14 | 15 | const check = async () => { 16 | const result = await checkDAProof('VlPh9JdZ2SNcnWaqgHFRfycT8xpuoX2MR5LnI95f87w', ethereumNode); 17 | if (result.isSuccess()) { 18 | console.log('proof valid', result.successResult); 19 | return; 20 | } 21 | 22 | // it failed! 23 | console.error('proof invalid do something', result.failure); 24 | 25 | console.log(ethereumNode); 26 | }; 27 | 28 | check(); 29 | 30 | function App() { 31 | return ( 32 |
33 |
34 |

35 | Edit src/App.tsx and save to reload. 36 |

37 | 43 | Learn React 44 | 45 |
46 |
47 | ); 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 4 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 11 | } 12 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /momoka-node/playground-browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /momoka-node/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'src/**' 3 | # if required, exclude some directories 4 | - '!**/test/**' -------------------------------------------------------------------------------- /momoka-node/render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: worker 3 | name: momoka 4 | env: docker 5 | dockerfilePath: ./Dockerfile.stable 6 | -------------------------------------------------------------------------------- /momoka-node/src/__TESTS__/config/jest.setup.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | Object.defineProperty(globalThis, 'crypto', { 4 | value: { 5 | subtle: crypto.webcrypto.subtle, 6 | }, 7 | }); 8 | 9 | // eslint-disable-next-line no-undef 10 | jest.setTimeout(15000); 11 | -------------------------------------------------------------------------------- /momoka-node/src/__TESTS__/db.test.ts: -------------------------------------------------------------------------------- 1 | import { MomokaValidatorError, TxValidatedResult } from '..'; 2 | import { getBlockDb, getTxDb, saveBlockDb, saveTxDb, startDb } from '../input-output/db'; 3 | 4 | const random = () => Math.random().toString(36).substring(7); 5 | 6 | const txValidatedResult: TxValidatedResult = { 7 | success: false, 8 | proofTxId: random(), 9 | failureReason: MomokaValidatorError.BLOCK_CANT_BE_READ_FROM_NODE, 10 | dataAvailabilityResult: undefined, 11 | }; 12 | 13 | describe('db', () => { 14 | beforeAll(async () => { 15 | await startDb(); 16 | }); 17 | 18 | describe('getTxDb', () => { 19 | test('should return back false if tx does not exist', async () => { 20 | const txId = random(); 21 | const result = await getTxDb(txId); 22 | expect(result).toBeNull(); 23 | }); 24 | 25 | test('should return back true if tx exists', async () => { 26 | const txId = random(); 27 | await saveTxDb(txId, txValidatedResult); 28 | const result = await getTxDb(txId); 29 | expect(result).toEqual(txValidatedResult); 30 | }); 31 | }); 32 | 33 | describe('saveTxDb', () => { 34 | test('should save to db', async () => { 35 | const txId = random(); 36 | await saveTxDb(txId, txValidatedResult); 37 | const result = await getTxDb(txId); 38 | expect(result).toEqual(txValidatedResult); 39 | }); 40 | }); 41 | 42 | describe('getBlockDb + saveBlockDb', () => { 43 | test('should return null if nothing found', async () => { 44 | const ran = random(); 45 | const result = await getBlockDb(ran as any); 46 | expect(result).toEqual(null); 47 | }); 48 | 49 | test('should return value if found', async () => { 50 | const ran = random(); 51 | const block = { number: ran as any } as any; 52 | 53 | await saveBlockDb(block); 54 | 55 | const result = await getBlockDb(ran as any); 56 | expect(result).toEqual(block); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /momoka-node/src/__TESTS__/publications/publication.base.test.ts: -------------------------------------------------------------------------------- 1 | import { whoSignedTypedData } from '../../proofs/publications/publication.base'; 2 | 3 | describe('publication base', () => { 4 | describe('whoSignedTypedData', () => { 5 | test('should return back correct address', async () => { 6 | const typedData = { 7 | types: { 8 | CommentWithSig: [ 9 | { 10 | name: 'profileId', 11 | type: 'uint256', 12 | }, 13 | { 14 | name: 'contentURI', 15 | type: 'string', 16 | }, 17 | { 18 | name: 'profileIdPointed', 19 | type: 'uint256', 20 | }, 21 | { 22 | name: 'pubIdPointed', 23 | type: 'uint256', 24 | }, 25 | { 26 | name: 'referenceModuleData', 27 | type: 'bytes', 28 | }, 29 | { 30 | name: 'collectModule', 31 | type: 'address', 32 | }, 33 | { 34 | name: 'collectModuleInitData', 35 | type: 'bytes', 36 | }, 37 | { 38 | name: 'referenceModule', 39 | type: 'address', 40 | }, 41 | { 42 | name: 'referenceModuleInitData', 43 | type: 'bytes', 44 | }, 45 | { 46 | name: 'nonce', 47 | type: 'uint256', 48 | }, 49 | { 50 | name: 'deadline', 51 | type: 'uint256', 52 | }, 53 | ], 54 | }, 55 | domain: { 56 | name: 'Lens Protocol Profiles', 57 | version: '1', 58 | chainId: 80001, 59 | verifyingContract: '0x60Ae865ee4C725cd04353b5AAb364553f56ceF82', 60 | }, 61 | value: { 62 | profileId: '0x18', 63 | profileIdPointed: '0x18', 64 | pubIdPointed: '0x3a', 65 | contentURI: 'ar://IZHY6SC77JtgMpuKmzbecgKV9HACdMtYb6a0lY1xL14', 66 | referenceModule: '0x0000000000000000000000000000000000000000', 67 | collectModule: '0x5E70fFD2C6D04d65C3abeBa64E93082cfA348dF8', 68 | collectModuleInitData: '0x', 69 | referenceModuleInitData: '0x', 70 | referenceModuleData: '0x', 71 | nonce: 243, 72 | deadline: 1674751411, 73 | }, 74 | }; 75 | 76 | const result = await whoSignedTypedData( 77 | typedData.domain, 78 | typedData.types, 79 | typedData.value, 80 | '0x56b1108c5c15aa3344fe87aa0a5b2d827625de8a7283e5cdb21088e30d49cd4203088a336bc9c0fac5cca0a84af84b7227be24e0bef55ece423f25015e807cd71b' 81 | ); 82 | 83 | expect(result.isSuccess()).toEqual(true); 84 | if (result.isSuccess()) { 85 | expect(result.successResult).toEqual('0xD8c789626CDb461ec9347f26DDbA98F9383aa457'); 86 | } 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /momoka-node/src/__TESTS__/random.test.ts: -------------------------------------------------------------------------------- 1 | import { getBundlrByIdAPI } from '../input-output/bundlr/get-bundlr-by-id.api'; 2 | import { LibCurlProvider } from '../input-output/lib-curl-provider'; 3 | 4 | describe('random', () => { 5 | it('getBundlrByIdAPI', async () => { 6 | const txId = 'oWnpbkMpnGxMMnFDxnwxCQVhEK55jJeuiyLGUv2bSrk'; 7 | await getBundlrByIdAPI(txId, { provider: new LibCurlProvider() }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /momoka-node/src/__TESTS__/submitters.test.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from '../common/environment'; 2 | import { getParamOrExit } from '../common/helpers'; 3 | import { EthereumNode } from '../evm/ethereum'; 4 | import { getOwnerOfTransactionAPI } from '../input-output/bundlr/get-owner-of-transaction.api'; 5 | 6 | import { getSubmitters, isValidSubmitter } from '../submitters'; 7 | 8 | jest.mock('../input-output/bundlr/get-owner-of-transaction.api'); 9 | 10 | export const mockGetOwnerOfTransactionAPI = getOwnerOfTransactionAPI as jest.MockedFunction< 11 | typeof getOwnerOfTransactionAPI 12 | >; 13 | 14 | const ethereumNode: EthereumNode = { 15 | environment: getParamOrExit('ETHEREUM_NETWORK') as Environment, 16 | nodeUrl: getParamOrExit('NODE_URL'), 17 | }; 18 | 19 | describe('submitters', () => { 20 | describe('getSubmitters', () => { 21 | it('should return 1 submitter', () => { 22 | expect(getSubmitters(ethereumNode.environment)).toHaveLength(1); 23 | }); 24 | }); 25 | 26 | describe('isValidSubmitter', () => { 27 | it('should return false with an invalid submitter', () => { 28 | expect(isValidSubmitter(ethereumNode.environment, '111')).toEqual(false); 29 | }); 30 | 31 | it('should return true with an valid submitter', () => { 32 | const submitter = getSubmitters(ethereumNode.environment)[0]; 33 | expect(isValidSubmitter(ethereumNode.environment, submitter)).toEqual(true); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /momoka-node/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from 'yargs'; 4 | import { Deployment, Environment } from '../common/environment'; 5 | import { turnedOffExperimentalWarning } from '../common/helpers'; 6 | import { startDAVerifierNode } from '../watchers/verifier.watcher'; 7 | 8 | turnedOffExperimentalWarning(); 9 | 10 | interface ProgramOptions { 11 | command: string; 12 | subcommands: string[]; 13 | options: { [key: string]: string }; 14 | } 15 | 16 | const getProgramArguments = (): ProgramOptions => { 17 | // tslint:disable-next-line: typedef 18 | const { 19 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 20 | // @ts-ignore 21 | _: [command, ...subcommands], 22 | ...options 23 | } = yargs.argv; 24 | return { 25 | command, 26 | options: Object.keys(options).reduce((r, v) => { 27 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 28 | // @ts-ignore 29 | r[v] = options[v]; 30 | return r; 31 | }, {}), 32 | subcommands, 33 | }; 34 | }; 35 | 36 | const args = getProgramArguments(); 37 | 38 | const nodeUrl = args.options.node; 39 | if (!nodeUrl) { 40 | console.log('No node url specified'); 41 | process.exit(1); 42 | } 43 | const environment = (args.options.environment as Environment) || Environment.POLYGON; 44 | const deployment = (args.options.deployment as Deployment) || Deployment.PRODUCTION; 45 | 46 | const concurrencyRaw = args.options.concurrency; 47 | const concurrency = concurrencyRaw ? Number(concurrencyRaw) : 100; 48 | 49 | const resyncRaw = args.options.resync; 50 | const resync = resyncRaw === 'true'; 51 | 52 | startDAVerifierNode( 53 | { 54 | nodeUrl, 55 | environment, 56 | deployment, 57 | }, 58 | concurrency, 59 | { resync } 60 | ).catch((error) => { 61 | console.error('momoka node failed to startup', error); 62 | process.exitCode = 1; 63 | }); 64 | -------------------------------------------------------------------------------- /momoka-node/src/client/axios-provider.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { FetchProvider } from '../input-output/fetch-with-timeout'; 3 | 4 | export class AxiosProvider implements FetchProvider { 5 | async get(url: string, { timeout }: { timeout: number }): Promise { 6 | const response = await axios.get(url, { 7 | timeout, 8 | }); 9 | 10 | return response.data as TResponse; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /momoka-node/src/client/check-da-proof-client.ts: -------------------------------------------------------------------------------- 1 | import { EthereumNode } from '../evm/ethereum'; 2 | import { 3 | CheckDASubmissionOptions, 4 | getDefaultCheckDASubmissionOptions, 5 | } from '../proofs/models/check-da-submisson-options'; 6 | import { PromiseWithContextResult } from '../data-availability-models/da-result'; 7 | import { 8 | DAEventType, 9 | DAStructurePublication, 10 | PublicationTypedData, 11 | } from '../data-availability-models/publications/data-availability-structure-publication'; 12 | import { ClientDaProofGateway } from './client-da-proof-gateway'; 13 | import { DaProofChecker } from '../proofs/da-proof-checker'; 14 | import { ClientDaProofVerifier } from './client-da-proof-verifier'; 15 | 16 | const gateway = new ClientDaProofGateway(); 17 | const verifier = new ClientDaProofVerifier(); 18 | const checker = new DaProofChecker(verifier, gateway); 19 | 20 | export const checkDAProof = ( 21 | txId: string, 22 | ethereumNode: EthereumNode, 23 | options: CheckDASubmissionOptions = getDefaultCheckDASubmissionOptions 24 | ): PromiseWithContextResult< 25 | DAStructurePublication | void, 26 | DAStructurePublication 27 | > => { 28 | return checker.checkDAProof(txId, ethereumNode, options); 29 | }; 30 | -------------------------------------------------------------------------------- /momoka-node/src/client/client-da-proof-gateway.ts: -------------------------------------------------------------------------------- 1 | import { DATimestampProofsResponse } from '../data-availability-models/data-availability-timestamp-proofs'; 2 | import { 3 | DAEventType, 4 | DAStructurePublication, 5 | PublicationTypedData, 6 | } from '../data-availability-models/publications/data-availability-structure-publication'; 7 | import { BlockInfo, EthereumNode, getBlock } from '../evm/ethereum'; 8 | import { getBundlrByIdAPI } from '../input-output/bundlr/get-bundlr-by-id.api'; 9 | import { TimeoutError } from '../input-output/common'; 10 | import { TxValidatedResult } from '../input-output/tx-validated-results'; 11 | import { DAProofsGateway } from '../proofs/da-proof-checker'; 12 | import { AxiosProvider } from './axios-provider'; 13 | 14 | export class ClientDaProofGateway implements DAProofsGateway { 15 | public getBlockRange(blockNumbers: number[], ethereumNode: EthereumNode): Promise { 16 | return Promise.all(blockNumbers.map((blockNumber) => getBlock(blockNumber, ethereumNode))); 17 | } 18 | 19 | public getDaPublication( 20 | txId: string 21 | ): Promise | TimeoutError | null> { 22 | return getBundlrByIdAPI>(txId, { 23 | provider: new AxiosProvider(), 24 | }); 25 | } 26 | 27 | public getTimestampProofs( 28 | timestampId: string 29 | ): Promise { 30 | return getBundlrByIdAPI(timestampId, { 31 | provider: new AxiosProvider(), 32 | }); 33 | } 34 | 35 | // No cache available in the client 36 | public getTxResultFromCache(): Promise { 37 | return Promise.resolve(null); 38 | } 39 | 40 | // No signature usage in the client 41 | public hasSignatureBeenUsedBefore(_signature: string): Promise { 42 | return Promise.resolve(false); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /momoka-node/src/client/client-da-proof-verifier.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers'; 2 | import Arweave from 'arweave/web'; 3 | import deepHash from 'arweave/web/lib/deepHash'; 4 | import { b64UrlToBuffer } from 'arweave/web/lib/utils'; 5 | 6 | import { 7 | DAEventType, 8 | DAStructurePublication, 9 | PublicationTypedData, 10 | } from '../data-availability-models/publications/data-availability-structure-publication'; 11 | import { deepClone } from '../common/helpers'; 12 | import { DAProofsVerifier } from '../proofs/da-proof-checker'; 13 | import { AxiosProvider } from './axios-provider'; 14 | import { Deployment, Environment } from '../common/environment'; 15 | import { LogFunctionType } from '../common/logger'; 16 | import { TimeoutError, TIMEOUT_ERROR } from '../input-output/common'; 17 | import { getOwnerOfTransactionAPI } from '../input-output/bundlr/get-owner-of-transaction.api'; 18 | import { isValidSubmitter } from '../submitters'; 19 | import { verifyReceipt } from '../proofs/utils'; 20 | 21 | export class ClientDaProofVerifier implements DAProofsVerifier { 22 | extractAddress( 23 | daPublication: DAStructurePublication 24 | ): Promise { 25 | const signature = deepClone(daPublication.signature); 26 | 27 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 28 | // @ts-ignore 29 | // TODO: Is that important to remove signature from the shared object? 30 | delete daPublication.signature; 31 | 32 | return Promise.resolve(utils.verifyMessage(JSON.stringify(daPublication), signature)); 33 | } 34 | 35 | verifyTimestampSignature( 36 | daPublication: DAStructurePublication 37 | ): Promise { 38 | return verifyReceipt(daPublication.timestampProofs.response, { 39 | crypto: Arweave.crypto, 40 | deepHash, 41 | b64UrlToBuffer, 42 | stringToBuffer: Arweave.utils.stringToBuffer, 43 | }); 44 | } 45 | 46 | async verifyTransactionSubmitter( 47 | environment: Environment, 48 | txId: string, 49 | log: LogFunctionType, 50 | deployment?: Deployment 51 | ): Promise { 52 | const owner = await getOwnerOfTransactionAPI(txId, { provider: new AxiosProvider() }); 53 | if (owner === TIMEOUT_ERROR) { 54 | return TIMEOUT_ERROR; 55 | } 56 | if (!owner) { 57 | return false; 58 | } 59 | log('owner result', owner); 60 | return isValidSubmitter(environment, owner, deployment); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /momoka-node/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export { Deployment, Environment } from '../common/environment'; 2 | export { 3 | DAEventType, 4 | DAStructurePublication, 5 | PublicationTypedData, 6 | } from '../data-availability-models/publications/data-availability-structure-publication'; 7 | export { MomokaValidatorError } from '../data-availability-models/validator-errors'; 8 | export { EthereumNode } from '../evm/ethereum'; 9 | export { 10 | TxValidatedFailureResult, 11 | TxValidatedResult, 12 | TxValidatedSuccessResult, 13 | } from '../input-output/tx-validated-results'; 14 | export { checkDAProof } from './check-da-proof-client'; 15 | -------------------------------------------------------------------------------- /momoka-node/src/common/environment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum defining the supported Ethereum environments for the verifier node. 3 | */ 4 | export enum Environment { 5 | POLYGON = 'POLYGON', 6 | MUMBAI = 'MUMBAI', 7 | AMOY = 'AMOY', 8 | } 9 | 10 | /** 11 | * Enum defining the deployment configurations for the verifier node. 12 | */ 13 | export enum Deployment { 14 | PRODUCTION = 'PRODUCTION', 15 | STAGING = 'STAGING', 16 | LOCAL = 'LOCAL', 17 | } 18 | 19 | /** 20 | * Maps an Ethereum environment to its corresponding chain ID. 21 | * @param environment The Ethereum environment to map to a chain ID. 22 | * @returns The chain ID corresponding to the provided Ethereum environment. 23 | * @throws An error if the provided environment is invalid. 24 | */ 25 | export const environmentToChainId = (environment: Environment): 137 | 80001 | 80002 => { 26 | switch (environment) { 27 | case Environment.POLYGON: 28 | return 137; 29 | case Environment.MUMBAI: 30 | return 80001; 31 | case Environment.AMOY: 32 | return 80002; 33 | default: 34 | throw new Error('Invalid environment'); 35 | } 36 | }; 37 | 38 | /** 39 | * Maps an Ethereum environment to its corresponding Lens Hub contract address. 40 | * @param environment The Ethereum environment to map to a Lens Hub contract address. 41 | * @returns The Lens Hub contract address corresponding to the provided Ethereum environment. 42 | * @throws An error if the provided environment is invalid. 43 | */ 44 | export const environmentToLensHubContract = (environment: Environment): string => { 45 | switch (environment) { 46 | case Environment.POLYGON: 47 | return '0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d'; 48 | case Environment.MUMBAI: 49 | return '0x60Ae865ee4C725cd04353b5AAb364553f56ceF82'; 50 | case Environment.AMOY: 51 | return '0xA2574D9DdB6A325Ad2Be838Bd854228B80215148'; 52 | default: 53 | throw new Error('Invalid environment'); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /momoka-node/src/common/in-memory-store.ts: -------------------------------------------------------------------------------- 1 | export class InMemoryStore { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | private state: Map; 4 | 5 | constructor() { 6 | this.state = new Map(); 7 | } 8 | 9 | public set(key: string, value: T): void { 10 | this.state.set(key, value); 11 | } 12 | 13 | public get(key: string): TResponse | undefined { 14 | return this.state.get(key); 15 | } 16 | 17 | public has(key: string): boolean { 18 | return this.state.has(key); 19 | } 20 | 21 | public delete(key: string): boolean { 22 | return this.state.delete(key); 23 | } 24 | 25 | public clear(): void { 26 | this.state.clear(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /momoka-node/src/common/logger.ts: -------------------------------------------------------------------------------- 1 | export type LogFunctionType = (message: string, ...optionalParams: unknown[]) => void; 2 | 3 | export enum LoggerLevelColours { 4 | INFO = '\x1b[36m', 5 | SUCCESS = '\x1b[38;2;0;128;0m', 6 | ERROR = '\x1b[31m', 7 | WARNING = '\x1b[33m', 8 | } 9 | 10 | /** 11 | * A function to build a log message with the lens node footprint. 12 | * @param message The message 13 | */ 14 | export const buildLensNodeFootprintLog = (message: string): string => { 15 | return `[${new Date().toUTCString()}] LENS VERIFICATION NODE - ${message}`; 16 | }; 17 | 18 | /** 19 | * A function to log a message to the console with a info font color. 20 | * @param message - The message to be logged to the console. 21 | * @param optionalParams - An optional list of additional parameters to be logged to the console. 22 | */ 23 | export const consoleLog = (message: string, ...optionalParams: unknown[]): void => { 24 | console.log( 25 | LoggerLevelColours.INFO, 26 | `[${new Date().toUTCString()}] ${message}`, 27 | ...optionalParams 28 | ); 29 | }; 30 | 31 | /** 32 | * A function to log a message to the console with a info font color including the lens node footprint. 33 | * @param message - The message to be logged to the console. 34 | * @param optionalParams - An optional list of additional parameters to be logged to the console. 35 | */ 36 | export const consoleLogWithLensNodeFootprint = ( 37 | message: string, 38 | ...optionalParams: unknown[] 39 | ): void => { 40 | console.log(LoggerLevelColours.INFO, buildLensNodeFootprintLog(message), ...optionalParams); 41 | }; 42 | 43 | /** 44 | * A function to log a message to the console with a green font color. 45 | * @param message - The message to be logged to the console. 46 | * @param optionalParams - An optional list of additional parameters to be logged to the console. 47 | */ 48 | export const consoleSuccess = (message: string, ...optionalParams: unknown[]): void => { 49 | console.log( 50 | LoggerLevelColours.SUCCESS, 51 | `[${new Date().toUTCString()}] ${message}`, 52 | ...optionalParams 53 | ); 54 | }; 55 | 56 | /** 57 | * A function to log a error message to the console with a red font color. 58 | * @param message - The message to be logged to the console. 59 | * @param optionalParams - An optional list of additional parameters to be logged to the console. 60 | */ 61 | export const consoleError = (message: string, ...optionalParams: unknown[]): void => { 62 | console.log( 63 | LoggerLevelColours.ERROR, 64 | `[${new Date().toUTCString()}] ${message}`, 65 | ...optionalParams 66 | ); 67 | }; 68 | 69 | /** 70 | * A function to log a warning message to the console with a yellow font color. 71 | * @param message - The message to be logged to the console. 72 | * @param optionalParams - An optional list of additional parameters to be logged to the console. 73 | */ 74 | export const consoleWarn = (message: string, ...optionalParams: unknown[]): void => { 75 | console.log( 76 | LoggerLevelColours.WARNING, 77 | `[${new Date().toUTCString()}] ${message}`, 78 | ...optionalParams 79 | ); 80 | }; 81 | 82 | /** 83 | * A function to log a message to the console with a dyanmic font color. 84 | * @param message - The message to be logged to the console. 85 | * @param optionalParams - An optional list of additional parameters to be logged to the console. 86 | */ 87 | export const consoleDynamic = ( 88 | color: LoggerLevelColours, 89 | message: string, 90 | ...optionalParams: unknown[] 91 | ): void => { 92 | console.log(color, `[${new Date().toUTCString()}] ${message}`, ...optionalParams); 93 | }; 94 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/da-result.ts: -------------------------------------------------------------------------------- 1 | import { MomokaValidatorError } from './validator-errors'; 2 | 3 | class Success { 4 | public constructor(public readonly successResult: TSuccess) {} 5 | 6 | public isSuccess(): this is Success { 7 | return true; 8 | } 9 | 10 | public isFailure(): this is Failure { 11 | return false; 12 | } 13 | } 14 | 15 | class Failure { 16 | public constructor(public readonly failure: TError, public context?: TContext) {} 17 | 18 | public isSuccess(): this is Success { 19 | return false; 20 | } 21 | 22 | public isFailure(): this is Failure { 23 | return true; 24 | } 25 | } 26 | 27 | /** 28 | * Represents the result of a data availability check. 29 | * @template TSuccessResult The type of the successful result. 30 | * @template TContext The type of the context, which is undefined by default. 31 | */ 32 | export type DAResult = 33 | | Success 34 | | Failure; 35 | 36 | /** 37 | * Represents a Promise of a data availability result. 38 | * @template TSuccessResult The type of the successful result. 39 | */ 40 | export type PromiseResult = Promise>; 41 | 42 | /** 43 | * Represents a data availability result. 44 | * @template TSuccessResult The type of the successful result. 45 | */ 46 | export type Result = DAResult; 47 | 48 | /** 49 | * Creates a successful data availability result. 50 | * @template TResult The type of the successful result. 51 | * @param result The successful result. 52 | * @returns The successful data availability result. 53 | */ 54 | export function success(): Success; 55 | export function success(result: T): Success; 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-arrow/prefer-arrow-functions 57 | export function success(result: any = undefined): Success { 58 | return new Success(result); 59 | } 60 | 61 | /** 62 | * Creates a failed data availability result. 63 | * @param error The claimable validator error in case of failure. 64 | * @returns The failed data availability result. 65 | */ 66 | export const failure = (error: MomokaValidatorError): Failure => 67 | new Failure(error); 68 | 69 | /** 70 | * Represents a Promise of a data availability result with context. 71 | * @template TSuccessResult The type of the successful result. 72 | * @template TContext The type of the context. 73 | */ 74 | export type PromiseWithContextResult = Promise< 75 | DAResult 76 | >; 77 | 78 | /** 79 | * Represents a Promise of a data availability result with optional context. 80 | * @template TSuccessResult The type of the successful result. 81 | * @template TContext The type of the context. 82 | */ 83 | export type PromiseWithContextResultOrNull = Promise | null>; 87 | 88 | /** 89 | * Creates a failed data availability result with context. 90 | * @template TContext The type of the context. 91 | * @param error The claimable validator error in case of failure. 92 | * @param context The context associated with the result. 93 | * @returns The failed data availability result with context. 94 | */ 95 | export const failureWithContext = ( 96 | error: MomokaValidatorError, 97 | context: TContext 98 | ): Failure => new Failure(error, context); 99 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/data-availability-action-types.ts: -------------------------------------------------------------------------------- 1 | export enum MomokaActionTypes { 2 | POST_CREATED = 'POST_CREATED', 3 | COMMENT_CREATED = 'COMMENT_CREATED', 4 | MIRROR_CREATED = 'MIRROR_CREATED', 5 | QUOTE_CREATED = 'QUOTE_CREATED', 6 | } 7 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/data-availability-provider.ts: -------------------------------------------------------------------------------- 1 | export enum MomokaProvider { 2 | BUNDLR = 'BUNDLR', 3 | } 4 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/data-availability-structure-base.ts: -------------------------------------------------------------------------------- 1 | import { MomokaActionTypes } from './data-availability-action-types'; 2 | import { DATimestampProofs } from './data-availability-timestamp-proofs'; 3 | 4 | export interface DAStructureBase { 5 | /** 6 | * The ID which links all the other proofs together 7 | */ 8 | dataAvailabilityId: string; 9 | 10 | /** 11 | * The signature of the entire payload by the submitter 12 | */ 13 | signature: string; 14 | 15 | /** 16 | * The DA action type 17 | */ 18 | type: MomokaActionTypes; 19 | 20 | /** 21 | * The timestamp proofs 22 | */ 23 | timestampProofs: DATimestampProofs; 24 | } 25 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/data-availability-timestamp-proofs.ts: -------------------------------------------------------------------------------- 1 | import { MomokaActionTypes } from './data-availability-action-types'; 2 | import { MomokaProvider } from './data-availability-provider'; 3 | import { DAPublicationsBatchResult } from './publications/data-availability-structure-publication'; 4 | 5 | export type DATimestampProofs = BundlrTimestampProofs; 6 | 7 | export interface BundlrUploadResponse { 8 | // The ID of the transaction 9 | id: string; 10 | // The Arweave public key of the node that received the transaction 11 | public: string; 12 | // The signature of this receipt 13 | signature: string; 14 | // response version 15 | version: '1.0.0'; 16 | // the maximum expected Arweave block height for transaction inclusion 17 | block: number; 18 | deadlineHeight: number; 19 | // List of validator signatures 20 | validatorSignatures: { address: string; signature: string }[]; 21 | // The UNIX (MS precision) timestamp of when the node received the Tx. Only optional if the upload receives a `201` error in response to a duplicate transaction upload. 22 | timestamp: number; 23 | } 24 | 25 | export interface BundlrTimestampProofs { 26 | /** 27 | * The proofs type 28 | */ 29 | type: MomokaProvider.BUNDLR; 30 | 31 | /** 32 | * The hash prefix may not change for 10 years but good to know! 33 | */ 34 | hashPrefix: '1'; 35 | /** 36 | * The response from bundlr including the timestamp 37 | */ 38 | response: BundlrUploadResponse; 39 | } 40 | 41 | export interface DATimestampProofsResponse { 42 | type: MomokaActionTypes; 43 | dataAvailabilityId: string; 44 | } 45 | 46 | export interface DAPublicationWithTimestampProofsBatchResult extends DAPublicationsBatchResult { 47 | timestampProofsData: DATimestampProofsResponse; 48 | } 49 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/data-availability-typed-data.ts: -------------------------------------------------------------------------------- 1 | export interface TypedDataField { 2 | name: string; 3 | type: string; 4 | } 5 | 6 | export interface TypedDataDomain { 7 | name: string; 8 | version: string; 9 | chainId: number; 10 | verifyingContract: string; 11 | } 12 | 13 | export interface EIP712TypedDataValueBase { 14 | nonce: number; 15 | deadline: number; 16 | } 17 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/publications/data-availability-publication-typed-data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EIP712TypedDataValueBase, 3 | TypedDataDomain, 4 | TypedDataField, 5 | } from '../data-availability-typed-data'; 6 | 7 | interface CreatePostV1EIP712TypedDataValue extends EIP712TypedDataValueBase { 8 | profileId: string; 9 | contentURI: string; 10 | collectModule: string; 11 | collectModuleInitData: string; 12 | referenceModule: string; 13 | referenceModuleInitData: string; 14 | } 15 | 16 | interface CreatePostV2EIP712TypedDataValue extends EIP712TypedDataValueBase { 17 | profileId: string; 18 | contentURI: string; 19 | actionModules: string[]; 20 | actionModulesInitDatas: string[]; 21 | referenceModule: string; 22 | referenceModuleInitData: string; 23 | } 24 | 25 | export interface CreatePostV1EIP712TypedData { 26 | types: { 27 | PostWithSig: Array; 28 | }; 29 | domain: TypedDataDomain; 30 | value: CreatePostV1EIP712TypedDataValue; 31 | } 32 | 33 | export interface CreatePostV2EIP712TypedData { 34 | types: { 35 | PostWithSig: Array; 36 | }; 37 | domain: TypedDataDomain; 38 | value: CreatePostV2EIP712TypedDataValue; 39 | } 40 | 41 | interface CreateCommentV1EIP712TypedDataValue extends EIP712TypedDataValueBase { 42 | profileId: string; 43 | contentURI: string; 44 | profileIdPointed: string; 45 | pubIdPointed: string; 46 | referenceModule: string; 47 | collectModule: string; 48 | collectModuleInitData: string; 49 | referenceModuleInitData: string; 50 | referenceModuleData: string; 51 | } 52 | 53 | interface CreateCommentV2EIP712TypedDataValue extends EIP712TypedDataValueBase { 54 | profileId: string; 55 | contentURI: string; 56 | pointedProfileId: string; 57 | pointedPubId: string; 58 | referrerProfileIds: string[]; 59 | referrerPubIds: string[]; 60 | referenceModuleData: string; 61 | actionModules: string[]; 62 | actionModulesInitDatas: string[]; 63 | referenceModule: string; 64 | referenceModuleInitData: string; 65 | } 66 | 67 | export interface CreateCommentV1EIP712TypedData { 68 | types: { 69 | CommentWithSig: Array; 70 | }; 71 | domain: TypedDataDomain; 72 | value: CreateCommentV1EIP712TypedDataValue; 73 | } 74 | 75 | export interface CreateCommentV2EIP712TypedData { 76 | types: { 77 | CommentWithSig: Array; 78 | }; 79 | domain: TypedDataDomain; 80 | value: CreateCommentV2EIP712TypedDataValue; 81 | } 82 | 83 | export interface CreateMirrorV1EIP712TypedDataValue extends EIP712TypedDataValueBase { 84 | profileId: string; 85 | profileIdPointed: string; 86 | pubIdPointed: string; 87 | referenceModuleData: string; 88 | referenceModule: string; 89 | referenceModuleInitData: string; 90 | } 91 | 92 | export interface CreateMirrorV2EIP712TypedDataValue extends EIP712TypedDataValueBase { 93 | profileId: string; 94 | pointedProfileId: string; 95 | pointedPubId: string; 96 | referenceModuleData: string; 97 | metadataURI: string; 98 | referrerPubIds: string[]; 99 | referrerProfileIds: string[]; 100 | } 101 | 102 | export interface CreateMirrorV1EIP712TypedData { 103 | types: { 104 | MirrorWithSig: Array; 105 | }; 106 | domain: TypedDataDomain; 107 | value: CreateMirrorV1EIP712TypedDataValue; 108 | } 109 | 110 | export interface CreateMirrorV2EIP712TypedData { 111 | types: { 112 | MirrorWithSig: Array; 113 | }; 114 | domain: TypedDataDomain; 115 | value: CreateMirrorV2EIP712TypedDataValue; 116 | } 117 | 118 | interface CreatQuoteV2EIP712TypedDataValue extends EIP712TypedDataValueBase { 119 | profileId: string; 120 | contentURI: string; 121 | pointedProfileId: string; 122 | pointedPubId: string; 123 | referrerProfileIds: string[]; 124 | referrerPubIds: string[]; 125 | referenceModuleData: string; 126 | actionModules: string[]; 127 | actionModulesInitDatas: string[]; 128 | referenceModule: string; 129 | referenceModuleInitData: string; 130 | } 131 | 132 | export interface CreateQuoteV2EIP712TypedData { 133 | types: { 134 | MirrorWithSig: Array; 135 | }; 136 | domain: TypedDataDomain; 137 | value: CreatQuoteV2EIP712TypedDataValue; 138 | } 139 | 140 | export type PublicationV1TypedData = 141 | | CreatePostV1EIP712TypedData 142 | | CreateCommentV1EIP712TypedData 143 | | CreateMirrorV1EIP712TypedData; 144 | 145 | export type PublicationV2TypedData = 146 | | CreatePostV2EIP712TypedData 147 | | CreateCommentV2EIP712TypedData 148 | | CreateMirrorV2EIP712TypedData 149 | | CreateQuoteV2EIP712TypedData; 150 | 151 | export type PublicationTypedData = PublicationV1TypedData | PublicationV2TypedData; 152 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/publications/data-availability-structure-publication.ts: -------------------------------------------------------------------------------- 1 | import { DAStructureBase } from '../data-availability-structure-base'; 2 | import { DAEventType } from './data-availability-structure-publications-events'; 3 | import { PublicationTypedData } from './data-availability-publication-typed-data'; 4 | 5 | export enum DAPublicationPointerType { 6 | ON_EVM_CHAIN = 'ON_EVM_CHAIN', 7 | ON_DA = 'ON_DA', 8 | } 9 | 10 | interface DAStructurePublicationProofs { 11 | thisPublication: { 12 | /** 13 | * The signature which can be submitted on that block to prove the publication would of passed 14 | */ 15 | signature: string; 16 | 17 | /** 18 | * The signature has been signed by a delegate/dispatcher 19 | */ 20 | signedByDelegate: boolean; 21 | 22 | /** 23 | * The signature deadline unix timestamp 24 | */ 25 | signatureDeadline: number; 26 | 27 | /** 28 | * The typed data that was signed 29 | */ 30 | typedData: TTypedData; 31 | 32 | /** 33 | * The block hash of the block that contains the proof and should be ran on 34 | */ 35 | blockHash: string; 36 | 37 | /** 38 | * The block timestamp the proof ran on 39 | */ 40 | blockTimestamp: number; 41 | 42 | /** 43 | * The block number at this point make sure hash matches (you fork on block number not on block hash) 44 | */ 45 | blockNumber: number; 46 | }; 47 | 48 | /** 49 | * This is the pointers proofs, if the publication is another DA publication then this will be the proofs of that publication 50 | */ 51 | pointer: { 52 | /** 53 | * The location of the pointer 54 | */ 55 | location: string; 56 | /** 57 | * This maps if the pointer type is a DA publication or on the evm chain 58 | */ 59 | type: DAPublicationPointerType; 60 | } | null; 61 | } 62 | 63 | // TODO: Refactor to discriminated union by `type` so narrowing works properly 64 | export interface DAStructurePublication< 65 | TEvent extends DAEventType = DAEventType, 66 | TTypedData extends PublicationTypedData = PublicationTypedData 67 | > extends DAStructureBase { 68 | /** 69 | * As close if not exactly the same as how the blockchain event was emitted. 70 | */ 71 | event: TEvent; 72 | 73 | /** 74 | * The proofs that can be verified on the blockchain. 75 | */ 76 | chainProofs: DAStructurePublicationProofs; 77 | 78 | /** 79 | * The unquie publication id should be used as the primary key in the database 80 | * and should be a build up of data availability id and the publication id 81 | */ 82 | publicationId: string; 83 | } 84 | 85 | export interface DAPublicationsBatchResult { 86 | id: string; 87 | daPublication: DAStructurePublication; 88 | submitter: string; 89 | } 90 | 91 | // TODO: This is done to avoid modifying a lot of files. Remove this when we have time as DAStructurePublication has these as default. 92 | export { DAEventType, PublicationTypedData }; 93 | -------------------------------------------------------------------------------- /momoka-node/src/data-availability-models/publications/data-availability-structure-publications-events.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PostCreatedEventEmittedResponse as PostCreatedEventEmittedResponseV1, 3 | MirrorCreatedEventEmittedResponse as MirrorCreatedEventEmittedResponseV1, 4 | CommentCreatedEventEmittedResponse as CommentCreatedEventEmittedResponseV1, 5 | } from '../../evm/abi-types/LensHubV1Events'; 6 | 7 | import { 8 | PostCreatedEventEmittedResponse as PostCreatedEventEmittedResponseV2, 9 | CommentCreatedEventEmittedResponse as CommentCreatedEventEmittedResponseV2, 10 | MirrorCreatedEventEmittedResponse as MirrorCreatedEventEmittedResponseV2, 11 | QuoteCreatedEventEmittedResponse as QuoteCreatedEventEmittedResponseV2, 12 | } from '../../evm/abi-types/LensHubV2Events'; 13 | 14 | export type DAPostCreatedEventEmittedResponseV1 = PostCreatedEventEmittedResponseV1; 15 | export type DAPostCreatedEventEmittedResponseV2 = PostCreatedEventEmittedResponseV2; 16 | export type DAPostCreatedEventEmittedResponse = 17 | | DAPostCreatedEventEmittedResponseV1 18 | | DAPostCreatedEventEmittedResponseV2; 19 | 20 | export type DACommentCreatedEventEmittedResponseV1 = CommentCreatedEventEmittedResponseV1; 21 | export type DACommentCreatedEventEmittedResponseV2 = CommentCreatedEventEmittedResponseV2; 22 | export type DACommentCreatedEventEmittedResponse = 23 | | DACommentCreatedEventEmittedResponseV1 24 | | DACommentCreatedEventEmittedResponseV2; 25 | 26 | export type DAMirrorCreatedEventEmittedResponseV1 = MirrorCreatedEventEmittedResponseV1; 27 | export type DAMirrorCreatedEventEmittedResponseV2 = MirrorCreatedEventEmittedResponseV2; 28 | export type DAMirrorCreatedEventEmittedResponse = 29 | | DAMirrorCreatedEventEmittedResponseV1 30 | | DAMirrorCreatedEventEmittedResponseV2; 31 | 32 | export type DAQuoteCreatedEventEmittedResponseV2 = QuoteCreatedEventEmittedResponseV2; 33 | export type DAQuoteCreatedEventEmittedResponse = DAQuoteCreatedEventEmittedResponseV2; 34 | 35 | export type DAEventTypeV1 = 36 | | DAPostCreatedEventEmittedResponseV1 37 | | DACommentCreatedEventEmittedResponseV1 38 | | DAMirrorCreatedEventEmittedResponseV1; 39 | 40 | export type DAEventTypeV2 = 41 | | DAPostCreatedEventEmittedResponseV2 42 | | DACommentCreatedEventEmittedResponseV2 43 | | DAMirrorCreatedEventEmittedResponseV2 44 | | DAQuoteCreatedEventEmittedResponseV2; 45 | 46 | export type DAEventType = DAEventTypeV1 | DAEventTypeV2; 47 | -------------------------------------------------------------------------------- /momoka-node/src/evm/jsonrpc-methods.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON RPC methods we use 3 | */ 4 | export enum JSONRPCMethods { 5 | eth_getBlockByNumber = 'eth_getBlockByNumber', 6 | eth_call = 'eth_call', 7 | eth_getBlockByHash = 'eth_getBlockByHash', 8 | anvil_reset = 'anvil_reset', 9 | } 10 | -------------------------------------------------------------------------------- /momoka-node/src/graphql/data-availability-transactions.graphql: -------------------------------------------------------------------------------- 1 | query DataAvailabilityTransactions( 2 | $owners: [String!] 3 | $limit: Int 4 | $after: String 5 | $order: SortOrder 6 | ) { 7 | transactions(owners: $owners, limit: $limit, after: $after, order: $order, hasTags: true) { 8 | edges { 9 | node { 10 | id 11 | address 12 | } 13 | cursor 14 | } 15 | pageInfo { 16 | endCursor 17 | hasNextPage 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /momoka-node/src/graphql/urql.client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@urql/core'; 2 | import { BUNDLR_NODE_GRAPHQL } from '../input-output/bundlr/bundlr-config'; 3 | 4 | export const client = createClient({ 5 | url: BUNDLR_NODE_GRAPHQL, 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | fetch: fetch as any, 8 | requestPolicy: 'network-only', 9 | }); 10 | -------------------------------------------------------------------------------- /momoka-node/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Deployment, Environment } from './common/environment'; 2 | export { 3 | DAEventType, 4 | DAStructurePublication, 5 | PublicationTypedData, 6 | } from './data-availability-models/publications/data-availability-structure-publication'; 7 | export { MomokaValidatorError } from './data-availability-models/validator-errors'; 8 | export { EthereumNode } from './evm/ethereum'; 9 | export { 10 | TxValidatedFailureResult, 11 | TxValidatedResult, 12 | TxValidatedSuccessResult, 13 | } from './input-output/tx-validated-results'; 14 | export { checkDAProof } from './proofs/check-da-proof'; 15 | export * from './submitters'; 16 | export * from './watchers/models/stream.type'; 17 | export { 18 | StartDATrustingIndexingRequest, 19 | startDATrustingIndexing, 20 | } from './watchers/trusting-indexing.watcher'; 21 | export { startDAVerifierNode } from './watchers/verifier.watcher'; 22 | export { MomokaActionTypes } from './data-availability-models/data-availability-action-types'; 23 | export { MomokaProvider } from './data-availability-models/data-availability-provider'; 24 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/arweave/get-arweave-by-id.api.ts: -------------------------------------------------------------------------------- 1 | import { AxiosProvider } from '../../client/axios-provider'; 2 | import { TimeoutError } from '../common'; 3 | import { fetchWithTimeout } from '../fetch-with-timeout'; 4 | 5 | /** 6 | * Retrieves data associated with a given transaction ID using the arweave gateway. 7 | * @param txId The transaction ID to retrieve data for. 8 | * @param options The options for the request. 9 | * @returns The data associated with the transaction, or `null` if the transaction cannot be found, or `TimeoutError` if the request times out. 10 | * @note This function is not used internally on the data availability node as it is too slow. Even so you can use this if you wish on a fork or anything else. 11 | */ 12 | export const getArweaveByIdAPI = (txId: string): Promise => { 13 | return fetchWithTimeout(`https://arweave.net/${txId}`, { provider: new AxiosProvider() }); 14 | }; 15 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/bundlr/bundlr-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The URL for the Lens Bundlr node. 3 | */ 4 | export const BUNDLR_NODE = 'https://lens.bundlr.network/'; 5 | 6 | /** 7 | * The URL for the GraphQL endpoint on the Lens Bundlr node. 8 | */ 9 | export const BUNDLR_NODE_GRAPHQL = `${BUNDLR_NODE}graphql`; 10 | 11 | /** 12 | * The base URL for transaction-related endpoints on the Lens Bundlr node. 13 | */ 14 | export const BUNDLR_NODE_TX = `${BUNDLR_NODE}tx/`; 15 | 16 | /** 17 | * The URL for the Lens Bundlr gateway. 18 | */ 19 | export const BUNDLR_GATEWAY = 'https://lens-gateway.bundlr.network/'; 20 | 21 | /** 22 | * The base URL for transaction-related endpoints on the Lens Bundlr gateway. 23 | */ 24 | export const BUNDLR_GATEWAY_TX = `${BUNDLR_GATEWAY}tx/`; 25 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/bundlr/get-bundlr-bulk-txs.api.ts: -------------------------------------------------------------------------------- 1 | import { retryWithTimeout } from '../../common/helpers'; 2 | import { TimeoutError, TIMEOUT_ERROR } from '../common'; 3 | import { postWithTimeout } from '../post-with-timeout'; 4 | import { BUNDLR_NODE } from './bundlr-config'; 5 | 6 | /** 7 | * The response format for the `getBundlrBulkTxsAPI` function when it is successful. 8 | */ 9 | export interface BundlrBulkTxSuccess { 10 | id: string; 11 | address: string; 12 | data: string; 13 | } 14 | 15 | /** 16 | * The response format for the `getBundlrBulkTxsAPI` function. 17 | */ 18 | export interface BundlrBulkTxsResponse { 19 | success: BundlrBulkTxSuccess[]; 20 | failed: Record; 21 | } 22 | 23 | /** 24 | * Sends a POST request to the Lens Bundlr API to retrieve data associated with multiple transaction IDs. 25 | * @param txIds The transaction IDs to retrieve data for. 26 | * @returns The data associated with the given transaction IDs, or `TimeoutError` if the request times out. 27 | */ 28 | export const getBundlrBulkTxsAPI = ( 29 | txIds: string[] 30 | ): Promise => { 31 | return retryWithTimeout( 32 | async () => { 33 | try { 34 | return await postWithTimeout( 35 | `${BUNDLR_NODE}bulk/txs/data`, 36 | txIds 37 | ); 38 | } catch (error) { 39 | return TIMEOUT_ERROR; 40 | } 41 | }, 42 | { maxRetries: 3, delayMs: 200 } 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/bundlr/get-bundlr-by-id.api.ts: -------------------------------------------------------------------------------- 1 | import { retryWithTimeout } from '../../common/helpers'; 2 | import { TIMEOUT_ERROR, TimeoutError } from '../common'; 3 | import { FetchProvider, fetchWithTimeout } from '../fetch-with-timeout'; 4 | import { BUNDLR_GATEWAY_TX } from './bundlr-config'; 5 | 6 | /** 7 | * Sends a GET request to the Lens Bundlr API to retrieve data associated with a given transaction ID. 8 | * @param txId The transaction ID to retrieve data for. 9 | * @param options The options for the request. 10 | * @returns The data associated with the given transaction ID, or `null` if the transaction cannot be found, or `TimeoutError` if the request times out. 11 | */ 12 | export const getBundlrByIdAPI = ( 13 | txId: string, 14 | { provider }: { provider: FetchProvider } 15 | ): Promise => { 16 | return retryWithTimeout( 17 | async () => { 18 | try { 19 | return await fetchWithTimeout(`${BUNDLR_GATEWAY_TX}${txId}/data`, { provider }); 20 | } catch (error) { 21 | return TIMEOUT_ERROR; 22 | } 23 | }, 24 | { maxRetries: 3, delayMs: 200 } 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/bundlr/get-data-availability-transactions.api.ts: -------------------------------------------------------------------------------- 1 | import { Deployment, Environment } from '../../common/environment'; 2 | import { DataAvailabilityTransactionsDocument } from '../../graphql/generated'; 3 | import { client } from '../../graphql/urql.client'; 4 | import { getSubmitters } from '../../submitters'; 5 | 6 | /** 7 | * The response format for the `getDataAvailabilityTransactionsAPI` function. 8 | */ 9 | export interface getDataAvailabilityTransactionsAPIResponse { 10 | edges: { 11 | node: { 12 | id: string; 13 | address: string; 14 | }; 15 | cursor: string; 16 | }[]; 17 | pageInfo: { 18 | hasNextPage: boolean; 19 | endCursor: string | null; 20 | }; 21 | } 22 | 23 | export enum DataAvailabilityTransactionsOrderTypes { 24 | ASC = 'ASC', 25 | DESC = 'DESC', 26 | } 27 | 28 | /** 29 | * Sends a query to the bundlr GraphQL API to retrieve data availability transactions. 30 | * @param environment The environment to retrieve transactions for. 31 | * @param deployment The deployment to retrieve transactions for, or `undefined` to retrieve transactions for all deployments. 32 | * @param cursor The cursor to use for paginating results, or `null` to retrieve the first page of results. 33 | * @returns The data availability transactions matching the given parameters. 34 | */ 35 | export const getDataAvailabilityTransactionsAPI = async ( 36 | environment: Environment, 37 | deployment: Deployment | undefined, 38 | cursor: string | null, 39 | order: DataAvailabilityTransactionsOrderTypes, 40 | limit = 1000 41 | ): Promise => { 42 | const result = await client 43 | .query(DataAvailabilityTransactionsDocument, { 44 | owners: getSubmitters(environment, deployment), 45 | after: cursor, 46 | order, 47 | // max fetch is 1000 48 | limit: limit <= 1000 ? limit : 1000, 49 | }) 50 | .toPromise(); 51 | 52 | if (!result.data) { 53 | throw new Error( 54 | 'No data returned from Bundlr GraphQL API - normally due to a network drop, will auto retry and 99.9% sort itself out when network is stable again.' 55 | ); 56 | } 57 | 58 | return result.data.transactions as getDataAvailabilityTransactionsAPIResponse; 59 | }; 60 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/bundlr/get-owner-of-transaction.api.ts: -------------------------------------------------------------------------------- 1 | import { retryWithTimeout } from '../../common/helpers'; 2 | import { TimeoutError } from '../common'; 3 | import { FetchProvider, fetchWithTimeout } from '../fetch-with-timeout'; 4 | import { BUNDLR_NODE_TX } from './bundlr-config'; 5 | 6 | /** 7 | * Information about a Lens Bundlr transaction. 8 | */ 9 | interface BundlrTx { 10 | id: string; 11 | currency: string; 12 | address: string; 13 | owner: string; 14 | signature: string; 15 | target: string; 16 | tags: { 17 | name: string; 18 | value: string; 19 | }[]; 20 | anchor: string; 21 | data_size: number; 22 | } 23 | 24 | /** 25 | * Sends a GET request to the Lens Bundlr API to retrieve the owner of a given transaction. 26 | * @param txId The ID of the transaction to retrieve the owner for. 27 | * @param options The options to use when sending the request. 28 | * @returns The owner of the transaction with the given ID, or `null` if the transaction cannot be found, or `TimeoutError` if the request times out. 29 | */ 30 | export const getOwnerOfTransactionAPI = ( 31 | txId: string, 32 | { provider }: { provider: FetchProvider } 33 | ): Promise => { 34 | return retryWithTimeout(async () => { 35 | const result = await fetchWithTimeout(`${BUNDLR_NODE_TX}${txId}`, { 36 | provider, 37 | }); 38 | if (!result) return null; 39 | return result.address; 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/common.ts: -------------------------------------------------------------------------------- 1 | export const TIMEOUT_ERROR = 'timeout'; 2 | export type TimeoutError = 'timeout'; 3 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/fetch-with-timeout.ts: -------------------------------------------------------------------------------- 1 | export interface FetchProvider { 2 | get: (url: string, options: { timeout: number }) => Promise; 3 | } 4 | 5 | export const fetchWithTimeout = ( 6 | url: string, 7 | { provider }: { provider: FetchProvider } 8 | ): Promise => { 9 | return provider.get(url, { timeout: 5000 }); 10 | }; 11 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/json-rpc-with-timeout.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { JSONRPCMethods } from '../evm/jsonrpc-methods'; 3 | 4 | // most RPC nodes rate limits is per second! 5 | export const RATE_LIMIT_TIME = 1000; 6 | 7 | export const JSONRPCWithTimeout = async ( 8 | url: string, 9 | method: JSONRPCMethods, 10 | params: unknown[], 11 | returnErrorData = false 12 | ): Promise => { 13 | const request = { 14 | id: 0, 15 | jsonrpc: '2.0', 16 | method, 17 | params, 18 | }; 19 | 20 | const response = await axios.post(url, JSON.stringify(request), { 21 | timeout: 5000, 22 | }); 23 | 24 | if (returnErrorData && response.data.error) { 25 | return response.data.error.data as TResponse; 26 | } 27 | 28 | return response.data.result as TResponse; 29 | }; 30 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/lib-curl-provider.ts: -------------------------------------------------------------------------------- 1 | // import { curly } from 'node-libcurl'; 2 | // import { FetchProvider } from './fetch-with-timeout'; 3 | 4 | // export class LibCurlProvider implements FetchProvider { 5 | // async get(url: string, { timeout }: { timeout: number }): Promise { 6 | // const { statusCode, data } = await curly.get(url, { 7 | // httpHeader: ['Content-Type: application/json'], 8 | // curlyResponseBodyParser: false, 9 | // followLocation: true, 10 | // timeout, 11 | // }); 12 | 13 | // if (statusCode !== 200) { 14 | // throw new Error(`Failed to fetch ${url}`); 15 | // } 16 | 17 | // return JSON.parse(data.toString()) as TResponse; 18 | // } 19 | // } 20 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/paths.ts: -------------------------------------------------------------------------------- 1 | export type PathType = { 2 | join(...paths: string[]): string; 3 | }; 4 | let _path: PathType | undefined; 5 | export const pathResolver = async (): Promise => { 6 | if (_path) return Promise.resolve(_path); 7 | 8 | const pathImport = await import('path'); 9 | 10 | return (_path = pathImport); 11 | }; 12 | 13 | let lensDAPathCache: string | undefined; 14 | export const lensDAPath = async (): Promise => { 15 | if (lensDAPathCache) return Promise.resolve(lensDAPathCache); 16 | 17 | const path = await pathResolver(); 18 | const result = path.join(process.cwd(), 'lens__da'); 19 | return (lensDAPathCache = result); 20 | }; 21 | 22 | let failedProofsPathCache: string | undefined; 23 | export const failedProofsPath = async (): Promise => { 24 | if (failedProofsPathCache) return Promise.resolve(failedProofsPathCache); 25 | 26 | const path = await pathResolver(); 27 | const result = path.join(process.cwd(), 'lens__da', 'failed-proofs'); 28 | return (failedProofsPathCache = result); 29 | }; 30 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/post-with-timeout.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const postWithTimeout = async ( 4 | url: string, 5 | body: TBody 6 | ): Promise => { 7 | const { status, data } = await axios.post(url, body, { 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | timeout: 5000, 12 | }); 13 | 14 | if (status !== 200) { 15 | throw new Error(`postWithTimeout: ${status} - ${data.toString()}`); 16 | } 17 | 18 | return data as TResponse; 19 | }; 20 | -------------------------------------------------------------------------------- /momoka-node/src/input-output/tx-validated-results.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DAEventType, 3 | DAStructurePublication, 4 | PublicationTypedData, 5 | } from '../data-availability-models/publications/data-availability-structure-publication'; 6 | import { MomokaValidatorError } from '../data-availability-models/validator-errors'; 7 | 8 | export type TxValidatedResult = TxValidatedFailureResult | TxValidatedSuccessResult; 9 | 10 | interface TxValidatedResultBase { 11 | proofTxId: string; 12 | success: TSuccess; 13 | dataAvailabilityResult: TDAStructurePublication; 14 | } 15 | 16 | export interface TxValidatedFailureResult 17 | extends TxValidatedResultBase< 18 | false, 19 | DAStructurePublication | undefined 20 | > { 21 | failureReason: MomokaValidatorError; 22 | extraErrorInfo?: string; 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 26 | export interface TxValidatedSuccessResult 27 | extends TxValidatedResultBase> {} 28 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/check-da-proof.ts: -------------------------------------------------------------------------------- 1 | import { EthereumNode } from '../evm/ethereum'; 2 | import { 3 | CheckDASubmissionOptions, 4 | getDefaultCheckDASubmissionOptions, 5 | } from './models/check-da-submisson-options'; 6 | import { PromiseWithContextResult } from '../data-availability-models/da-result'; 7 | import { 8 | DAEventType, 9 | DAStructurePublication, 10 | PublicationTypedData, 11 | } from '../data-availability-models/publications/data-availability-structure-publication'; 12 | import { DaProofVerifier } from './da-proof-verifier'; 13 | import { DaProofChecker } from './da-proof-checker'; 14 | import { DAPublicationWithTimestampProofsBatchResult } from '../data-availability-models/data-availability-timestamp-proofs'; 15 | import { DaProofGateway } from './da-proof-gateway'; 16 | 17 | const gateway = new DaProofGateway(); 18 | const verifier = new DaProofVerifier(); 19 | const checker = new DaProofChecker(verifier, gateway); 20 | 21 | /** 22 | * Validates a data availability proof of a given transaction on the Arweave network, including the timestamp proofs. 23 | * @param txId The transaction ID to check. 24 | * @param ethereumNode The Ethereum node to use to validate the data availability proof. 25 | * @param options The options for validating the data availability proof. 26 | * @returns A `Promise` that resolves to a `PromiseResult` containing the validated data availability proof, or `void` if the validation fails. 27 | */ 28 | export const checkDAProof = ( 29 | txId: string, 30 | ethereumNode: EthereumNode, 31 | options: CheckDASubmissionOptions = getDefaultCheckDASubmissionOptions 32 | ): PromiseWithContextResult< 33 | DAStructurePublication | void, 34 | DAStructurePublication 35 | > => { 36 | return checker.checkDAProof(txId, ethereumNode, options); 37 | }; 38 | 39 | /** 40 | * Checks a data availability proof with metadata, including the timestamp proofs and transaction ID. 41 | * If the proof has already been checked, returns the previous result. 42 | * If the submitter is invalid, returns an error. 43 | * Otherwise, runs the internal proof check and returns the result. 44 | * @param txId The transaction ID associated with the proof. 45 | * @param daPublicationWithTimestampProofs The data availability publication with associated timestamp proofs. 46 | * @param ethereumNode The Ethereum node to use for validation. 47 | * @param options Optional options for the check, including logging and pointer verification. 48 | * @returns A context result with the validated publication, or an error if validation fails. 49 | */ 50 | export const checkDAProofWithMetadata = ( 51 | txId: string, 52 | daPublicationWithTimestampProofs: DAPublicationWithTimestampProofsBatchResult, 53 | ethereumNode: EthereumNode, 54 | options: CheckDASubmissionOptions = getDefaultCheckDASubmissionOptions 55 | ): PromiseWithContextResult< 56 | DAStructurePublication, 57 | DAStructurePublication 58 | > => { 59 | return checker.checkDAProofWithMetadata( 60 | txId, 61 | daPublicationWithTimestampProofs, 62 | ethereumNode, 63 | options 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/da-proof-gateway.ts: -------------------------------------------------------------------------------- 1 | import { AxiosProvider } from '../client/axios-provider'; 2 | import { DATimestampProofsResponse } from '../data-availability-models/data-availability-timestamp-proofs'; 3 | import { 4 | DAEventType, 5 | DAStructurePublication, 6 | PublicationTypedData, 7 | } from '../data-availability-models/publications/data-availability-structure-publication'; 8 | import { BlockInfo, EthereumNode, getBlock } from '../evm/ethereum'; 9 | import { getBundlrByIdAPI } from '../input-output/bundlr/get-bundlr-by-id.api'; 10 | import { TimeoutError } from '../input-output/common'; 11 | import { 12 | getBlockDb, 13 | getTxDAMetadataDb, 14 | getTxDb, 15 | getTxTimestampProofsMetadataDb, 16 | hasSignatureBeenUsedBeforeDb, 17 | saveBlockDb, 18 | } from '../input-output/db'; 19 | import { TxValidatedResult } from '../input-output/tx-validated-results'; 20 | import { DAProofsGateway } from '../proofs/da-proof-checker'; 21 | 22 | export class DaProofGateway implements DAProofsGateway { 23 | public hasSignatureBeenUsedBefore(signature: string): Promise { 24 | return hasSignatureBeenUsedBeforeDb(signature); 25 | } 26 | 27 | public getTxResultFromCache(txId: string): Promise { 28 | // Check if the transaction ID exists in the database 29 | return getTxDb(txId); 30 | } 31 | 32 | public getBlockRange(blockNumbers: number[], ethereumNode: EthereumNode): Promise { 33 | return Promise.all( 34 | blockNumbers.map(async (blockNumber) => { 35 | const cachedBlock = await getBlockDb(blockNumber); 36 | if (cachedBlock) { 37 | return cachedBlock; 38 | } 39 | 40 | const block = await getBlock(blockNumber, ethereumNode); 41 | 42 | // fire and forget! 43 | saveBlockDb(block); 44 | 45 | return block; 46 | }) 47 | ); 48 | } 49 | 50 | public async getDaPublication( 51 | txId: string 52 | ): Promise | TimeoutError | null> { 53 | return ( 54 | (await getTxDAMetadataDb(txId)) || 55 | (await getBundlrByIdAPI>(txId, { 56 | // use Axios for now as lib curl is causing some issues on different OS 57 | provider: new AxiosProvider(), 58 | })) 59 | ); 60 | } 61 | 62 | public async getTimestampProofs( 63 | timestampId: string, 64 | txId: string 65 | ): Promise { 66 | return ( 67 | (await getTxTimestampProofsMetadataDb(txId)) || 68 | (await getBundlrByIdAPI(timestampId, { 69 | // use Axios for now as lib curl is causing some issues on different OS 70 | provider: new AxiosProvider(), 71 | })) 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/da-proof-verifier.ts: -------------------------------------------------------------------------------- 1 | import { AxiosProvider } from '../client/axios-provider'; 2 | import { Deployment, Environment } from '../common/environment'; 3 | import { deepClone } from '../common/helpers'; 4 | import { LogFunctionType } from '../common/logger'; 5 | import { 6 | DAEventType, 7 | DAStructurePublication, 8 | PublicationTypedData, 9 | } from '../data-availability-models/publications/data-availability-structure-publication'; 10 | import { getOwnerOfTransactionAPI } from '../input-output/bundlr/get-owner-of-transaction.api'; 11 | import { TIMEOUT_ERROR, TimeoutError } from '../input-output/common'; 12 | import { isValidSubmitter } from '../submitters'; 13 | import { HandlerWorkers } from '../workers/handler-communication.worker'; 14 | import { workerPool } from '../workers/worker-pool'; 15 | import { DAProofsVerifier } from './da-proof-checker'; 16 | 17 | export class DaProofVerifier implements DAProofsVerifier { 18 | public extractAddress( 19 | daPublication: DAStructurePublication 20 | ): Promise { 21 | const signature = deepClone(daPublication.signature); 22 | 23 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 24 | // @ts-ignore 25 | // TODO: Is that important to remove signature from the shared object? 26 | delete daPublication.signature; 27 | 28 | return workerPool.execute({ 29 | worker: HandlerWorkers.EVM_VERIFY_MESSAGE, 30 | data: { 31 | daPublication, 32 | signature, 33 | }, 34 | }); 35 | } 36 | 37 | public verifyTimestampSignature( 38 | daPublication: DAStructurePublication 39 | ): Promise { 40 | return workerPool.execute({ 41 | worker: HandlerWorkers.BUNDLR_VERIFY_RECEIPT, 42 | data: { 43 | bundlrUploadResponse: daPublication.timestampProofs.response, 44 | }, 45 | }); 46 | } 47 | 48 | public async verifyTransactionSubmitter( 49 | environment: Environment, 50 | txId: string, 51 | log: LogFunctionType, 52 | deployment?: Deployment 53 | ): Promise { 54 | // use Axios for now as lib curl is causing some issues on different OS 55 | const owner = await getOwnerOfTransactionAPI(txId, { provider: new AxiosProvider() }); 56 | if (owner === TIMEOUT_ERROR) { 57 | return TIMEOUT_ERROR; 58 | } 59 | 60 | log('owner result', owner); 61 | if (!owner) { 62 | return false; 63 | } 64 | 65 | return isValidSubmitter(environment, owner, deployment); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/models/check-da-submisson-options.ts: -------------------------------------------------------------------------------- 1 | import { LogFunctionType } from '../../common/logger'; 2 | 3 | export interface CheckDASubmissionOptions { 4 | verifyPointer: boolean; 5 | byPassDb: boolean; 6 | log: LogFunctionType; 7 | } 8 | 9 | export const getDefaultCheckDASubmissionOptions: CheckDASubmissionOptions = { 10 | // eslint-disable-next-line @typescript-eslint/no-empty-function 11 | log: () => {}, 12 | byPassDb: false, 13 | verifyPointer: true, 14 | }; 15 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/comment/da-comment-verifier-v1.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { DACommentCreatedEventEmittedResponseV1 } from '../../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { DAPublicationVerifierV1 } from '../da-publication-verifier-v1'; 4 | import { CreateCommentV1EIP712TypedData } from '../../../data-availability-models/publications/data-availability-publication-typed-data'; 5 | import { LogFunctionType } from '../../../common/logger'; 6 | import { failure, PromiseResult, success } from '../../../data-availability-models/da-result'; 7 | import { BigNumber } from 'ethers'; 8 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 9 | import { EMPTY_BYTE, EthereumNode } from '../../../evm/ethereum'; 10 | import { MomokaActionTypes } from '../../../data-availability-models/data-availability-action-types'; 11 | 12 | export type DACommentPublicationV1 = DAStructurePublication< 13 | DACommentCreatedEventEmittedResponseV1, 14 | CreateCommentV1EIP712TypedData 15 | >; 16 | 17 | export const isDACommentPublicationV1 = ( 18 | daPublication: DAStructurePublication 19 | ): daPublication is DACommentPublicationV1 => { 20 | return ( 21 | daPublication.type === MomokaActionTypes.COMMENT_CREATED && 22 | !('commentParams' in daPublication.event) 23 | ); 24 | }; 25 | 26 | export class DACommentVerifierV1 extends DAPublicationVerifierV1 { 27 | public readonly type = MomokaActionTypes.COMMENT_CREATED; 28 | 29 | constructor( 30 | public readonly daPublication: DACommentPublicationV1, 31 | ethereumNode: EthereumNode, 32 | log: LogFunctionType 33 | ) { 34 | super(daPublication, ethereumNode, log); 35 | } 36 | 37 | public verifyEventWithTypedData(pubCountAtBlock: string): PromiseResult { 38 | const event = this.daPublication.event; 39 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 40 | 41 | // compare all event emitted to typed data value 42 | this.log('cross check event with typed data value'); 43 | 44 | // check the pub count makes sense from the block! 45 | if (BigNumber.from(pubCountAtBlock).add(1).toHexString() !== event.pubId) { 46 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 47 | } 48 | 49 | this.log('pub count at block is correct'); 50 | 51 | // compare all others! 52 | if ( 53 | typedData.value.profileId !== event.profileId || 54 | typedData.value.contentURI !== event.contentURI || 55 | typedData.value.profileIdPointed !== event.profileIdPointed || 56 | typedData.value.pubIdPointed !== event.pubIdPointed || 57 | typedData.value.collectModule !== event.collectModule || 58 | event.collectModuleReturnData !== EMPTY_BYTE || 59 | typedData.value.referenceModule !== event.referenceModule || 60 | event.referenceModuleReturnData !== EMPTY_BYTE || 61 | typedData.value.collectModuleInitData !== EMPTY_BYTE || 62 | typedData.value.referenceModuleInitData !== EMPTY_BYTE 63 | ) { 64 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 65 | } 66 | 67 | this.log('cross check event is complete'); 68 | 69 | return Promise.resolve(success()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/comment/da-comment-verifier-v2.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { DACommentCreatedEventEmittedResponseV2 } from '../../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { CreateCommentV2EIP712TypedData } from '../../../data-availability-models/publications/data-availability-publication-typed-data'; 4 | import { LogFunctionType } from '../../../common/logger'; 5 | import { failure, PromiseResult, success } from '../../../data-availability-models/da-result'; 6 | import { BigNumber } from 'ethers'; 7 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 8 | import { DAPublicationVerifierV2 } from '../da-publication-verifier-v2'; 9 | import { generatePublicationId } from '../../utils'; 10 | import { MomokaActionTypes } from '../../../data-availability-models/data-availability-action-types'; 11 | import { EMPTY_BYTE, EthereumNode } from '../../../evm/ethereum'; 12 | import { arraysEqual } from '../../../utils/arrays-equal'; 13 | 14 | export type DACommentPublicationV2 = DAStructurePublication< 15 | DACommentCreatedEventEmittedResponseV2, 16 | CreateCommentV2EIP712TypedData 17 | >; 18 | 19 | export const isDACommentPublicationV2 = ( 20 | daPublication: DAStructurePublication 21 | ): daPublication is DACommentPublicationV2 => { 22 | return ( 23 | daPublication.type === MomokaActionTypes.COMMENT_CREATED && 24 | 'commentParams' in daPublication.event 25 | ); 26 | }; 27 | 28 | export class DACommentVerifierV2 extends DAPublicationVerifierV2 { 29 | public readonly type = MomokaActionTypes.COMMENT_CREATED; 30 | 31 | constructor( 32 | public readonly daPublication: DACommentPublicationV2, 33 | ethereumNode: EthereumNode, 34 | log: LogFunctionType 35 | ) { 36 | super(daPublication, ethereumNode, log); 37 | } 38 | 39 | verifyPublicationIdMatches(): PromiseResult { 40 | const generatedPublicationId = generatePublicationId( 41 | this.daPublication.event.commentParams.profileId, 42 | this.daPublication.event.pubId, 43 | this.daPublication.dataAvailabilityId 44 | ); 45 | 46 | if (generatedPublicationId !== this.daPublication.publicationId) { 47 | this.log('publicationId does not match the generated one'); 48 | 49 | return Promise.resolve(failure(MomokaValidatorError.GENERATED_PUBLICATION_ID_MISMATCH)); 50 | } 51 | 52 | return Promise.resolve(success()); 53 | } 54 | 55 | verifyEventWithTypedData(pubCountAtBlock: string): PromiseResult { 56 | const event = this.daPublication.event; 57 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 58 | 59 | // compare all event emitted to typed data value 60 | this.log('cross check event with typed data value'); 61 | 62 | // check the pub count makes sense from the block! 63 | if (BigNumber.from(pubCountAtBlock).add(1).toHexString() !== event.pubId) { 64 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 65 | } 66 | 67 | this.log('pub count at block is correct'); 68 | 69 | // compare all others! 70 | if ( 71 | typedData.value.profileId !== event.commentParams.profileId || 72 | typedData.value.contentURI !== event.commentParams.contentURI || 73 | typedData.value.pointedProfileId !== event.commentParams.pointedProfileId || 74 | typedData.value.pointedPubId !== event.commentParams.pointedPubId || 75 | !arraysEqual(typedData.value.actionModules, event.commentParams.actionModules) || 76 | !arraysEqual( 77 | typedData.value.actionModulesInitDatas, 78 | event.commentParams.actionModulesInitDatas 79 | ) || 80 | typedData.value.referenceModule !== event.commentParams.referenceModule || 81 | typedData.value.referenceModuleInitData !== event.commentParams.referenceModuleInitData || 82 | !arraysEqual(typedData.value.referrerProfileIds, event.commentParams.referrerProfileIds) || 83 | !arraysEqual(typedData.value.referrerPubIds, event.commentParams.referrerPubIds) || 84 | event.actionModulesInitReturnDatas.length !== 0 || 85 | event.referenceModuleReturnData !== EMPTY_BYTE || 86 | event.referenceModuleInitReturnData !== EMPTY_BYTE 87 | ) { 88 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 89 | } 90 | 91 | this.log('cross check event is complete'); 92 | 93 | return Promise.resolve(success()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/comment/index.ts: -------------------------------------------------------------------------------- 1 | import { LogFunctionType } from '../../../common/logger'; 2 | import { failure, PromiseResult } from '../../../data-availability-models/da-result'; 3 | import { DAPublicationPointerType } from '../../../data-availability-models/publications/data-availability-structure-publication'; 4 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 5 | import { EthereumNode } from '../../../evm/ethereum'; 6 | import { DaProofChecker } from '../../da-proof-checker'; 7 | import { DACommentVerifierV1 } from './da-comment-verifier-v1'; 8 | import { DACommentVerifierV2 } from './da-comment-verifier-v2'; 9 | 10 | /** 11 | * Checks if the given DACommentPublication is valid by verifying the proof chain, cross-checking against the event, and 12 | * validating the signature. 13 | * @param daPublicationVerifier The verifier for the DACommentPublication. 14 | * @param verifyPointer If true, the pointer chain will be verified before checking the publication. 15 | * @param ethereumNode The EthereumNode to use for fetching data from the Ethereum blockchain. 16 | * @param log A function used for logging output. 17 | * @param checker The DAProofChecker to use for checking the proof. 18 | * @returns A PromiseResult indicating success or failure, along with an optional error message. 19 | */ 20 | export const checkDAComment = async ( 21 | daPublicationVerifier: DACommentVerifierV1 | DACommentVerifierV2, 22 | verifyPointer: boolean, 23 | ethereumNode: EthereumNode, 24 | log: LogFunctionType, 25 | checker: DaProofChecker 26 | ): PromiseResult => { 27 | log('check DA comment'); 28 | 29 | const publication = daPublicationVerifier.daPublication; 30 | 31 | if (!publication.chainProofs.pointer) { 32 | return failure(MomokaValidatorError.PUBLICATION_NO_POINTER); 33 | } 34 | 35 | if (publication.chainProofs.pointer.type !== DAPublicationPointerType.ON_DA) { 36 | return failure(MomokaValidatorError.PUBLICATION_NONE_DA); 37 | } 38 | 39 | if (verifyPointer) { 40 | log('verify pointer first'); 41 | 42 | // check the pointer! 43 | const pointerResult = await checker.checkDAProof( 44 | publication.chainProofs.pointer.location, 45 | ethereumNode, 46 | { 47 | byPassDb: false, 48 | verifyPointer: false, 49 | log, 50 | } 51 | ); 52 | if (pointerResult.isFailure()) { 53 | return failure(MomokaValidatorError.POINTER_FAILED_VERIFICATION); 54 | } 55 | } 56 | 57 | const signerResult = await daPublicationVerifier.verifySigner(); 58 | 59 | if (signerResult.isFailure()) { 60 | return failure(signerResult.failure); 61 | } 62 | 63 | const eventResult = await daPublicationVerifier.verifyEventWithTypedData( 64 | signerResult.successResult.currentPublicationId 65 | ); 66 | 67 | log('finished checking DA comment'); 68 | 69 | return eventResult; 70 | }; 71 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/create-da-publication-verifier.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { MomokaActionTypes } from '../../data-availability-models/data-availability-action-types'; 3 | import { DACommentVerifierV1, isDACommentPublicationV1 } from './comment/da-comment-verifier-v1'; 4 | import { DACommentVerifierV2, isDACommentPublicationV2 } from './comment/da-comment-verifier-v2'; 5 | import { LogFunctionType } from '../../common/logger'; 6 | import { DAMirrorVerifierV1, isDAMirrorPublicationV1 } from './mirror/da-mirror-verifier-v1'; 7 | import { DAMirrorVerifierV2, isDAMirrorPublicationV2 } from './mirror/da-mirror-verifier-v2'; 8 | import { EthereumNode } from '../../evm/ethereum'; 9 | import { DAPostVerifierV1, isDAPostPublicationV1 } from './post/da-post-verifier-v1'; 10 | import { DAPostVerifierV2, isDAPostPublicationV2 } from './post/da-post-verifier-v2'; 11 | import { DAQuoteVerifierV2, isDAQuotePublicationV2 } from './quote/da-quote-verifier-v2'; 12 | import { failure, PromiseResult, success } from '../../data-availability-models/da-result'; 13 | import { MomokaValidatorError } from '../../data-availability-models/validator-errors'; 14 | 15 | export type DAPublicationVerifier = 16 | | DAPostVerifierV1 17 | | DAPostVerifierV2 18 | | DACommentVerifierV1 19 | | DACommentVerifierV2 20 | | DAMirrorVerifierV1 21 | | DAMirrorVerifierV2 22 | | DAQuoteVerifierV2; 23 | 24 | export const createDAPublicationVerifier = ( 25 | daPublication: DAStructurePublication, 26 | ethereumNode: EthereumNode, 27 | log: LogFunctionType 28 | ): PromiseResult => { 29 | switch (daPublication.type) { 30 | case MomokaActionTypes.POST_CREATED: 31 | if (isDAPostPublicationV1(daPublication)) { 32 | log('verifying post v1'); 33 | return Promise.resolve(success(new DAPostVerifierV1(daPublication, ethereumNode, log))); 34 | } 35 | 36 | if (isDAPostPublicationV2(daPublication)) { 37 | log('verifying post v2'); 38 | 39 | return Promise.resolve(success(new DAPostVerifierV2(daPublication, ethereumNode, log))); 40 | } 41 | 42 | log('post was not recognized as v1 or v2'); 43 | 44 | return Promise.resolve(failure(MomokaValidatorError.PUBLICATION_NOT_RECOGNIZED)); 45 | case MomokaActionTypes.COMMENT_CREATED: 46 | if (isDACommentPublicationV1(daPublication)) { 47 | log('verifying comment v1'); 48 | 49 | return Promise.resolve(success(new DACommentVerifierV1(daPublication, ethereumNode, log))); 50 | } 51 | 52 | if (isDACommentPublicationV2(daPublication)) { 53 | log('verifying comment v2'); 54 | 55 | return Promise.resolve(success(new DACommentVerifierV2(daPublication, ethereumNode, log))); 56 | } 57 | 58 | log('comment was not recognized as v1 or v2'); 59 | 60 | return Promise.resolve(failure(MomokaValidatorError.PUBLICATION_NOT_RECOGNIZED)); 61 | case MomokaActionTypes.MIRROR_CREATED: 62 | if (isDAMirrorPublicationV1(daPublication)) { 63 | log('verifying mirror v1'); 64 | 65 | return Promise.resolve(success(new DAMirrorVerifierV1(daPublication, ethereumNode, log))); 66 | } 67 | if (isDAMirrorPublicationV2(daPublication)) { 68 | log('verifying mirror v2'); 69 | 70 | return Promise.resolve(success(new DAMirrorVerifierV2(daPublication, ethereumNode, log))); 71 | } 72 | 73 | log('mirror was not recognized as v1 or v2'); 74 | 75 | return Promise.resolve(failure(MomokaValidatorError.PUBLICATION_NOT_RECOGNIZED)); 76 | case MomokaActionTypes.QUOTE_CREATED: 77 | if (isDAQuotePublicationV2(daPublication)) { 78 | log('verifying quote v2'); 79 | 80 | return Promise.resolve(success(new DAQuoteVerifierV2(daPublication, ethereumNode, log))); 81 | } 82 | log('quote was not recognized as v2'); 83 | 84 | return Promise.resolve(failure(MomokaValidatorError.PUBLICATION_NOT_RECOGNIZED)); 85 | default: 86 | return Promise.resolve(failure(MomokaValidatorError.PUBLICATION_NOT_RECOGNIZED)); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/da-publication-verifier-v1.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { DAEventTypeV1 } from '../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { PublicationV1TypedData } from '../../data-availability-models/publications/data-availability-publication-typed-data'; 4 | import { generatePublicationId } from '../utils'; 5 | import { failure, PromiseResult, success } from '../../data-availability-models/da-result'; 6 | import { whoSignedTypedData } from './publication.base'; 7 | import { MomokaValidatorError } from '../../data-availability-models/validator-errors'; 8 | import { LensHubV1Gateway } from '../../evm/gateway/LensHubV1Gateway'; 9 | import { EthereumNode } from '../../evm/ethereum'; 10 | import { LogFunctionType } from '../../common/logger'; 11 | 12 | export abstract class DAPublicationVerifierV1 { 13 | protected readonly lensHubGateway: LensHubV1Gateway; 14 | 15 | constructor( 16 | public readonly daPublication: DAStructurePublication, 17 | protected readonly ethereumNode: EthereumNode, 18 | protected readonly log: LogFunctionType 19 | ) { 20 | this.lensHubGateway = new LensHubV1Gateway(ethereumNode); 21 | } 22 | 23 | public verifyPublicationIdMatches(): PromiseResult { 24 | const generatedPublicationId = generatePublicationId( 25 | this.daPublication.event.profileId, 26 | this.daPublication.event.pubId, 27 | this.daPublication.dataAvailabilityId 28 | ); 29 | 30 | if (generatedPublicationId !== this.daPublication.publicationId) { 31 | this.log('publicationId does not match the generated one'); 32 | 33 | return Promise.resolve(failure(MomokaValidatorError.GENERATED_PUBLICATION_ID_MISMATCH)); 34 | } 35 | 36 | return Promise.resolve(success()); 37 | } 38 | 39 | public async verifySigner(): PromiseResult<{ 40 | currentPublicationId: string; 41 | ownerOfAddress: string; 42 | }> { 43 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 44 | 45 | const whoSignedResult = await whoSignedTypedData( 46 | typedData.domain, 47 | typedData.types, 48 | typedData.value, 49 | this.daPublication.chainProofs.thisPublication.signature 50 | ); 51 | 52 | if (whoSignedResult.isFailure()) { 53 | return failure(whoSignedResult.failure); 54 | } 55 | 56 | const whoSigned = whoSignedResult.successResult; 57 | 58 | this.log('who signed', whoSigned); 59 | 60 | const chainProfileDetailsResult = await this.lensHubGateway.getOnChainProfileDetails( 61 | this.daPublication.chainProofs.thisPublication.blockNumber, 62 | typedData.value.profileId, 63 | whoSigned 64 | ); 65 | if (chainProfileDetailsResult.isFailure()) { 66 | return failure(chainProfileDetailsResult.failure); 67 | } 68 | 69 | const details = chainProfileDetailsResult.successResult; 70 | 71 | if (details.sigNonce !== typedData.value.nonce) { 72 | this.log('nonce mismatch', { expected: details.sigNonce, actual: typedData.value.nonce }); 73 | return failure(MomokaValidatorError.PUBLICATION_NONCE_INVALID); 74 | } 75 | 76 | if (details.dispatcherAddress !== whoSigned && details.ownerOfAddress !== whoSigned) { 77 | return failure(MomokaValidatorError.PUBLICATION_SIGNER_NOT_ALLOWED); 78 | } 79 | 80 | return success({ 81 | currentPublicationId: details.currentPublicationId, 82 | ownerOfAddress: details.ownerOfAddress, 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/da-publication-verifier-v2.ts: -------------------------------------------------------------------------------- 1 | import { failure, PromiseResult, success } from '../../data-availability-models/da-result'; 2 | import { DAEventTypeV2 } from '../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { PublicationV2TypedData } from '../../data-availability-models/publications/data-availability-publication-typed-data'; 4 | import { DAStructurePublication } from '../../data-availability-models/publications/data-availability-structure-publication'; 5 | import { whoSignedTypedData } from './publication.base'; 6 | import { MomokaValidatorError } from '../../data-availability-models/validator-errors'; 7 | import { EthereumNode } from '../../evm/ethereum'; 8 | import { LogFunctionType } from '../../common/logger'; 9 | import { LensHubV2Gateway } from '../../evm/gateway/LensHubV2Gateway'; 10 | 11 | export abstract class DAPublicationVerifierV2 { 12 | protected readonly lensHubGateway: LensHubV2Gateway; 13 | 14 | constructor( 15 | public readonly daPublication: DAStructurePublication, 16 | protected readonly ethereumNode: EthereumNode, 17 | protected readonly log: LogFunctionType 18 | ) { 19 | this.lensHubGateway = new LensHubV2Gateway(ethereumNode); 20 | } 21 | 22 | abstract verifyPublicationIdMatches(): PromiseResult; 23 | 24 | public async verifySigner(): PromiseResult<{ 25 | currentPublicationId: string; 26 | ownerOfAddress: string; 27 | }> { 28 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 29 | 30 | const whoSignedResult = await whoSignedTypedData( 31 | typedData.domain, 32 | typedData.types, 33 | typedData.value, 34 | this.daPublication.chainProofs.thisPublication.signature 35 | ); 36 | 37 | if (whoSignedResult.isFailure()) { 38 | return failure(whoSignedResult.failure); 39 | } 40 | 41 | const whoSigned = whoSignedResult.successResult; 42 | 43 | this.log('who signed', whoSigned); 44 | 45 | const chainProfileDetailsResult = await this.lensHubGateway.getOnChainProfileDetails( 46 | this.daPublication.chainProofs.thisPublication.blockNumber, 47 | typedData.value.profileId, 48 | whoSigned 49 | ); 50 | 51 | if (chainProfileDetailsResult.isFailure()) { 52 | return failure(chainProfileDetailsResult.failure); 53 | } 54 | 55 | const details = chainProfileDetailsResult.successResult; 56 | 57 | if (details.nonces !== typedData.value.nonce) { 58 | this.log('nonce mismatch', { expected: details.nonces, actual: typedData.value.nonce }); 59 | return failure(MomokaValidatorError.PUBLICATION_NONCE_INVALID); 60 | } 61 | 62 | if (details.ownerOfAddress !== whoSigned && !details.isSignerApprovedExecutor) { 63 | return failure(MomokaValidatorError.PUBLICATION_SIGNER_NOT_ALLOWED); 64 | } 65 | 66 | return success({ 67 | currentPublicationId: details.currentPublicationId, 68 | ownerOfAddress: details.ownerOfAddress, 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/mirror/da-mirror-verifier-v1.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { DAMirrorCreatedEventEmittedResponseV1 } from '../../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { DAPublicationVerifierV1 } from '../da-publication-verifier-v1'; 4 | import { LogFunctionType } from '../../../common/logger'; 5 | import { failure, PromiseResult, success } from '../../../data-availability-models/da-result'; 6 | import { BigNumber } from 'ethers'; 7 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 8 | import { EMPTY_BYTE, EthereumNode } from '../../../evm/ethereum'; 9 | import { CreateMirrorV1EIP712TypedData } from '../../../data-availability-models/publications/data-availability-publication-typed-data'; 10 | import { MomokaActionTypes } from '../../../data-availability-models/data-availability-action-types'; 11 | 12 | export type DAMirrorPublicationV1 = DAStructurePublication< 13 | DAMirrorCreatedEventEmittedResponseV1, 14 | CreateMirrorV1EIP712TypedData 15 | >; 16 | 17 | export const isDAMirrorPublicationV1 = ( 18 | daPublication: DAStructurePublication 19 | ): daPublication is DAMirrorPublicationV1 => { 20 | return ( 21 | daPublication.type === MomokaActionTypes.MIRROR_CREATED && 22 | !('mirrorParams' in daPublication.event) 23 | ); 24 | }; 25 | 26 | export class DAMirrorVerifierV1 extends DAPublicationVerifierV1 { 27 | public readonly type = MomokaActionTypes.MIRROR_CREATED; 28 | 29 | constructor( 30 | public readonly daPublication: DAMirrorPublicationV1, 31 | ethereumNode: EthereumNode, 32 | log: LogFunctionType 33 | ) { 34 | super(daPublication, ethereumNode, log); 35 | } 36 | 37 | verifyEventWithTypedData(pubCountAtBlock: string): PromiseResult { 38 | const event = this.daPublication.event; 39 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 40 | 41 | // compare all event emitted to typed data value 42 | this.log('cross check event with typed data value'); 43 | 44 | // check the pub count makes sense from the block! 45 | if (BigNumber.from(pubCountAtBlock).add(1).toHexString() !== event.pubId) { 46 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 47 | } 48 | 49 | this.log('pub count at block is correct'); 50 | 51 | // compare all others! 52 | if ( 53 | typedData.value.profileId !== event.profileId || 54 | typedData.value.profileIdPointed !== event.profileIdPointed || 55 | typedData.value.pubIdPointed !== event.pubIdPointed || 56 | typedData.value.referenceModule !== event.referenceModule || 57 | event.referenceModuleReturnData !== EMPTY_BYTE || 58 | typedData.value.referenceModuleInitData !== EMPTY_BYTE 59 | ) { 60 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 61 | } 62 | 63 | this.log('cross check event is complete'); 64 | 65 | return Promise.resolve(success()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/mirror/da-mirror-verifier-v2.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { DAMirrorCreatedEventEmittedResponseV2 } from '../../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { CreateMirrorV2EIP712TypedData } from '../../../data-availability-models/publications/data-availability-publication-typed-data'; 4 | import { LogFunctionType } from '../../../common/logger'; 5 | import { failure, PromiseResult, success } from '../../../data-availability-models/da-result'; 6 | import { BigNumber } from 'ethers'; 7 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 8 | import { DAPublicationVerifierV2 } from '../da-publication-verifier-v2'; 9 | import { generatePublicationId } from '../../utils'; 10 | import { MomokaActionTypes } from '../../../data-availability-models/data-availability-action-types'; 11 | import { EMPTY_BYTE, EthereumNode } from '../../../evm/ethereum'; 12 | import { arraysEqual } from '../../../utils/arrays-equal'; 13 | 14 | export type DAMirrorPublicationV2 = DAStructurePublication< 15 | DAMirrorCreatedEventEmittedResponseV2, 16 | CreateMirrorV2EIP712TypedData 17 | >; 18 | 19 | export const isDAMirrorPublicationV2 = ( 20 | daPublication: DAStructurePublication 21 | ): daPublication is DAMirrorPublicationV2 => { 22 | return ( 23 | daPublication.type === MomokaActionTypes.MIRROR_CREATED && 'mirrorParams' in daPublication.event 24 | ); 25 | }; 26 | 27 | export class DAMirrorVerifierV2 extends DAPublicationVerifierV2 { 28 | public readonly type = MomokaActionTypes.MIRROR_CREATED; 29 | 30 | constructor( 31 | public readonly daPublication: DAMirrorPublicationV2, 32 | ethereumNode: EthereumNode, 33 | log: LogFunctionType 34 | ) { 35 | super(daPublication, ethereumNode, log); 36 | } 37 | 38 | verifyPublicationIdMatches(): PromiseResult { 39 | const generatedPublicationId = generatePublicationId( 40 | this.daPublication.event.mirrorParams.profileId, 41 | this.daPublication.event.pubId, 42 | this.daPublication.dataAvailabilityId 43 | ); 44 | 45 | if (generatedPublicationId !== this.daPublication.publicationId) { 46 | this.log('publicationId does not match the generated one'); 47 | 48 | return Promise.resolve(failure(MomokaValidatorError.GENERATED_PUBLICATION_ID_MISMATCH)); 49 | } 50 | 51 | return Promise.resolve(success()); 52 | } 53 | 54 | verifyEventWithTypedData(pubCountAtBlock: string): PromiseResult { 55 | const event = this.daPublication.event; 56 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 57 | 58 | // compare all event emitted to typed data value 59 | this.log('cross check event with typed data value'); 60 | 61 | // check the pub count makes sense from the block! 62 | if (BigNumber.from(pubCountAtBlock).add(1).toHexString() !== event.pubId) { 63 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 64 | } 65 | 66 | this.log('pub count at block is correct'); 67 | 68 | // compare all others! 69 | if ( 70 | typedData.value.profileId !== event.mirrorParams.profileId || 71 | typedData.value.metadataURI !== event.mirrorParams.metadataURI || 72 | typedData.value.pointedProfileId !== event.mirrorParams.pointedProfileId || 73 | typedData.value.pointedPubId !== event.mirrorParams.pointedPubId || 74 | !arraysEqual(typedData.value.referrerProfileIds, event.mirrorParams.referrerProfileIds) || 75 | !arraysEqual(typedData.value.referrerPubIds, event.mirrorParams.referrerPubIds) || 76 | event.referenceModuleReturnData !== EMPTY_BYTE 77 | ) { 78 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 79 | } 80 | 81 | this.log('cross check event is complete'); 82 | 83 | return Promise.resolve(success()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/mirror/index.ts: -------------------------------------------------------------------------------- 1 | import { LogFunctionType } from '../../../common/logger'; 2 | import { failure, PromiseResult } from '../../../data-availability-models/da-result'; 3 | import { DAPublicationPointerType } from '../../../data-availability-models/publications/data-availability-structure-publication'; 4 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 5 | import { EthereumNode } from '../../../evm/ethereum'; 6 | import { DaProofChecker } from '../../da-proof-checker'; 7 | import { DAMirrorVerifierV1 } from './da-mirror-verifier-v1'; 8 | import { DAMirrorVerifierV2 } from './da-mirror-verifier-v2'; 9 | 10 | /** 11 | * Checks if the given DAMirrorPublication is valid by verifying the proof chain, cross-checking against the event, and 12 | * validating the signature. 13 | * @param daPublicationVerifier The verifier for the DACommentPublication. 14 | * @param verifyPointer If true, the pointer chain will be verified before checking the publication. 15 | * @param ethereumNode The EthereumNode to use for fetching data from the Ethereum blockchain. 16 | * @param log A function used for logging output. 17 | * @param checker The DAProofChecker to use for checking the proof. 18 | * @returns A PromiseResult indicating success or failure, along with an optional error message. 19 | */ 20 | export const checkDAMirror = async ( 21 | daPublicationVerifier: DAMirrorVerifierV1 | DAMirrorVerifierV2, 22 | verifyPointer: boolean, 23 | ethereumNode: EthereumNode, 24 | log: LogFunctionType, 25 | // TODO: Improve that to avoid cycling dependencies 26 | checker: DaProofChecker 27 | ): PromiseResult => { 28 | log('check DA mirror'); 29 | 30 | const publication = daPublicationVerifier.daPublication; 31 | 32 | if (!publication.chainProofs.pointer) { 33 | return failure(MomokaValidatorError.PUBLICATION_NO_POINTER); 34 | } 35 | 36 | // only supports mirrors on DA at the moment 37 | if (publication.chainProofs.pointer.type !== DAPublicationPointerType.ON_DA) { 38 | return failure(MomokaValidatorError.PUBLICATION_NONE_DA); 39 | } 40 | 41 | if (verifyPointer) { 42 | log('verify pointer first'); 43 | 44 | // check the pointer! 45 | const pointerResult = await checker.checkDAProof( 46 | publication.chainProofs.pointer.location, 47 | ethereumNode, 48 | { 49 | verifyPointer: false, 50 | byPassDb: false, 51 | log, 52 | } 53 | ); 54 | 55 | if (pointerResult.isFailure()) { 56 | return failure(MomokaValidatorError.POINTER_FAILED_VERIFICATION); 57 | } 58 | } 59 | 60 | const signerResult = await daPublicationVerifier.verifySigner(); 61 | 62 | if (signerResult.isFailure()) { 63 | return failure(signerResult.failure); 64 | } 65 | 66 | const eventResult = await daPublicationVerifier.verifyEventWithTypedData( 67 | signerResult.successResult.currentPublicationId 68 | ); 69 | 70 | log('finished checking DA mirror'); 71 | 72 | return eventResult; 73 | }; 74 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/post/index.ts: -------------------------------------------------------------------------------- 1 | import { LogFunctionType } from '../../../common/logger'; 2 | import { failure, PromiseResult } from '../../../data-availability-models/da-result'; 3 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 4 | import { DAPostVerifierV1 } from './da-post-verifier-v1'; 5 | import { DAPostVerifierV2 } from './da-post-verifier-v2'; 6 | 7 | /** 8 | * Checks if the given DAPostPublication is valid by verifying the proof chain, cross-checking against the event, and 9 | * validating the signature. 10 | * @param daPublicationVerifier The verifier for the DACommentPublication. 11 | * @param log A function used for logging output. 12 | * @returns A PromiseResult indicating success or failure, along with an optional error message. 13 | */ 14 | export const checkDAPost = async ( 15 | daPublicationVerifier: DAPostVerifierV1 | DAPostVerifierV2, 16 | log: LogFunctionType 17 | ): PromiseResult => { 18 | log('check DA post'); 19 | 20 | const publication = daPublicationVerifier.daPublication; 21 | 22 | if (publication.chainProofs.pointer) { 23 | return failure(MomokaValidatorError.INVALID_POINTER_SET_NOT_NEEDED); 24 | } 25 | 26 | const simulatedResult = await daPublicationVerifier.verifySimulation(); 27 | 28 | if (simulatedResult.isFailure()) { 29 | return failure(simulatedResult.failure); 30 | } 31 | 32 | // cross-check event and typed data values 33 | const eventResult = daPublicationVerifier.verifyEventWithTypedData(simulatedResult.successResult); 34 | 35 | log('finished checking DA post'); 36 | 37 | return eventResult; 38 | }; 39 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/publication.base.ts: -------------------------------------------------------------------------------- 1 | import { SignatureLike } from '@ethersproject/bytes'; 2 | import { utils } from 'ethers'; 3 | import { failure, PromiseResult, success } from '../../data-availability-models/da-result'; 4 | import { 5 | TypedDataDomain, 6 | TypedDataField, 7 | } from '../../data-availability-models/data-availability-typed-data'; 8 | import { MomokaValidatorError } from '../../data-availability-models/validator-errors'; 9 | 10 | /** 11 | * Verifies the provided signature corresponds to the given typed data and returns the address of the signer. 12 | * @param domain The typed data domain. 13 | * @param types The typed data types. 14 | * @param value The typed data value. 15 | * @param signature The signature to verify. 16 | * @returns A `success` result with the signer's address if the signature is valid, or a `failure` result with a `MomokaValidatorError` if there's an error during the verification process. 17 | turned into a promise as its minimum CPU intensive 18 | */ 19 | export const whoSignedTypedData = ( 20 | domain: TypedDataDomain, 21 | types: Record>, 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | value: Record, 24 | signature: SignatureLike 25 | ): PromiseResult => { 26 | try { 27 | const address = utils.verifyTypedData(domain, types, value, signature); 28 | return Promise.resolve(success(address)); 29 | } catch { 30 | return Promise.resolve(failure(MomokaValidatorError.INVALID_FORMATTED_TYPED_DATA)); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/quote/da-quote-verifier-v2.ts: -------------------------------------------------------------------------------- 1 | import { DAStructurePublication } from '../../../data-availability-models/publications/data-availability-structure-publication'; 2 | import { DAQuoteCreatedEventEmittedResponseV2 } from '../../../data-availability-models/publications/data-availability-structure-publications-events'; 3 | import { CreateQuoteV2EIP712TypedData } from '../../../data-availability-models/publications/data-availability-publication-typed-data'; 4 | import { LogFunctionType } from '../../../common/logger'; 5 | import { failure, PromiseResult, success } from '../../../data-availability-models/da-result'; 6 | import { BigNumber } from 'ethers'; 7 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 8 | import { DAPublicationVerifierV2 } from '../da-publication-verifier-v2'; 9 | import { generatePublicationId } from '../../utils'; 10 | import { MomokaActionTypes } from '../../../data-availability-models/data-availability-action-types'; 11 | import { EMPTY_BYTE, EthereumNode } from '../../../evm/ethereum'; 12 | import { arraysEqual } from '../../../utils/arrays-equal'; 13 | 14 | export type DAQuotePublicationV2 = DAStructurePublication< 15 | DAQuoteCreatedEventEmittedResponseV2, 16 | CreateQuoteV2EIP712TypedData 17 | >; 18 | 19 | export const isDAQuotePublicationV2 = ( 20 | daPublication: DAStructurePublication 21 | ): daPublication is DAQuotePublicationV2 => { 22 | return ( 23 | daPublication.type === MomokaActionTypes.QUOTE_CREATED && 'quoteParams' in daPublication.event 24 | ); 25 | }; 26 | 27 | export class DAQuoteVerifierV2 extends DAPublicationVerifierV2 { 28 | public readonly type = MomokaActionTypes.QUOTE_CREATED; 29 | 30 | constructor( 31 | public readonly daPublication: DAQuotePublicationV2, 32 | ethereumNode: EthereumNode, 33 | log: LogFunctionType 34 | ) { 35 | super(daPublication, ethereumNode, log); 36 | } 37 | 38 | verifyPublicationIdMatches(): PromiseResult { 39 | const generatedPublicationId = generatePublicationId( 40 | this.daPublication.event.quoteParams.profileId, 41 | this.daPublication.event.pubId, 42 | this.daPublication.dataAvailabilityId 43 | ); 44 | 45 | if (generatedPublicationId !== this.daPublication.publicationId) { 46 | this.log('publicationId does not match the generated one'); 47 | 48 | return Promise.resolve(failure(MomokaValidatorError.GENERATED_PUBLICATION_ID_MISMATCH)); 49 | } 50 | 51 | return Promise.resolve(success()); 52 | } 53 | 54 | verifyEventWithTypedData(pubCountAtBlock: string): PromiseResult { 55 | const event = this.daPublication.event; 56 | const typedData = this.daPublication.chainProofs.thisPublication.typedData; 57 | 58 | // compare all event emitted to typed data value 59 | this.log('cross check event with typed data value'); 60 | 61 | // check the pub count makes sense from the block! 62 | if (BigNumber.from(pubCountAtBlock).add(1).toHexString() !== event.pubId) { 63 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 64 | } 65 | 66 | this.log('pub count at block is correct'); 67 | 68 | // compare all others! 69 | if ( 70 | typedData.value.profileId !== event.quoteParams.profileId || 71 | typedData.value.contentURI !== event.quoteParams.contentURI || 72 | typedData.value.pointedProfileId !== event.quoteParams.pointedProfileId || 73 | typedData.value.pointedPubId !== event.quoteParams.pointedPubId || 74 | !arraysEqual(typedData.value.actionModules, event.quoteParams.actionModules) || 75 | !arraysEqual( 76 | typedData.value.actionModulesInitDatas, 77 | event.quoteParams.actionModulesInitDatas 78 | ) || 79 | typedData.value.referenceModule !== event.quoteParams.referenceModule || 80 | typedData.value.referenceModuleInitData !== event.quoteParams.referenceModuleInitData || 81 | !arraysEqual(typedData.value.referrerProfileIds, event.quoteParams.referrerProfileIds) || 82 | !arraysEqual(typedData.value.referrerPubIds, event.quoteParams.referrerPubIds) || 83 | event.actionModulesInitReturnDatas.length !== 0 || 84 | event.referenceModuleReturnData !== EMPTY_BYTE || 85 | event.referenceModuleInitReturnData !== EMPTY_BYTE 86 | ) { 87 | return Promise.resolve(failure(MomokaValidatorError.EVENT_MISMATCH)); 88 | } 89 | 90 | this.log('cross check event is complete'); 91 | 92 | return Promise.resolve(success()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/publications/quote/index.ts: -------------------------------------------------------------------------------- 1 | import { LogFunctionType } from '../../../common/logger'; 2 | import { failure, PromiseResult } from '../../../data-availability-models/da-result'; 3 | import { DAPublicationPointerType } from '../../../data-availability-models/publications/data-availability-structure-publication'; 4 | import { MomokaValidatorError } from '../../../data-availability-models/validator-errors'; 5 | import { EthereumNode } from '../../../evm/ethereum'; 6 | import { DaProofChecker } from '../../da-proof-checker'; 7 | import { DAQuoteVerifierV2 } from './da-quote-verifier-v2'; 8 | 9 | /** 10 | * Checks if the given DAQuotePublication is valid by verifying the proof chain, cross-checking against the event, and 11 | * validating the signature. 12 | * @param daPublicationVerifier The verifier for the DAQuotePublication. 13 | * @param verifyPointer If true, the pointer chain will be verified before checking the publication. 14 | * @param ethereumNode The EthereumNode to use for fetching data from the Ethereum blockchain. 15 | * @param log A function used for logging output. 16 | * @param checker The DAProofChecker to use for checking the proof. 17 | * @returns A PromiseResult indicating success or failure, along with an optional error message. 18 | */ 19 | export const checkDAQuote = async ( 20 | daPublicationVerifier: DAQuoteVerifierV2, 21 | verifyPointer: boolean, 22 | ethereumNode: EthereumNode, 23 | log: LogFunctionType, 24 | checker: DaProofChecker 25 | ): PromiseResult => { 26 | log('check DA quote'); 27 | 28 | const publication = daPublicationVerifier.daPublication; 29 | 30 | if (!publication.chainProofs.pointer) { 31 | return failure(MomokaValidatorError.PUBLICATION_NO_POINTER); 32 | } 33 | 34 | if (publication.chainProofs.pointer.type !== DAPublicationPointerType.ON_DA) { 35 | return failure(MomokaValidatorError.PUBLICATION_NONE_DA); 36 | } 37 | 38 | if (verifyPointer) { 39 | log('verify pointer first'); 40 | 41 | // check the pointer! 42 | const pointerResult = await checker.checkDAProof( 43 | publication.chainProofs.pointer.location, 44 | ethereumNode, 45 | { 46 | byPassDb: false, 47 | verifyPointer: false, 48 | log, 49 | } 50 | ); 51 | if (pointerResult.isFailure()) { 52 | return failure(MomokaValidatorError.POINTER_FAILED_VERIFICATION); 53 | } 54 | } 55 | 56 | const signerResult = await daPublicationVerifier.verifySigner(); 57 | 58 | if (signerResult.isFailure()) { 59 | return failure(signerResult.failure); 60 | } 61 | 62 | const eventResult = await daPublicationVerifier.verifyEventWithTypedData( 63 | signerResult.successResult.currentPublicationId 64 | ); 65 | 66 | log('finished checking DA quote'); 67 | 68 | return eventResult; 69 | }; 70 | -------------------------------------------------------------------------------- /momoka-node/src/proofs/utils.ts: -------------------------------------------------------------------------------- 1 | import type CryptoInterface from 'arweave/node/lib/crypto/crypto-interface'; 2 | import { BlockInfo } from '../evm/ethereum'; 3 | import { unixTimestampToMilliseconds } from '../common/helpers'; 4 | import { DAStructurePublication } from '../data-availability-models/publications/data-availability-structure-publication'; 5 | import { BundlrUploadResponse } from '../data-availability-models/data-availability-timestamp-proofs'; 6 | import { BigNumberish } from 'ethers'; 7 | 8 | /** 9 | * Finds the closest block based on timestamp in milliseconds. 10 | * @param blocks List of blocks to search through 11 | * @param targetTimestamp Timestamp in milliseconds to match against block timestamp 12 | * @returns The block with the closest matching timestamp 13 | */ 14 | export const getClosestBlock = (blocks: BlockInfo[], targetTimestamp: number): BlockInfo => { 15 | const targetTimestampMs = unixTimestampToMilliseconds(targetTimestamp); 16 | 17 | return blocks.reduce((prev, curr) => { 18 | const prevTimestamp = unixTimestampToMilliseconds(prev.timestamp); 19 | const currTimestamp = unixTimestampToMilliseconds(curr.timestamp); 20 | 21 | if (currTimestamp > targetTimestampMs) { 22 | return prev; 23 | } 24 | 25 | const prevDifference = Math.abs(prevTimestamp - targetTimestampMs); 26 | const currDifference = Math.abs(currTimestamp - targetTimestampMs); 27 | 28 | return currDifference < prevDifference ? curr : prev; 29 | }); 30 | }; 31 | 32 | /** 33 | * Generates the unique ID for a DAStructurePublication. 34 | * @param profileId The profile ID of the publication. 35 | * @param publicationId The publication ID of the publication. 36 | * @param dataAvailabilityId The data availability ID of the publication. 37 | */ 38 | export const generatePublicationId = ( 39 | profileId: BigNumberish, 40 | publicationId: BigNumberish, 41 | dataAvailabilityId: string 42 | ): string => { 43 | return `${profileId}-${publicationId}-DA-${dataAvailabilityId.split('-')[0]}`; 44 | }; 45 | 46 | /** 47 | * Checks if the typed data deadline timestamp in the given DAStructurePublication matches 48 | * the block timestamp of the containing block. 49 | * @param daPublication The DAStructurePublication to check. 50 | * @returns True if the typed data deadline timestamp matches the block timestamp, false otherwise. 51 | */ 52 | export const isValidTypedDataDeadlineTimestamp = ( 53 | daPublication: DAStructurePublication 54 | ): boolean => { 55 | return ( 56 | daPublication.chainProofs.thisPublication.typedData.value.deadline === 57 | daPublication.chainProofs.thisPublication.blockTimestamp 58 | ); 59 | }; 60 | 61 | /** 62 | * Checks if the event timestamp in the given DA publication matches the publication timestamp of the block it was included in. 63 | * @param daPublication The DA publication to check. 64 | * @returns A boolean indicating whether or not the event timestamp matches the publication timestamp. 65 | */ 66 | export const isValidEventTimestamp = (daPublication: DAStructurePublication): boolean => { 67 | return daPublication.event.timestamp === daPublication.chainProofs.thisPublication.blockTimestamp; 68 | }; 69 | 70 | type Providers = { 71 | stringToBuffer: (str: string) => Uint8Array; 72 | b64UrlToBuffer: (str: string) => Uint8Array; 73 | deepHash: (str: Uint8Array[]) => Promise; 74 | crypto: CryptoInterface; 75 | }; 76 | 77 | /** 78 | * Verifies the signature of a Bundlr upload response. 79 | * Copied from https://github.com/Bundlr-Network/js-sdk/blob/main/src/common/utils.ts to make the code isomorphic 80 | * and avoid bringing node native modules to client code. 81 | * @param response The Bundlr upload response to verify. 82 | * @param providers The providers to use for the verification (should be either the one for node or client). 83 | */ 84 | export const verifyReceipt = async ( 85 | { deadlineHeight, id, public: pubKey, signature, timestamp, version }: BundlrUploadResponse, 86 | { crypto, b64UrlToBuffer, stringToBuffer, deepHash }: Providers 87 | ): Promise => { 88 | const dh = await deepHash([ 89 | stringToBuffer('Bundlr'), 90 | stringToBuffer(version), 91 | stringToBuffer(id), 92 | stringToBuffer(deadlineHeight.toString()), 93 | stringToBuffer(timestamp.toString()), 94 | ]); 95 | 96 | return await crypto.verify(pubKey, dh, b64UrlToBuffer(signature)); 97 | }; 98 | -------------------------------------------------------------------------------- /momoka-node/src/queue/base.queue.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from '../common/helpers'; 2 | 3 | /** 4 | * A queue is a data structure that follows the FIFO (First In First Out) principle. 5 | */ 6 | export class Queue { 7 | private items: T[]; 8 | 9 | constructor() { 10 | this.items = []; 11 | } 12 | 13 | public enqueue(item: T): void { 14 | this.items.push(item); 15 | } 16 | 17 | /** 18 | * Enqueues an item with a delay to not spam the event loop 19 | * @param item The item 20 | * @param delay The delay in milliseconds 21 | */ 22 | public async enqueueWithDelay(item: T, delay: number): Promise { 23 | await sleep(delay); 24 | this.enqueue(item); 25 | } 26 | 27 | public dequeue(): T | undefined { 28 | if (this.isEmpty()) { 29 | return undefined; 30 | } 31 | return this.items.shift(); 32 | } 33 | 34 | public isEmpty(): boolean { 35 | return this.items.length === 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /momoka-node/src/queue/known.queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from './base.queue'; 2 | import { ProcessFailedProofQueueRequest } from './process-failed-da-proof.queue'; 3 | import { ProcessRetryCheckDAProofsQueueRequest } from './process-retry-check-da-proofs.queue'; 4 | 5 | /** 6 | * The failed proofs queue 7 | */ 8 | export const failedDAProofQueue = new Queue(); 9 | /** 10 | * The retry check proofs queue 11 | */ 12 | export const retryCheckDAProofsQueue = new Queue(); 13 | -------------------------------------------------------------------------------- /momoka-node/src/queue/process-failed-da-proof.queue.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fs } from 'fs'; 2 | import { runForever } from '../common/helpers'; 3 | import { MomokaValidatorError } from '../data-availability-models/validator-errors'; 4 | import { failedProofsPath, pathResolver } from '../input-output/paths'; 5 | import { Queue } from './base.queue'; 6 | import { failedDAProofQueue } from './known.queue'; 7 | 8 | export interface ProcessFailedProofQueueRequest { 9 | txId: string; 10 | reason: MomokaValidatorError; 11 | submitter: string; 12 | } 13 | 14 | const writeFailedProof = async (failed: ProcessFailedProofQueueRequest): Promise => { 15 | const path = await pathResolver(); 16 | const failedPath = await failedProofsPath(); 17 | const errorLocation = path.join(failedPath, failed.reason); 18 | if (!existsSync(errorLocation)) { 19 | await fs.mkdir(errorLocation); 20 | } 21 | 22 | await fs.writeFile(path.join(errorLocation, failed.txId + '.json'), JSON.stringify(failed)); 23 | }; 24 | 25 | /** 26 | * Processes the failed proofs queue to save them on disk 27 | * @param failedQueue - The failed proofs queue 28 | */ 29 | export const processFailedDAProofQueue = async ( 30 | failedQueue: Queue 31 | ): Promise => { 32 | await runForever(async () => { 33 | if (!failedQueue.isEmpty()) { 34 | const failed = failedQueue.dequeue(); 35 | if (failed) { 36 | try { 37 | await writeFailedProof(failed); 38 | } catch (e) { 39 | console.error( 40 | 'Error writing the disk for failed publication.. make sure you have enough disk space', 41 | { 42 | error: e, 43 | } 44 | ); 45 | 46 | // add back in the queue in 30 seconds for retry 47 | failedDAProofQueue.enqueueWithDelay(failed, 30000); 48 | } 49 | } 50 | } 51 | }, 200); 52 | }; 53 | -------------------------------------------------------------------------------- /momoka-node/src/queue/process-retry-check-da-proofs.queue.ts: -------------------------------------------------------------------------------- 1 | import { runForever } from '../common/helpers'; 2 | import { MomokaValidatorError } from '../data-availability-models/validator-errors'; 3 | import { EthereumNode } from '../evm/ethereum'; 4 | import { checkDAProofsBatch } from '../proofs/check-da-proofs-batch'; 5 | import { StreamCallback } from '../watchers/models/stream.type'; 6 | import { Queue } from './base.queue'; 7 | import { retryCheckDAProofsQueue } from './known.queue'; 8 | 9 | export interface ProcessRetryCheckDAProofsQueueRequest { 10 | txIds: string[]; 11 | ethereumNode: EthereumNode; 12 | stream: StreamCallback | undefined; 13 | } 14 | 15 | /** 16 | * Validation errors whcih should be retried due to network issues out of 17 | * control of the proof verifier. 18 | * @param validatorError The validation error to check. 19 | */ 20 | export const shouldRetry = (validatorError: MomokaValidatorError): boolean => { 21 | return ( 22 | validatorError === MomokaValidatorError.UNKNOWN || 23 | validatorError === MomokaValidatorError.CAN_NOT_CONNECT_TO_BUNDLR || 24 | validatorError === MomokaValidatorError.BLOCK_CANT_BE_READ_FROM_NODE || 25 | validatorError === MomokaValidatorError.DATA_CANT_BE_READ_FROM_NODE || 26 | validatorError === MomokaValidatorError.SIMULATION_NODE_COULD_NOT_RUN 27 | ); 28 | }; 29 | 30 | /** 31 | * Processes the retry check proofs queue to check the proofs again (due to fails based on stuff which isnt valid) 32 | * @param retryQueue - The retry check proofs queue 33 | * @param usLocalNode - A boolean to indicate whether to use the local node. 34 | */ 35 | export const processRetryCheckDAProofsQueue = async ( 36 | retryQueue: Queue, 37 | concurrency: number, 38 | usLocalNode = false 39 | ): Promise => { 40 | await runForever(async () => { 41 | if (!retryQueue.isEmpty()) { 42 | const proofs = retryQueue.dequeue(); 43 | if (proofs && proofs.txIds.length > 0) { 44 | try { 45 | await checkDAProofsBatch( 46 | proofs.txIds, 47 | proofs.ethereumNode, 48 | true, 49 | concurrency, 50 | usLocalNode, 51 | proofs.stream 52 | ); 53 | } catch (e) { 54 | // add back in the queue in 30 seconds for retry 55 | retryCheckDAProofsQueue.enqueueWithDelay(proofs, 30000); 56 | } 57 | } 58 | } 59 | }, 200); 60 | }; 61 | -------------------------------------------------------------------------------- /momoka-node/src/queue/startup.queue.ts: -------------------------------------------------------------------------------- 1 | import { failedDAProofQueue, retryCheckDAProofsQueue } from './known.queue'; 2 | import { processFailedDAProofQueue } from './process-failed-da-proof.queue'; 3 | import { processRetryCheckDAProofsQueue } from './process-retry-check-da-proofs.queue'; 4 | 5 | /** 6 | * Starts the queues up 7 | */ 8 | export const startupQueues = (concurrency: number): void => { 9 | processFailedDAProofQueue(failedDAProofQueue); 10 | processRetryCheckDAProofsQueue(retryCheckDAProofsQueue, concurrency); 11 | }; 12 | -------------------------------------------------------------------------------- /momoka-node/src/runnable/da-verifier-node.runnable.ts: -------------------------------------------------------------------------------- 1 | import { getParam, turnedOffExperimentalWarning } from '../common/helpers'; 2 | import { startDAVerifierNode } from '../watchers/verifier.watcher'; 3 | import { ethereumNode } from './ethereum-node-instance'; 4 | 5 | turnedOffExperimentalWarning(); 6 | 7 | const concurrencyRaw = getParam('CONCURRENCY'); 8 | const concurrency = concurrencyRaw ? Number(concurrencyRaw) : 100; 9 | 10 | startDAVerifierNode(ethereumNode, concurrency).catch((error) => { 11 | console.error('DA verifier node failed to startup', error); 12 | process.exitCode = 1; 13 | }); 14 | -------------------------------------------------------------------------------- /momoka-node/src/runnable/ethereum-node-instance.ts: -------------------------------------------------------------------------------- 1 | import { Deployment, Environment } from '../common/environment'; 2 | import { getParam, getParamOrExit } from '../common/helpers'; 3 | import { EthereumNode } from '../evm/ethereum'; 4 | 5 | export const ethereumNode: EthereumNode = { 6 | environment: getParamOrExit('ETHEREUM_NETWORK') as Environment, 7 | nodeUrl: getParamOrExit('NODE_URL'), 8 | deployment: (getParam('DEPLOYMENT') as Deployment) || Deployment.PRODUCTION, 9 | }; 10 | -------------------------------------------------------------------------------- /momoka-node/src/runnable/failed-submissons.runnable.ts: -------------------------------------------------------------------------------- 1 | import { verifierFailedSubmissionsWatcher } from '../watchers/failed-submissons.watcher'; 2 | 3 | /** 4 | * Watches for failed submissions in the database and logs a summary of the errors. 5 | */ 6 | verifierFailedSubmissionsWatcher().catch((error) => { 7 | console.error('DA verifier failed watcher failed to startup', error); 8 | process.exitCode = 1; 9 | }); 10 | -------------------------------------------------------------------------------- /momoka-node/src/submitters.ts: -------------------------------------------------------------------------------- 1 | import { Deployment, Environment } from './common/environment'; 2 | 3 | /** 4 | * Returns the list of submitters based on the given environment and deployment 5 | * @param environment - The environment to get the submitters for 6 | * @param deployment - The deployment to get the submitters for. Defaults to Deployment.PRODUCTION 7 | * @returns An array of submitter addresses in lowercase 8 | */ 9 | export const getSubmitters = ( 10 | environment: Environment, 11 | deployment: Deployment = Deployment.PRODUCTION 12 | ): string[] => { 13 | // this will come from a smart contract later on! 14 | 15 | if (deployment === Deployment.PRODUCTION) { 16 | switch (environment) { 17 | case Environment.POLYGON: 18 | return ['0xBe29464B9784a0d8956f29630d8bc4D7B5737435'.toLowerCase()]; 19 | case Environment.MUMBAI: 20 | return ['0xF1143C45953066718dE115578cf31c237B062a15'.toLowerCase()]; 21 | case Environment.AMOY: 22 | return ['0x085be9a079aB75608fB794f2D288A375856e3f60'.toLowerCase()]; 23 | default: 24 | throw new Error('Invalid environment'); 25 | } 26 | } 27 | 28 | if (deployment === Deployment.STAGING) { 29 | switch (environment) { 30 | case Environment.POLYGON: 31 | throw new Error('Not Supported'); 32 | case Environment.MUMBAI: 33 | return ['0x55307bfae6DF8988F59FE20272bC68792b130415'.toLowerCase()]; 34 | case Environment.AMOY: 35 | return ['0xC1b3BF1D611f1148F1799E90f7342860499Ba9D9'.toLowerCase()]; 36 | default: 37 | throw new Error('Invalid environment'); 38 | } 39 | } 40 | 41 | if (deployment === Deployment.LOCAL) { 42 | switch (environment) { 43 | case Environment.POLYGON: 44 | throw new Error('Not Supported'); 45 | case Environment.MUMBAI: 46 | return ['0x8Fc176aA6FC843D3422f0C1832f1b9E17be00C1c'.toLowerCase()]; 47 | case Environment.AMOY: 48 | return ['0xcD7739d0b2ceFAb809FEF4e839a55b2627B60205'.toLowerCase()]; 49 | default: 50 | throw new Error('Invalid environment'); 51 | } 52 | } 53 | 54 | throw new Error('Invalid deployment'); 55 | }; 56 | 57 | /** 58 | * Checks if an Ethereum address is a valid submitter for the given environment and deployment. 59 | * @param environment The environment (Polygon, Mumbai, or Sandbox). 60 | * @param address The Ethereum address to check. 61 | * @param deployment The deployment (Production, Staging, or Local). Defaults to Production. 62 | * @returns True if the address is a valid submitter, false otherwise. 63 | */ 64 | export const isValidSubmitter = ( 65 | environment: Environment, 66 | address: string, 67 | deployment?: Deployment 68 | ): boolean => { 69 | return getSubmitters(environment, deployment).includes(address.toLowerCase()); 70 | }; 71 | -------------------------------------------------------------------------------- /momoka-node/src/utils/arrays-equal.ts: -------------------------------------------------------------------------------- 1 | export const arraysEqual = (array1: T[], array2: T[]): boolean => { 2 | if (array1.length !== array2.length) { 3 | return false; 4 | } 5 | 6 | for (let i = 0; i < array1.length; i++) { 7 | if (array1[i] !== array2[i]) { 8 | return false; 9 | } 10 | } 11 | 12 | return true; 13 | }; 14 | -------------------------------------------------------------------------------- /momoka-node/src/utils/invariant.ts: -------------------------------------------------------------------------------- 1 | export class InvariantError extends Error { 2 | constructor(message: string) { 3 | super(`InvariantError: ${message}`); 4 | } 5 | } 6 | 7 | type Invariant = (condition: unknown, message: string) => asserts condition; 8 | 9 | /** 10 | * Asserts that the given condition is truthy 11 | * 12 | * @param condition - Either truthy or falsy value 13 | * @param message - An error message 14 | */ 15 | export const invariant: Invariant = (condition: unknown, message: string): asserts condition => { 16 | if (!condition) { 17 | throw new InvariantError(message); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /momoka-node/src/utils/number-to-hex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Does this over ethers call as alchemy and some other providers dont like a padding hex number 3 | * - wont accept 0x01f1a494 4 | * - will accept 0x1f1a494 5 | * @param number 6 | * @returns 7 | */ 8 | export const numberToHex = (number: number): string => { 9 | return '0x' + number.toString(16); 10 | }; 11 | -------------------------------------------------------------------------------- /momoka-node/src/watchers/failed-submissons.watcher.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fs } from 'fs'; 2 | import path from 'path'; 3 | import { runForever } from '../common/helpers'; 4 | import { consoleLogWithLensNodeFootprint } from '../common/logger'; 5 | import { MomokaValidatorError } from '../data-availability-models/validator-errors'; 6 | import { failedProofsPath } from '../input-output/paths'; 7 | import { shouldRetry } from '../queue/process-retry-check-da-proofs.queue'; 8 | 9 | /** 10 | * Watches for failed submissions written to disk 11 | */ 12 | export const verifierFailedSubmissionsWatcher = async (): Promise => { 13 | consoleLogWithLensNodeFootprint('started up failed submission watcher...'); 14 | 15 | let firstRun = true; 16 | await runForever(async () => { 17 | if (firstRun) { 18 | firstRun = false; 19 | } else { 20 | try { 21 | const failedPath = await failedProofsPath(); 22 | const failedResults = []; 23 | // Count the number of failed submissions for each error reason 24 | for (const item in MomokaValidatorError) { 25 | if (isNaN(Number(item))) { 26 | if (!shouldRetry(item as MomokaValidatorError)) { 27 | const errorPath = path.join(failedPath, item); 28 | const errorCount = existsSync(errorPath) 29 | ? (await fs.readdir(path.join(failedPath, item))).length 30 | : 0; 31 | 32 | failedResults.push([item, errorCount]); 33 | } 34 | } 35 | } 36 | console.table(failedResults); 37 | } catch (error) { 38 | consoleLogWithLensNodeFootprint( 39 | 'verifier failed watcher failed try again in 5 seconds', 40 | error 41 | ); 42 | } 43 | } 44 | }, 60000); 45 | }; 46 | -------------------------------------------------------------------------------- /momoka-node/src/watchers/models/start-da-verifier-node-options.ts: -------------------------------------------------------------------------------- 1 | import { StreamCallback } from './stream.type'; 2 | 3 | /** 4 | * Options for starting the verifier node 5 | */ 6 | export interface StartDAVerifierNodeOptions { 7 | /** 8 | * The callback function to be called when a new result is available 9 | */ 10 | stream?: StreamCallback | undefined; 11 | 12 | /** 13 | * if true, the verifier node will resync back from zero 14 | * and will not sync from the genesis block 15 | */ 16 | resync?: boolean | undefined; 17 | } 18 | -------------------------------------------------------------------------------- /momoka-node/src/watchers/models/stream.type.ts: -------------------------------------------------------------------------------- 1 | import { TxValidatedResult } from '../../input-output/tx-validated-results'; 2 | 3 | export type StreamResult = TxValidatedResult; 4 | export type StreamCallback = (result: StreamResult) => void; 5 | -------------------------------------------------------------------------------- /momoka-node/src/watchers/trusting-indexing.watcher.ts: -------------------------------------------------------------------------------- 1 | import { Deployment, Environment } from '../common/environment'; 2 | import { runForever, sleep } from '../common/helpers'; 3 | import { consoleLog } from '../common/logger'; 4 | import { getBundlrBulkTxsAPI } from '../input-output/bundlr/get-bundlr-bulk-txs.api'; 5 | import { 6 | DataAvailabilityTransactionsOrderTypes, 7 | getDataAvailabilityTransactionsAPI, 8 | getDataAvailabilityTransactionsAPIResponse, 9 | } from '../input-output/bundlr/get-data-availability-transactions.api'; 10 | import { TIMEOUT_ERROR } from '../input-output/common'; 11 | import { buildDAPublicationsBatchResult } from '../proofs/check-da-proofs-batch'; 12 | import { StreamCallback } from './models/stream.type'; 13 | 14 | /** 15 | * The DA trusting indexer request. 16 | */ 17 | export interface StartDATrustingIndexingRequest { 18 | stream: StreamCallback; 19 | environment: Environment; 20 | /** 21 | * The deployment to use, only use this if you know what you are doing. 22 | */ 23 | deployment?: Deployment; 24 | } 25 | 26 | /** 27 | * Starts the DA trusting indexing to watch for new data availability coming in and index them. 28 | * @param request The trusting index request 29 | */ 30 | export const startDATrustingIndexing = async ( 31 | request: StartDATrustingIndexingRequest 32 | ): Promise => { 33 | consoleLog('LENS DA TRUSTING INDEXING - DA verification indexing starting...'); 34 | 35 | let endCursor: string | null = null; 36 | 37 | return await runForever(async () => { 38 | try { 39 | // Get new data availability transactions from the server. 40 | const arweaveTransactions: getDataAvailabilityTransactionsAPIResponse = 41 | await getDataAvailabilityTransactionsAPI( 42 | request.environment, 43 | request.deployment, 44 | endCursor, 45 | DataAvailabilityTransactionsOrderTypes.ASC 46 | ); 47 | 48 | if (arweaveTransactions.edges.length === 0) { 49 | consoleLog('LENS DA TRUSTING INDEXING - No new DA items found..'); 50 | // Sleep for 100ms before checking again. 51 | await sleep(100); 52 | } else { 53 | consoleLog( 54 | 'LENS DA TRUSTING INDEXING - Found new submissions...', 55 | arweaveTransactions.edges.length 56 | ); 57 | 58 | const bulkDAProofs = await getBundlrBulkTxsAPI( 59 | arweaveTransactions.edges.map((edge) => edge.node.id) 60 | ); 61 | if (bulkDAProofs === TIMEOUT_ERROR) { 62 | throw new Error('getBundlrBulkTxsAPI for proofs timed out'); 63 | } 64 | 65 | // Build the data availability publication result for each submission. 66 | const daPublications = await buildDAPublicationsBatchResult(bulkDAProofs.success); 67 | 68 | // Stream the results to the callback. 69 | daPublications.map((publication) => { 70 | request.stream({ 71 | proofTxId: publication.id, 72 | success: true, 73 | dataAvailabilityResult: publication.daPublication, 74 | }); 75 | }); 76 | 77 | endCursor = arweaveTransactions.pageInfo.endCursor; 78 | } 79 | } catch (error) { 80 | consoleLog('LENS DA TRUSTING INDEXING - Error while checking for new submissions', error); 81 | await sleep(100); 82 | } 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /momoka-node/src/workers/handler-communication.worker.ts: -------------------------------------------------------------------------------- 1 | import { parentPort } from 'worker_threads'; 2 | import { 3 | bundlrVerifyReceiptWorker, 4 | BundlrVerifyReceiptWorkerRequest, 5 | evmVerifyMessageWorker, 6 | EVMVerifyMessageWorkerRequest, 7 | } from './message-handlers'; 8 | 9 | export type EVMVerifyMessageHandlerWorkerRequest = HandlerWorkerRequest< 10 | HandlerWorkers.EVM_VERIFY_MESSAGE, 11 | EVMVerifyMessageWorkerRequest 12 | >; 13 | 14 | export type BundlrVerifyReceiptHandlerWorkerRequest = HandlerWorkerRequest< 15 | HandlerWorkers.BUNDLR_VERIFY_RECEIPT, 16 | BundlrVerifyReceiptWorkerRequest 17 | >; 18 | 19 | export enum HandlerWorkers { 20 | EVM_VERIFY_MESSAGE = 'EVM_VERIFY_MESSAGE', 21 | BUNDLR_VERIFY_RECEIPT = 'BUNDLR_VERIFY_RECEIPT', 22 | } 23 | 24 | export interface HandlerWorkerRequest { 25 | worker: THandlerWorkers; 26 | data: T; 27 | } 28 | 29 | export type HandlerWorkerData = 30 | | EVMVerifyMessageHandlerWorkerRequest 31 | | BundlrVerifyReceiptHandlerWorkerRequest; 32 | 33 | const executeWorker = async (request: HandlerWorkerData): Promise => { 34 | switch (request.worker) { 35 | case HandlerWorkers.EVM_VERIFY_MESSAGE: 36 | return await Promise.resolve( 37 | evmVerifyMessageWorker(request.data as EVMVerifyMessageWorkerRequest) 38 | ); 39 | case HandlerWorkers.BUNDLR_VERIFY_RECEIPT: 40 | return await bundlrVerifyReceiptWorker(request.data as BundlrVerifyReceiptWorkerRequest); 41 | } 42 | }; 43 | 44 | // eslint-disable-next-line require-await 45 | parentPort?.on('message', async (request: string) => { 46 | const handlerWorkerData = JSON.parse(request) as HandlerWorkerData; 47 | const result = await executeWorker(handlerWorkerData); 48 | 49 | parentPort!.postMessage(result); 50 | }); 51 | -------------------------------------------------------------------------------- /momoka-node/src/workers/message-handlers/bundlr-verify-receipt.worker.ts: -------------------------------------------------------------------------------- 1 | import deepHash from 'arweave/node/lib/deepHash'; 2 | import Arweave from 'arweave/node'; 3 | import { b64UrlToBuffer } from 'arweave/node/lib/utils'; 4 | 5 | import { BundlrUploadResponse } from '../../data-availability-models/data-availability-timestamp-proofs'; 6 | import { verifyReceipt } from '../../proofs/utils'; 7 | 8 | export interface BundlrVerifyReceiptWorkerRequest { 9 | bundlrUploadResponse: BundlrUploadResponse; 10 | } 11 | 12 | /** 13 | * Verifies the receipt of a Bundlr upload 14 | * @param request - The request to verify the receipt of a Bundlr upload 15 | */ 16 | export const bundlrVerifyReceiptWorker = ( 17 | request: BundlrVerifyReceiptWorkerRequest 18 | ): Promise => { 19 | return verifyReceipt(request.bundlrUploadResponse, { 20 | crypto: Arweave.crypto, 21 | deepHash, 22 | b64UrlToBuffer, 23 | stringToBuffer: Arweave.utils.stringToBuffer, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /momoka-node/src/workers/message-handlers/evm-verify-message.worker.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers'; 2 | import { 3 | DAEventType, 4 | DAStructurePublication, 5 | PublicationTypedData, 6 | } from '../../data-availability-models/publications/data-availability-structure-publication'; 7 | 8 | export interface EVMVerifyMessageWorkerRequest { 9 | daPublication: DAStructurePublication; 10 | signature: string; 11 | } 12 | 13 | /** 14 | * Verifies the signature of a message 15 | * @param request - The request to verify the signature of a message 16 | */ 17 | export const evmVerifyMessageWorker = (request: EVMVerifyMessageWorkerRequest): string => { 18 | return utils.verifyMessage(JSON.stringify(request.daPublication), request.signature); 19 | }; 20 | -------------------------------------------------------------------------------- /momoka-node/src/workers/message-handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bundlr-verify-receipt.worker'; 2 | export * from './evm-verify-message.worker'; 3 | -------------------------------------------------------------------------------- /momoka-node/src/workers/worker-pool.ts: -------------------------------------------------------------------------------- 1 | import { cpus } from 'os'; 2 | import { resolve as resolvePath } from 'path'; 3 | import { Worker } from 'worker_threads'; 4 | import { HandlerWorkerData } from './handler-communication.worker'; 5 | 6 | // const workerPath = resolvePath(__dirname, 'handler-communication.worker.js'); 7 | 8 | class WorkerPool { 9 | private workers: Worker[] = []; 10 | private queue: (() => void)[] = []; 11 | 12 | constructor() { 13 | // Set the pool size to the number of available CPU cores 14 | const size = cpus().length; 15 | for (let i = 0; i < size; i++) { 16 | const worker = new Worker( 17 | // worker path has to be a raw `.js` file so rewrite to `lib` 18 | resolvePath(__dirname, '..', '..', 'lib', 'workers', 'handler-communication.worker.js') 19 | ); 20 | worker.on('message', (_result) => { 21 | this.queue.shift()?.(); 22 | }); 23 | this.workers.push(worker); 24 | } 25 | } 26 | 27 | public execute(request: HandlerWorkerData): Promise { 28 | const availableWorker = this.workers.shift(); 29 | if (!availableWorker) { 30 | return new Promise((resolve) => { 31 | this.queue.push(() => resolve(this.execute(request))); 32 | }); 33 | } 34 | 35 | return new Promise((resolve, _reject) => { 36 | availableWorker.once('message', (result) => { 37 | resolve(result); 38 | this.workers.push(availableWorker); 39 | this.queue.shift()?.(); 40 | }); 41 | 42 | availableWorker.postMessage(JSON.stringify(request)); 43 | }); 44 | } 45 | } 46 | 47 | export const workerPool = new WorkerPool(); 48 | -------------------------------------------------------------------------------- /momoka-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "rootDir": "src", 6 | "outDir": "lib", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "declaration": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "noImplicitReturns": true, 16 | "allowSyntheticDefaultImports": true, 17 | "resolveJsonModule": true, 18 | "sourceMap": true 19 | }, 20 | "include": ["./src"], 21 | "exclude": ["./src/__TESTS__"] 22 | } 23 | -------------------------------------------------------------------------------- /momoka-rs/.env: -------------------------------------------------------------------------------- 1 | ETHEREUM_NETWORK=POLYGON 2 | # ETHEREUM_NETWORK=MUMBAI 3 | # ETHEREUM_NETWORK=POLYGON 4 | NODE_URL=https://polygon-mainnet.g.alchemy.com/v2/WaQlz3BZ9rps6PTOiJmcdTm7XaFVnJhs 5 | # NODE_URL='ws://127.0.0.1:8545/' 6 | # NODE_URL=https://matic-mumbai.chainstacklabs.com 7 | # NODE_URL(FREE)=https://polygon-mumbai.g.alchemy.com/v2/s1Hq6Tyswh1mDXOlgawIqSCndvsNfYb6 8 | DEPLOYMENT=PRODUCTION 9 | # DEPLOYMENT=STAGING 10 | # DEPLOYMENT=PRODUCTION 11 | CONCURRENCY=10 12 | -------------------------------------------------------------------------------- /momoka-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "momoka" 3 | version = "1.1.0" 4 | edition = "2021" 5 | authors = ["Josh Stevens "] 6 | description = "The momoka rs client" 7 | license = "MIT" 8 | repository = "https://github.com/lens-protocol/momoka" 9 | 10 | [dependencies] 11 | tokio = { version = "1", features = ["full"] } 12 | serde_json = "1.0" 13 | gql_client = "1.0.7" 14 | reqwest = "0.11.4" 15 | serde = { version = "1.0", features = ["derive"] } 16 | base64 = "0.21.0" 17 | ethers = "2.0.4" 18 | clap = { version = "4.2.5", features = ["derive"] } 19 | futures = "0.3" 20 | async-trait = "0.1.51" 21 | lazy_static = "1.4.0" 22 | bundlr-sdk = "0.4.1" 23 | regex = "1.5" 24 | json = "0.12" 25 | chrono = "0.4" 26 | strum = "0.24.1" 27 | strum_macros = "0.24.3" 28 | data-encoding = "2.3.0" 29 | uuid = { version = "1.3.2", features = ["v4", "serde"] } 30 | hex = "0.4.3" 31 | 32 | [profile.performance] 33 | inherits = "release" 34 | lto = "fat" 35 | codegen-units = 1 36 | incremental = false 37 | 38 | [target.'cfg(all(not(windows), not(target_env = "musl")))'.dependencies] 39 | jemallocator = "0.5.0" -------------------------------------------------------------------------------- /momoka-rs/README.md: -------------------------------------------------------------------------------- 1 | # momoka_rs 2 | 3 | THIS ONLY SUPPORTS LENS V1 PUBLICATIONS AND HAS NOT BEEN MIGRATED TO LENS V2 SUPPORT YET. 4 | 5 | This is the rust implementation of the momoka library. It is currently beta and still recommended you use the momoka-node for now. The rust library will be the main client in the future, the node and client verifier logic will always be supported and maintained so people can verify client side. 6 | 7 | ## Installing 8 | 9 | ```bash 10 | $ cargo install momoka 11 | ``` 12 | 13 | ## CLI 14 | 15 | Usage: momoka [OPTIONS] 16 | 17 | ```bash 18 | Options: 19 | -n The URL of the node 20 | -e The environment (e.g., "MUMBAI", "AMOY" or "POLYGON") 21 | -d The deployment (e.g., "PRODUCTION") 22 | -t The transaction ID to check proof for 23 | -r Flag indicating whether to perform a resync 24 | -h, --help Print help 25 | -V, --version Print version 26 | ``` 27 | 28 | ## Usage CLI 29 | 30 | It is easy to run the momoka verifier locally using cargo. You can do so by running the following commands: 31 | 32 | ### Verifying live transactions 33 | 34 | ```bash 35 | $ momoka 36 | ``` 37 | 38 | Note if you do not supply a `-n="YOUR_NODE"` it will use a free node which has very low rate limits. If using it for anything in production you should supply your own node. 39 | 40 | This will start verifying any incoming momoka transactions live. You can also can specify to resync from transaction 1 by adding the `-r` flag (this needs a node which is paid and has a high rate limit). 41 | 42 | ### Verifying a single transaction 43 | 44 | ```bash 45 | $ momoka -t="TX_ID" 46 | ``` 47 | 48 | ### Running locally from source 49 | 50 | It is easy to run the momoka verifier locally using cargo. You can do so by running the following command: 51 | 52 | ```bash 53 | $ cargo run -- -n="YOUR_NODE" [-r] [-t="TX_ID"] 54 | ``` 55 | -------------------------------------------------------------------------------- /momoka-rs/src/bundlr/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod verify; 3 | -------------------------------------------------------------------------------- /momoka-rs/src/bundlr/verify.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ 2 | transaction::TransactionTimestampProofsValidation, verifier_error::MomokaVerifierError, 3 | }; 4 | use bundlr_sdk::{ 5 | deep_hash::DeepHashChunk, deep_hash_sync::deep_hash_sync, ArweaveSigner, Verifier, 6 | }; 7 | 8 | use data_encoding::BASE64URL_NOPAD; 9 | 10 | /// Verifies the timestamp proofs for a transaction. 11 | /// 12 | /// This function takes a `TransactionTimestampProofsValidation` object, which contains the 13 | /// timestamp proofs for a transaction, and verifies the signature of the proofs using Arweave's 14 | /// verification algorithm. If the signature is invalid, this function returns an error. 15 | /// 16 | /// # Examples 17 | /// 18 | /// ``` 19 | /// use momoka_verifier::api::verify_timestamp_proofs; 20 | /// use momoka_verifier::types::transaction::TransactionTimestampProofsValidation; 21 | /// use momoka_verifier::verifier_error::MomokaVerifierError; 22 | /// 23 | /// let tx_proofs = TransactionTimestampProofsValidation { 24 | /// version: "1".into(), 25 | /// id: "abc123".into(), 26 | /// deadline_height: 100, 27 | /// timestamp: 1620680199, 28 | /// public_key: "key".into(), 29 | /// signature: "sig".into(), 30 | /// }; 31 | /// 32 | /// let result = verify_timestamp_proofs(&tx_proofs); 33 | /// assert!(result.is_ok()); 34 | /// ``` 35 | pub async fn verify_timestamp_proofs( 36 | timestamp_proofs: &TransactionTimestampProofsValidation, 37 | ) -> Result<(), MomokaVerifierError> { 38 | let fields = DeepHashChunk::Chunks(vec![ 39 | DeepHashChunk::Chunk("Bundlr".into()), 40 | DeepHashChunk::Chunk(timestamp_proofs.version.clone().into()), 41 | DeepHashChunk::Chunk(timestamp_proofs.id.clone().into()), 42 | DeepHashChunk::Chunk(timestamp_proofs.deadline_height.to_string().into()), 43 | DeepHashChunk::Chunk(timestamp_proofs.timestamp.to_string().into()), 44 | ]); 45 | 46 | let pubk = BASE64URL_NOPAD 47 | .decode(×tamp_proofs.public_key.clone().into_bytes()) 48 | .map_err(|_| MomokaVerifierError::TimestampProofInvalidSignature)?; 49 | 50 | let msg = 51 | deep_hash_sync(fields).map_err(|_| MomokaVerifierError::TimestampProofInvalidSignature)?; 52 | let sig = BASE64URL_NOPAD 53 | .decode(×tamp_proofs.signature.clone().into_bytes()) 54 | .map_err(|_| MomokaVerifierError::TimestampProofInvalidSignature)?; 55 | 56 | // Verify the signature using Arweave's verification algorithm 57 | ArweaveSigner::verify(pubk.into(), msg, sig.into()) 58 | .map_err(|_| MomokaVerifierError::TimestampProofInvalidSignature) 59 | } 60 | 61 | #[cfg(test)] 62 | #[tokio::test] 63 | async fn test_verify_timestamp_proofs() { 64 | // Prepare the input 65 | let timestamp_proofs = TransactionTimestampProofsValidation { 66 | id: "1cgDW9R4aSFXYd2NuVHITPvXQbA13-nUQwS1fhL6R0g".to_string(), 67 | timestamp: 1682525560422, 68 | version: "1.0.0".to_string(), 69 | public_key: "sq9JbppKLlAKtQwalfX5DagnGMlTirditXk7y4jgoeA7DEM0Z6cVPE5xMQ9kz_T9VppP6BFHtHyZCZODercEVWipzkr36tfQkR5EDGUQyLivdxUzbWgVkzw7D27PJEa4cd1Uy6r18rYLqERgbRvAZph5YJZmpSJk7r3MwnQquuktjvSpfCLFwSxP1w879-ss_JalM9ICzRi38henONio8gll6GV9-omrWwRMZer_15bspCK5txCwpY137nfKwKD5YBAuzxxcj424M7zlSHlsafBwaRwFbf8gHtW03iJER4lR4GxeY0WvnYaB3KDISHQp53a9nlbmiWO5WcHHYsR83OT2eJ0Pl3RWA-_imk_SNwGQTCjmA6tf_UVwL8HzYS2iyuu85b7iYK9ZQoh8nqbNC6qibICE4h9Fe3bN7AgitIe9XzCTOXDfMr4ahjC8kkqJ1z4zNAI6-Leei_Mgd8JtZh2vqFNZhXK0lSadFl_9Oh3AET7tUds2E7s-6zpRPd9oBZu6-kNuHDRJ6TQhZSwJ9ZO5HYsccb_G_1so72aXJymR9ggJgWr4J3bawAYYnqmvmzGklYOlE_5HVnMxf-UxpT7ztdsHbc9QEH6W2bzwxbpjTczEZs3JCCB3c-NewNHsj9PYM3b5tTlTNP9kNAwPZHWpt11t79LuNkNGt9LfOek".to_string(), 70 | signature: "VwDTklBWgxilmvgwZnal6JvGwF0fKcPx3JqZ5TMo35jKVOEKyCR8czY82x0fYz_rRqeZc96DAJPtMeHaKK-p3Taw-WvEbX9vvDISTjaEQMEYAl1aeAQG-RzcmmB8Ac9a57-OXThDUa88lQPYRrRCu8pIMc1fa-CnBY9CxXJQLv8K1XbZ5L1Hsg97lF64c0wYsxD72svLsc-s9fUmAZ1aB3fpAVYSUgpxK5FPZI1dxFA_TjJSrVEBGUz_ODWho1ZPtGpLlkr81Z10WkaohTLPe-_CBEouLy6fDPCrE3MUUj_-F-OHtzRgK756MQreMxoDEZSXNI22E7CFRiyy_1Rbw4Ax2lu65JeedGnajGcTpTVPlV6UTJRo8kPm6Zo6O6nTqaiZCvnNcLmcOXhNWSSJXVX2zxHWo6kT3ffwKRPuawaNgXFmIDzznfEqg-7uVEByI2UxpD_pF74J44ZxKUurBl8vm6OM7zvyL86VNNTVjafy4Qi6Y45NNqfcbsQpkYfindz0gBWU64NktRE3qUsPce4pL8C1vifL3P7SGF8RLhKedPi52-BNaufRk_vmUlBcNpsvsBSECcCU9SLgY3cSaZekClnPCM2kPQjg5bAIvHr88WSnFwm2niQ8ZZSJPaEEy6qI0QrgXnYDidgbGeUvygeFKG-E2itlF3tBtvR4SlQ".to_string(), 71 | deadline_height: 1170647, 72 | block: 1170647, 73 | validator_signatures: vec![] 74 | }; 75 | 76 | // Verify the timestamp proofs 77 | let _ = verify_timestamp_proofs(×tamp_proofs).await.is_ok(); 78 | } 79 | -------------------------------------------------------------------------------- /momoka-rs/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, RwLock}; 3 | 4 | use crate::types::transaction::MomokaTxId; 5 | use crate::types::verifier_error::MomokaVerifierError; 6 | 7 | pub struct TransactionCacheResult { 8 | pub success: bool, 9 | pub error: Option, 10 | } 11 | 12 | lazy_static::lazy_static! { 13 | static ref TRANSACTION_CACHE: RwLock>> = RwLock::new(HashMap::new()); 14 | static ref SIGNATURE_CACHE: RwLock>> = RwLock::new(HashMap::new()); 15 | } 16 | 17 | /// Reads a value from the transaction cache dictionary based on the given key. 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `key` - A momoka tx id slice representing the key to look up in the cache. 22 | /// 23 | /// # Returns 24 | /// 25 | /// An `Option` containing a reference to the value if the key is found in the cache, or `None` if the key is not present. 26 | pub fn read_transaction_cache(key: &MomokaTxId) -> Option> { 27 | TRANSACTION_CACHE.read().unwrap().get(key).cloned() 28 | } 29 | 30 | /// Sets a value in the transaction cache dictionary based on the given key. 31 | /// 32 | /// # Arguments 33 | /// 34 | /// * `key` - A momoka tx id representing the key to set in the cache. 35 | /// * `value` - The value to associate with the key in the cache. 36 | pub fn set_transaction_cache(key: MomokaTxId, value: TransactionCacheResult) { 37 | let cache_value = Arc::new(value); 38 | TRANSACTION_CACHE.write().unwrap().insert(key, cache_value); 39 | } 40 | 41 | /// Reads a value from the signature cache dictionary based on the given key. 42 | /// 43 | /// # Arguments 44 | /// 45 | /// * `key` - A signature slice representing the key to look up in the cache. 46 | /// 47 | /// # Returns 48 | /// 49 | /// An `Option` containing a reference to the value if the key is found in the cache, or `None` if the key is not present. 50 | pub fn read_signature_cache(key: &str) -> Option> { 51 | SIGNATURE_CACHE.read().unwrap().get(key).cloned() 52 | } 53 | 54 | /// Sets a value in the signature cache dictionary based on the given key. 55 | /// 56 | /// # Arguments 57 | /// 58 | /// * `key` - A signature representing the key to set in the cache. 59 | /// * `value` - The value to associate with the key in the cache. 60 | pub fn set_signature_cache(key: String) { 61 | let cache_value = Arc::new(()); 62 | SIGNATURE_CACHE.write().unwrap().insert(key, cache_value); 63 | } 64 | -------------------------------------------------------------------------------- /momoka-rs/src/contracts/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lens_hub; 2 | -------------------------------------------------------------------------------- /momoka-rs/src/environment.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use ethers::types::Address; 4 | 5 | /// Represents different deployment environments. 6 | #[derive(Debug, Clone)] 7 | pub enum Deployment { 8 | Production, 9 | Staging, 10 | Local, 11 | } 12 | 13 | impl FromStr for Deployment { 14 | type Err = (); 15 | 16 | /// Parses a string into a `Deployment` enum variant. 17 | /// 18 | /// # Arguments 19 | /// 20 | /// * `s` - A string slice representing the deployment value. 21 | /// 22 | /// # Returns 23 | /// 24 | /// A `Result` containing the parsed `Deployment` variant if successful, or an error if parsing fails. 25 | fn from_str(s: &str) -> Result { 26 | match s.to_ascii_lowercase().as_str() { 27 | "production" => Ok(Deployment::Production), 28 | "staging" => Ok(Deployment::Staging), 29 | "local" => Ok(Deployment::Local), 30 | _ => Err(()), 31 | } 32 | } 33 | } 34 | 35 | /// Contains information about the environment configuration. 36 | #[derive(Debug)] 37 | pub struct EnvironmentInfo { 38 | /// The environment value. 39 | pub environment: Environment, 40 | /// The URL of the node. 41 | pub node_url: String, 42 | /// The deployment value. 43 | pub deployment: Deployment, 44 | } 45 | 46 | /// Represents different network environments. 47 | #[derive(Debug, Clone)] 48 | pub enum Environment { 49 | Polygon, 50 | Mumbai, 51 | Amoy, 52 | Sandbox, 53 | } 54 | 55 | impl FromStr for Environment { 56 | type Err = (); 57 | 58 | /// Parses a string into an `Environment` enum variant. 59 | /// 60 | /// # Arguments 61 | /// 62 | /// * `s` - A string slice representing the environment value. 63 | /// 64 | /// # Returns 65 | /// 66 | /// A `Result` containing the parsed `Environment` variant if successful, or an error if parsing fails. 67 | fn from_str(s: &str) -> Result { 68 | match s.to_lowercase().as_str() { 69 | "polygon" => Ok(Environment::Polygon), 70 | "mumbai" => Ok(Environment::Mumbai), 71 | "amoy" => Ok(Environment::Amoy), 72 | "sandbox" => Ok(Environment::Sandbox), 73 | _ => Err(()), 74 | } 75 | } 76 | } 77 | 78 | /// Maps an Ethereum environment to its corresponding chain ID. 79 | /// 80 | /// # Arguments 81 | /// 82 | /// * `environment` - The Ethereum environment to map to a chain ID. 83 | /// 84 | /// # Returns 85 | /// 86 | /// The chain ID corresponding to the provided Ethereum environment. 87 | /// 88 | /// # Errors 89 | /// 90 | /// An error is returned if the provided environment is invalid. 91 | #[allow(dead_code, unreachable_patterns)] 92 | pub fn environment_to_chain_id(environment: Environment) -> Result { 93 | match environment { 94 | Environment::Polygon => Ok(137), 95 | Environment::Mumbai | Environment::Sandbox => Ok(80001), 96 | Environment::Amoy => Ok(80002), 97 | _ => Err("Invalid environment"), 98 | } 99 | } 100 | 101 | /// Maps an Ethereum environment to its corresponding Lens Hub contract address. 102 | /// 103 | /// # Arguments 104 | /// 105 | /// * `environment` - The Ethereum environment to map to a Lens Hub contract address. 106 | /// 107 | /// # Returns 108 | /// 109 | /// The Lens Hub contract address corresponding to the provided Ethereum environment. 110 | /// 111 | /// # Errors 112 | /// 113 | /// An error is returned if the provided environment is invalid. 114 | #[allow(unreachable_patterns)] 115 | pub fn environment_to_lens_hub_contract( 116 | environment: &Environment, 117 | ) -> Result { 118 | match environment { 119 | Environment::Polygon => { 120 | Ok(Address::from_str("0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d").unwrap()) 121 | } 122 | Environment::Mumbai => { 123 | Ok(Address::from_str("0x60Ae865ee4C725cd04353b5AAb364553f56ceF82").unwrap()) 124 | } 125 | Environment::Amoy => { 126 | Ok(Address::from_str("0xA2574D9DdB6A325Ad2Be838Bd854228B80215148").unwrap()) 127 | } 128 | Environment::Sandbox => { 129 | Ok(Address::from_str("0x7582177F9E536aB0b6c721e11f383C326F2Ad1D5").unwrap()) 130 | } 131 | _ => Err("Invalid environment"), 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /momoka-rs/src/evm.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, time::Duration}; 2 | 3 | use ethers::{ 4 | providers::{Http, HttpRateLimitRetryPolicy, Provider, RetryClient, RetryClientBuilder}, 5 | utils::hex, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{ 10 | environment::{Deployment, Environment}, 11 | types::verifier_error::MomokaVerifierError, 12 | }; 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 15 | pub struct SigRequest { 16 | pub v: u8, 17 | pub r: [u8; 32], 18 | pub s: [u8; 32], 19 | pub deadline: u64, 20 | } 21 | 22 | /// Parses the signature string and constructs a `SigRequest` object. 23 | /// 24 | /// This function takes a signature string in hexadecimal format and decodes it into its 25 | /// corresponding components: `v`, `r`, `s`, and `deadline`. The resulting components are 26 | /// used to create a `SigRequest` object that represents the signature. 27 | /// 28 | /// # Arguments 29 | /// 30 | /// * `signature` - The signature string in hexadecimal format. 31 | /// * `deadline` - The deadline value associated with the signature. 32 | /// 33 | /// # Errors 34 | /// 35 | /// This function returns a `Result` which can contain a `MomokaVerifierError` if the 36 | /// signature fails to decode from hexadecimal format. 37 | /// 38 | /// # Examples 39 | /// 40 | /// ``` 41 | /// use crate::MomokaVerifierError; 42 | /// 43 | /// let signature = "0x1234567890abcdef"; 44 | /// let deadline = 1234567890; 45 | /// 46 | /// match parse_signature(signature, deadline) { 47 | /// Ok(sig_request) => println!("{:?}", sig_request), 48 | /// Err(err) => eprintln!("Failed to parse signature: {:?}", err), 49 | /// } 50 | /// ``` 51 | pub fn parse_signature(signature: &str, deadline: u64) -> Result { 52 | let bytes = hex::decode(&signature[2..]).map_err(|_| MomokaVerifierError::SimulationFailed)?; 53 | let v = bytes[64]; 54 | let mut r_bytes = [0u8; 32]; 55 | r_bytes.copy_from_slice(&bytes[..32]); 56 | let mut s_bytes = [0u8; 32]; 57 | s_bytes.copy_from_slice(&bytes[32..64]); 58 | 59 | Ok(SigRequest { 60 | v, 61 | r: r_bytes, 62 | s: s_bytes, 63 | deadline, 64 | }) 65 | } 66 | 67 | /// Represents the provider context, including the environment, node provider, and deployment details. 68 | #[derive(Debug)] 69 | pub struct ProviderContext { 70 | /// The environment configuration. 71 | pub environment: Environment, 72 | /// The node provider with retry capabilities. 73 | pub node: Provider>, 74 | /// The deployment details. 75 | pub deployment: Deployment, 76 | } 77 | 78 | /// Creates an EVM provider using the provided node URL. 79 | /// 80 | /// # Arguments 81 | /// 82 | /// * `node_url` - The URL of the Ethereum node to connect to. 83 | /// 84 | /// # Returns 85 | /// 86 | /// A provider instance with retry capabilities for interacting with the EVM. 87 | pub fn evm_provider(node_url: &str) -> Provider> { 88 | Provider::new( 89 | RetryClientBuilder::default() 90 | .rate_limit_retries(10) 91 | .timeout_retries(10) 92 | .initial_backoff(Duration::from_millis(500)) 93 | .build( 94 | Http::from_str(node_url).unwrap(), 95 | Box::::default(), 96 | ), 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /momoka-rs/src/http.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use serde::de::DeserializeOwned; 3 | use serde::Serialize; 4 | use std::thread::sleep; 5 | use std::time::Duration; 6 | 7 | const MAX_RETRIES: i32 = 5; 8 | 9 | /// Sends a POST request to the specified URL with a JSON-encoded body, and returns the response as a deserialized object. 10 | /// 11 | /// # Arguments 12 | /// 13 | /// * `url` - The URL to which the request will be sent. 14 | /// * `body` - The body of the request, which will be JSON-encoded and sent as the request payload. 15 | /// 16 | /// # Returns 17 | /// 18 | /// The deserialized response object, if the request was successful. 19 | /// 20 | /// # Errors 21 | /// 22 | /// This function will return an error if the request fails for any reason, or if the response cannot be deserialized into the specified type. 23 | /// 24 | /// # Example 25 | /// 26 | /// ``` 27 | /// use momoka_verifier::calls::post_with_timeout; 28 | /// use momoka_verifier::types::verifier_error::MomokaVerifierError; 29 | /// 30 | /// #[derive(serde::Deserialize)] 31 | /// struct ExampleResponse { 32 | /// message: String, 33 | /// value: i32, 34 | /// } 35 | /// 36 | /// async fn example_post_request() -> Result { 37 | /// let body = serde_json::json!({ 38 | /// "some_key": "some_value", 39 | /// "another_key": 42, 40 | /// }); 41 | /// let response = post_with_timeout::("http://example.com/api/endpoint", &body).await?; 42 | /// Ok(response) 43 | /// } 44 | /// ``` 45 | pub async fn post_with_timeout( 46 | url: &str, 47 | body: &TBody, 48 | ) -> Result 49 | where 50 | TBody: Serialize, 51 | TResponse: DeserializeOwned, 52 | { 53 | let mut retries = 0; 54 | 55 | loop { 56 | match post_request(url, body).await { 57 | Ok(response) => return Ok(response), 58 | Err(err) => { 59 | if retries >= MAX_RETRIES { 60 | return Err(err); 61 | } 62 | // sleep for 100ms and go again 63 | sleep(Duration::from_millis(100)); 64 | retries += 1; 65 | } 66 | } 67 | } 68 | } 69 | 70 | /// Performs a POST request with a timeout and JSON body serialization. 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `url` - The URL to send the POST request to. 75 | /// * `body` - The body of the request, to be serialized as JSON. 76 | /// 77 | /// # Returns 78 | /// 79 | /// A `Result` containing the deserialized response if the request is successful, or an `Error` if an error occurs. 80 | async fn post_request( 81 | url: &str, 82 | body: &TBody, 83 | ) -> Result 84 | where 85 | TBody: serde::Serialize, 86 | TResponse: serde::de::DeserializeOwned, 87 | { 88 | let client = Client::new(); 89 | 90 | let response = client 91 | .post(url) 92 | .timeout(Duration::from_millis(10000)) 93 | .header("Content-Type", "application/json") 94 | .json(body) 95 | .send() 96 | .await?; 97 | 98 | response.json().await 99 | } 100 | -------------------------------------------------------------------------------- /momoka-rs/src/logger.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::get_current_utc_string; 2 | 3 | /// A logger for printing log messages in different colors. 4 | pub struct Logger; 5 | 6 | #[allow(dead_code)] 7 | impl Logger { 8 | fn print_colored_message(&self, color_code: &str, content: &str) { 9 | println!( 10 | "{}LENS VERIFICATION NODE - {} - {}\x1b[0m", 11 | color_code, 12 | get_current_utc_string(), 13 | content 14 | ); 15 | } 16 | 17 | /// Prints an error message with the specified content in red. 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `content` - The content of the error message. 22 | pub fn error(&self, content: &str) { 23 | self.print_colored_message("\x1b[31m", content); // Red 24 | } 25 | 26 | /// Prints a warning message with the specified content in yellow. 27 | /// 28 | /// # Arguments 29 | /// 30 | /// * `content` - The content of the warning message. 31 | pub fn warning(&self, content: &str) { 32 | self.print_colored_message("\x1b[33m", content); // Yellow 33 | } 34 | 35 | /// Prints an informational message with the specified content in blue. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// * `content` - The content of the informational message. 40 | pub fn info(&self, content: &str) { 41 | self.print_colored_message("\x1b[36m", content); // Blue 42 | } 43 | 44 | /// Prints a success message with the specified content in green. 45 | /// 46 | /// # Arguments 47 | /// 48 | /// * `content` - The content of the success message. 49 | pub fn success(&self, content: &str) { 50 | self.print_colored_message("\x1b[38;2;0;128;0m", content); // Green 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /momoka-rs/src/submitter/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod state; 2 | -------------------------------------------------------------------------------- /momoka-rs/src/submitter/state.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use ethers::types::Address; 4 | 5 | use crate::environment::{Deployment, Environment}; 6 | 7 | /// Returns the list of authorized submitters for a given environment and deployment. 8 | /// 9 | /// This method will return a list of authorized submitters for a given environment and deployment. 10 | /// 11 | /// # Arguments 12 | /// 13 | /// * `environment` - An `Environment` value representing the target environment. 14 | /// * `deployment` - A `Deployment` value representing the target deployment. 15 | /// 16 | /// # Examples 17 | /// 18 | /// ``` 19 | /// use momoka_sdk::shared::{get_submitters, Environment, Deployment}; 20 | /// 21 | /// let environment = Environment::MUMBAI; 22 | /// let deployment = Deployment::STAGING; 23 | /// 24 | /// let submitters = get_submitters(&environment, &deployment); 25 | /// assert_eq!(submitters.len(), 1); 26 | /// ``` 27 | pub fn get_submitters(environment: &Environment, deployment: &Deployment) -> Vec
{ 28 | match deployment { 29 | Deployment::Production => match environment { 30 | Environment::Polygon => { 31 | vec![Address::from_str("0xBe29464B9784a0d8956f29630d8bc4D7B5737435").unwrap()] 32 | } 33 | Environment::Mumbai => { 34 | vec![Address::from_str("0xEE3E8f53df70C3A3eeDA2076CDCa17c451aa8F96").unwrap()] 35 | } 36 | Environment::Amoy => { 37 | vec![Address::from_str("0x085be9a079aB75608fB794f2D288A375856e3f60").unwrap()] 38 | } 39 | Environment::Sandbox => panic!("Not Supported"), 40 | }, 41 | Deployment::Staging => match environment { 42 | Environment::Polygon => panic!("Not Supported"), 43 | Environment::Mumbai => { 44 | vec![Address::from_str("0x122938FE0d1fC6e00EF1b814cD7e44677e99b4f7").unwrap()] 45 | } 46 | Environment::Amoy => { 47 | vec![Address::from_str("0xC1b3BF1D611f1148F1799E90f7342860499Ba9D9").unwrap()] 48 | } 49 | Environment::Sandbox => panic!("Not Supported"), 50 | }, 51 | Deployment::Local => match environment { 52 | Environment::Polygon => panic!("Not Supported"), 53 | Environment::Mumbai => { 54 | vec![Address::from_str("0x8Fc176aA6FC843D3422f0C1832f1b9E17be00C1c").unwrap()] 55 | } 56 | Environment::Amoy => { 57 | vec![Address::from_str("0xcD7739d0b2ceFAb809FEF4e839a55b2627B60205").unwrap()] 58 | } 59 | Environment::Sandbox => panic!("Not Supported"), 60 | }, 61 | } 62 | } 63 | 64 | /// Checks if a given address is an authorized submitter. 65 | /// 66 | /// This method will check if a given address is an authorized submitter for a given environment and deployment. 67 | /// 68 | /// # Arguments 69 | /// 70 | /// * `environment` - An `Environment` value representing the target environment. 71 | /// * `address` - A `&str` value representing the address to check. 72 | /// * `deployment` - A `Deployment` value representing the target deployment. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ``` 77 | /// use momoka_sdk::shared::{is_valid_submitter, Environment, Deployment}; 78 | /// 79 | /// let environment = Environment::MUMBAI; 80 | /// let deployment = Deployment::STAGING; 81 | /// 82 | /// let is_valid = is_valid_submitter(&environment, "0x122938FE0d1fC6e00EF1b814cD7e44677e99b4f7", &deployment); 83 | /// assert!(is_valid); 84 | /// ``` 85 | pub fn is_valid_submitter( 86 | environment: &Environment, 87 | address: &Address, 88 | deployment: &Deployment, 89 | ) -> bool { 90 | get_submitters(environment, deployment).contains(address) 91 | } 92 | -------------------------------------------------------------------------------- /momoka-rs/src/types/chain_proofs.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::H256; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::{eip721::TypedData, transaction::TransactionPointer}; 5 | 6 | /// Represents a collection of cryptographic proofs related to a chain publication, 7 | /// where the proofs are verified and validated by the chain. This proof structure is 8 | /// specific to the Lens Network protocol and depends on the format of typed data and 9 | /// the blockchain where the publication was recorded. 10 | /// 11 | /// `TTypedData` is the type of the typed data that is part of this publication, 12 | /// and `TPointer` is the type of the transaction pointer, which may be `Option` 13 | /// or a similar type depending on how it is being used. 14 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct ChainProofs 17 | where 18 | TTypedData: TypedData, 19 | TPointer: Into>, 20 | { 21 | /// The current chain publication 22 | pub this_publication: ChainPublication, 23 | 24 | /// The transaction pointer 25 | pub pointer: TPointer, 26 | } 27 | 28 | /// Represents a publication on the chain that has been signed by the delegate, 29 | /// and can be verified and validated by the chain. 30 | /// 31 | /// `TTypedData` is the type of the typed data that is part of this publication. 32 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct ChainPublication 35 | where 36 | TTypedData: TypedData, 37 | { 38 | /// The signature for the publication 39 | pub signature: String, 40 | 41 | /// Indicates whether the publication was signed by the delegate or not 42 | pub signed_by_delegate: bool, 43 | 44 | /// The deadline for the signature 45 | pub signature_deadline: u64, 46 | 47 | /// The typed data 48 | pub typed_data: TTypedData, 49 | 50 | /// The block hash 51 | pub block_hash: H256, 52 | 53 | /// The block number 54 | pub block_number: u64, 55 | 56 | /// The block timestamp 57 | pub block_timestamp: u64, 58 | } 59 | -------------------------------------------------------------------------------- /momoka-rs/src/types/eip721.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::{ 2 | transaction::eip712::{EIP712Domain, Eip712DomainType}, 3 | Address, U256, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::{hex::Hex, profile_id::ProfileId, publication_id::PublicationId}; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct TypedDataDomain { 12 | pub name: String, 13 | 14 | pub version: String, 15 | 16 | pub chain_id: u32, 17 | 18 | pub verifying_contract: Address, 19 | } 20 | 21 | impl TypedDataDomain { 22 | pub fn to_ethers_type(&self) -> EIP712Domain { 23 | EIP712Domain { 24 | name: Some(self.name.clone()), 25 | version: Some(self.version.clone()), 26 | chain_id: Some(U256::from(self.chain_id)), 27 | salt: None, 28 | verifying_contract: Some(self.verifying_contract), 29 | } 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 34 | pub struct EIP712TypedData { 35 | pub types: TTypes, 36 | 37 | pub domain: TypedDataDomain, 38 | 39 | pub value: TValue, 40 | } 41 | 42 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 43 | pub struct EIP712TypedDataValueBase { 44 | pub nonce: u32, 45 | 46 | pub deadline: u32, 47 | } 48 | 49 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 50 | #[serde(rename_all = "camelCase")] 51 | pub struct CreatePostEIP712TypedDataValue { 52 | pub profile_id: ProfileId, 53 | 54 | #[serde(rename = "contentURI")] 55 | pub content_uri: String, 56 | 57 | pub collect_module: Address, 58 | 59 | pub collect_module_init_data: Hex, 60 | 61 | pub reference_module: Address, 62 | 63 | pub reference_module_init_data: Hex, 64 | 65 | pub nonce: u64, 66 | 67 | pub deadline: u64, 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 71 | pub struct CreatePostEIP712Types { 72 | #[serde(rename = "PostWithSig")] 73 | pub post_with_sig: Vec, 74 | } 75 | 76 | pub type CreatePostEIP712TypedData = 77 | EIP712TypedData; 78 | 79 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 80 | #[serde(rename_all = "camelCase")] 81 | pub struct CreateCommentEIP712TypedDataValue { 82 | pub profile_id: ProfileId, 83 | 84 | pub profile_id_pointed: ProfileId, 85 | 86 | pub pub_id_pointed: PublicationId, 87 | 88 | #[serde(rename = "contentURI")] 89 | pub content_uri: String, 90 | 91 | pub reference_module: Address, 92 | 93 | pub collect_module: Address, 94 | 95 | pub collect_module_init_data: Hex, 96 | 97 | pub reference_module_init_data: Hex, 98 | 99 | pub reference_module_data: Hex, 100 | 101 | pub nonce: u64, 102 | 103 | pub deadline: u64, 104 | } 105 | 106 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 107 | pub struct CreateCommentEIP712Types { 108 | #[serde(rename = "CommentWithSig")] 109 | pub comment_with_sig: Vec, 110 | } 111 | 112 | pub type CreateCommentEIP712TypedData = 113 | EIP712TypedData; 114 | 115 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 116 | #[serde(rename_all = "camelCase")] 117 | pub struct CreateMirrorEIP712TypedDataValue { 118 | pub profile_id: ProfileId, 119 | 120 | pub profile_id_pointed: ProfileId, 121 | 122 | pub pub_id_pointed: PublicationId, 123 | 124 | pub reference_module_data: Hex, 125 | 126 | pub reference_module: Address, 127 | 128 | pub reference_module_init_data: Hex, 129 | 130 | pub nonce: u64, 131 | 132 | pub deadline: u64, 133 | } 134 | 135 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 136 | pub struct CreateMirrorEIP712Types { 137 | #[serde(rename = "MirrorWithSig")] 138 | pub mirror_with_sig: Vec, 139 | } 140 | 141 | pub type CreateMirrorEIP712TypedData = 142 | EIP712TypedData; 143 | 144 | pub trait TypedData {} 145 | impl TypedData for Box where T: TypedData + ?Sized {} 146 | impl TypedData for CreatePostEIP712TypedData {} 147 | impl TypedData for CreateCommentEIP712TypedData {} 148 | impl TypedData for CreateMirrorEIP712TypedData {} 149 | -------------------------------------------------------------------------------- /momoka-rs/src/types/evm_event.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::Address; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::{hex::Hex, profile_id::ProfileId, publication_id::PublicationId}; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct PostCreatedEventEmittedResponse { 9 | pub profile_id: ProfileId, 10 | 11 | pub pub_id: PublicationId, 12 | 13 | #[serde(rename = "contentURI")] 14 | pub content_uri: String, 15 | 16 | pub collect_module: Address, 17 | 18 | pub collect_module_return_data: Hex, 19 | 20 | pub reference_module: Address, 21 | 22 | pub reference_module_return_data: Hex, 23 | 24 | pub timestamp: u64, 25 | } 26 | 27 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 28 | #[serde(rename_all = "camelCase")] 29 | pub struct CommentCreatedEventEmittedResponse { 30 | pub profile_id: ProfileId, 31 | 32 | pub pub_id: PublicationId, 33 | 34 | #[serde(rename = "contentURI")] 35 | pub content_uri: String, 36 | 37 | pub profile_id_pointed: ProfileId, 38 | 39 | pub pub_id_pointed: PublicationId, 40 | 41 | pub reference_module_data: Hex, 42 | 43 | pub collect_module: Address, 44 | 45 | pub collect_module_return_data: Hex, 46 | 47 | pub reference_module: Address, 48 | 49 | pub reference_module_return_data: Hex, 50 | 51 | pub timestamp: u64, 52 | } 53 | 54 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 55 | #[serde(rename_all = "camelCase")] 56 | pub struct MirrorCreatedEventEmittedResponse { 57 | pub profile_id: ProfileId, 58 | 59 | pub pub_id: PublicationId, 60 | 61 | pub profile_id_pointed: ProfileId, 62 | 63 | pub pub_id_pointed: PublicationId, 64 | 65 | pub reference_module_data: Hex, 66 | 67 | pub reference_module: Address, 68 | 69 | pub reference_module_return_data: Hex, 70 | 71 | pub timestamp: u64, 72 | } 73 | 74 | pub trait EvmEvent { 75 | fn get_timestamp(&self) -> u64; 76 | } 77 | 78 | impl EvmEvent for Box 79 | where 80 | T: EvmEvent + ?Sized, 81 | { 82 | fn get_timestamp(&self) -> u64 { 83 | (**self).get_timestamp() 84 | } 85 | } 86 | 87 | impl EvmEvent for PostCreatedEventEmittedResponse { 88 | fn get_timestamp(&self) -> u64 { 89 | self.timestamp 90 | } 91 | } 92 | 93 | impl EvmEvent for CommentCreatedEventEmittedResponse { 94 | fn get_timestamp(&self) -> u64 { 95 | self.timestamp 96 | } 97 | } 98 | 99 | impl EvmEvent for MirrorCreatedEventEmittedResponse { 100 | fn get_timestamp(&self) -> u64 { 101 | self.timestamp 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /momoka-rs/src/types/hex.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::U256; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | use std::fmt; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct Hex(Vec); 7 | 8 | #[allow(dead_code)] 9 | impl Hex { 10 | pub fn new(bytes: Vec) -> Self { 11 | Hex(bytes) 12 | } 13 | 14 | pub fn empty() -> Self { 15 | Hex(Vec::new()) 16 | } 17 | 18 | pub fn as_bytes(&self) -> &[u8] { 19 | &self.0 20 | } 21 | 22 | pub fn is_empty(&self) -> bool { 23 | self.0.is_empty() 24 | } 25 | 26 | pub fn as_str(&self) -> String { 27 | format!("0x{}", hex::encode(&self.0)) 28 | } 29 | } 30 | 31 | impl<'de> Deserialize<'de> for Hex { 32 | fn deserialize(deserializer: D) -> Result 33 | where 34 | D: Deserializer<'de>, 35 | { 36 | struct HexVisitor; 37 | 38 | impl<'de> serde::de::Visitor<'de> for HexVisitor { 39 | type Value = Hex; 40 | 41 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 42 | formatter.write_str("a hexadecimal string starting with '0x'") 43 | } 44 | 45 | fn visit_str(self, value: &str) -> Result 46 | where 47 | E: serde::de::Error, 48 | { 49 | if value == "0x" { 50 | Ok(Hex::empty()) 51 | } else { 52 | let hex_string = value.trim_start_matches("0x"); 53 | let bytes = hex::decode(hex_string).map_err(serde::de::Error::custom)?; 54 | Ok(Hex(bytes)) 55 | } 56 | } 57 | } 58 | 59 | deserializer.deserialize_str(HexVisitor) 60 | } 61 | } 62 | 63 | impl Serialize for Hex { 64 | fn serialize(&self, serializer: S) -> Result 65 | where 66 | S: Serializer, 67 | { 68 | let hex_string = format!("0x{}", hex::encode(&self.0)); 69 | serializer.serialize_str(&hex_string) 70 | } 71 | } 72 | 73 | impl From for Vec { 74 | fn from(hex: Hex) -> Self { 75 | hex.0 76 | } 77 | } 78 | 79 | impl From for U256 { 80 | fn from(hex: Hex) -> Self { 81 | U256::from_str_radix(&hex.as_str(), 16).unwrap() 82 | } 83 | } 84 | 85 | impl<'a> From<&'a Hex> for U256 { 86 | fn from(hex: &'a Hex) -> Self { 87 | U256::from_str_radix(&hex.as_str(), 16).expect("Invalid hex string") 88 | } 89 | } 90 | 91 | impl From for ethers::types::Bytes { 92 | fn from(bytes: Hex) -> Self { 93 | ethers::types::Bytes(bytes.0.into()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /momoka-rs/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chain_proofs; 2 | pub mod eip721; 3 | pub mod evm_event; 4 | pub mod hex; 5 | pub mod profile_id; 6 | pub mod publication_id; 7 | pub mod transaction; 8 | pub mod verifier_error; 9 | -------------------------------------------------------------------------------- /momoka-rs/src/types/publication_id.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::U256; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | use std::fmt; 4 | 5 | /// Represents a publication ID as a wrapper around U256. 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct PublicationId(U256); 8 | 9 | impl PublicationId { 10 | /// Creates a new PublicationId instance. 11 | pub fn new(value: U256) -> Self { 12 | PublicationId(value) 13 | } 14 | 15 | /// Returns the inner U256 value. 16 | pub fn into_inner(self) -> U256 { 17 | self.0 18 | } 19 | 20 | /// Returns the publication ID as a formatted hex string. 21 | pub fn as_str(&self) -> String { 22 | let mut hex_string = format!("{:x}", self.0); 23 | if hex_string.len() % 2 != 0 { 24 | hex_string.insert(0, '0'); 25 | } 26 | format!("0x{}", hex_string) 27 | } 28 | } 29 | 30 | impl From for PublicationId { 31 | fn from(value: U256) -> Self { 32 | PublicationId::new(value) 33 | } 34 | } 35 | 36 | impl Serialize for PublicationId { 37 | fn serialize(&self, serializer: S) -> Result 38 | where 39 | S: Serializer, 40 | { 41 | serializer.serialize_str(&self.as_str()) 42 | } 43 | } 44 | 45 | impl<'de> Deserialize<'de> for PublicationId { 46 | fn deserialize(deserializer: D) -> Result 47 | where 48 | D: Deserializer<'de>, 49 | { 50 | let hex_string = String::deserialize(deserializer)?; 51 | let publication_id = U256::from_str_radix(hex_string.trim_start_matches("0x"), 16) 52 | .map_err(serde::de::Error::custom)?; 53 | Ok(PublicationId::new(publication_id)) 54 | } 55 | } 56 | 57 | impl fmt::Display for PublicationId { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | write!(f, "{}", self.as_str()) 60 | } 61 | } 62 | 63 | impl From for U256 { 64 | fn from(publication_id: PublicationId) -> Self { 65 | U256::from_str_radix(&publication_id.as_str(), 16).unwrap() 66 | } 67 | } 68 | 69 | impl<'a> From<&'a PublicationId> for U256 { 70 | fn from(publication_id: &'a PublicationId) -> Self { 71 | U256::from_str_radix(&publication_id.as_str(), 16).expect("Invalid hex string") 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | 79 | #[test] 80 | fn test_publication_id_new() { 81 | let u256 = U256::from(42u64); 82 | let publication_id = PublicationId::new(u256); 83 | 84 | // Assert that the PublicationId constructor returns the expected value 85 | assert_eq!(publication_id, PublicationId(U256::from(42u64))); 86 | } 87 | 88 | #[test] 89 | fn test_publication_id_into_inner() { 90 | let u256 = U256::from(42u64); 91 | let publication_id = PublicationId::new(u256); 92 | 93 | // Assert that the into_inner method returns the expected value 94 | assert_eq!(publication_id.into_inner(), U256::from(42u64)); 95 | } 96 | 97 | #[test] 98 | fn test_publication_id_as_str() { 99 | let u256 = U256::from(42u64); 100 | let publication_id = PublicationId::new(u256); 101 | 102 | // Assert that the as_str method returns the expected value 103 | assert_eq!(publication_id.as_str(), "0x2a"); 104 | } 105 | 106 | #[test] 107 | fn test_publication_id_from_u256() { 108 | let u256 = U256::from(42u64); 109 | let publication_id = PublicationId::from(u256); 110 | 111 | // Assert that the From implementation returns the expected value 112 | assert_eq!(publication_id, PublicationId(U256::from(42u64))); 113 | } 114 | 115 | #[test] 116 | fn test_publication_id_serde() { 117 | let publiation_id = PublicationId::new(U256::from(1)); 118 | 119 | let serialized = serde_json::to_string(&publiation_id).unwrap(); 120 | let deserialized: PublicationId = serde_json::from_str(&serialized).unwrap(); 121 | 122 | assert_eq!(deserialized, publiation_id); 123 | } 124 | 125 | #[test] 126 | fn test_publication_id_fmt() { 127 | let u256 = U256::from(42u64); 128 | let publication_id = PublicationId::new(u256); 129 | 130 | // Assert that the Display implementation returns the expected value 131 | assert_eq!(format!("{}", publication_id), "0x2a"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /momoka-rs/src/types/verifier_error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{ 3 | error::Error, 4 | fmt::{Display, Formatter, Result}, 5 | }; 6 | use strum_macros::EnumString; 7 | 8 | #[derive(Debug, EnumString, Clone, PartialEq, Eq, Deserialize, Serialize)] 9 | pub enum MomokaVerifierError { 10 | /// This means it has an invalid transaction type 11 | InvalidTransactionType, 12 | /// This means it has an invalid transaction format and could not parse it (missing data from the object) 13 | InvalidTransactionFormat, 14 | /// This means the main signature has not been signed by the same payload as the data itself 15 | InvalidSignatureSubmitter, 16 | /// This means the submitted timestamp proof does not have a valid timestamp proof signature 17 | TimestampProofInvalidSignature, 18 | /// This means the type in the timestamp proofs do not match timestamp proofs are not portable 19 | TimestampProofInvalidType, 20 | /// This means the da id in the timestamp proofs do not match up timestamp proofs are not portable 21 | TimestampProofInvalidDAID, 22 | /// This means the timestamp proof uploaded was not done by a valid submitter 23 | TimestampProofNotSubmitter, 24 | /// We tried to call them 5 times and its errored out - this is not a bad proof but bundlr/arweave are having issues 25 | CannotConnectToBundlr, 26 | /// The DA tx could not be found or invalid on the bundlr/arweave nodes can happened if pasted it in wrong 27 | InvalidTxID, // NOT USED! 28 | /// This the typed data format is invalid (aka a invalid address type etc) 29 | InvalidFormattedTypedData, 30 | /// This means it can not read the block from the node 31 | BlockCantBeReadFromNode, 32 | /// This means it can not read the data from the node 33 | DataCantBeReadFromNode, 34 | /// This means the simulation was not able to be ran on the node, this does not mean that it would fail on chain, it means the nodes may of been down and needs rechecking 35 | SimulationNodeCouldNotRun, 36 | /// This means the simulation was not successful and got rejected on-chain or the result from the simulation did not match the expected result 37 | SimulationFailed, 38 | /// This means the event emitted from the simulation does not match the expected event 39 | EventMismatch, 40 | /// This means the event timestamp passed into the emitted event does not match the signature timestamp 41 | InvalidEventTimestamp, 42 | /// This means the deadline set in the typed data is not correct 43 | InvalidTypedDataDeadlineTimestamp, 44 | /// This means the generated publication id for the generic id does not match what it should be 45 | GeneratedPublicationIdMismatch, 46 | /// This means the pointer set in the chain proofs is not required but set anyway 47 | InvalidPointerSetNotNeeded, 48 | /// This means the pointer has failed verification 49 | PointerFailedVerification, 50 | /// This means the block processed against is not the closest block to the timestamp proofs 51 | NotClosestBlock, 52 | /// This means the timestamp proofs are not close enough to the block 53 | BlockTooFar, // NOT USED! 54 | /// This means the publication submitted does not have a valid pointer and a pointer is required 55 | PublicationNoPointer, 56 | /// Some publications (comment and mirror) for now can only be on another DA publication not on evm chain publications 57 | PublicationNoneDA, 58 | /// This means the publication nonce is invalid at the time of submission 59 | PublicationNonceInvalid, 60 | /// This means the publication submisson was signed by a wallet that is not allowed 61 | PublicationSignerNotAllowed, 62 | /// This means the evm signature has already been used Only really starts to be able to be properly used when many submitters 63 | ChainSignatureAlreadyUsed, 64 | /// This means the publication submisson could not pass potentional due to a reorg 65 | PotentialReorg, 66 | /// internal cache has broken! 67 | CacheError, 68 | // bundlr could not find last transaction (most likely API down) 69 | NoLastTransactionFound, 70 | } 71 | 72 | impl Display for MomokaVerifierError { 73 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 74 | let error_name = self; 75 | write!(f, "{}", error_name) 76 | } 77 | } 78 | 79 | impl Error for MomokaVerifierError {} 80 | 81 | // Implement the Send trait for your error type 82 | unsafe impl Send for MomokaVerifierError {} 83 | -------------------------------------------------------------------------------- /momoka-rs/src/utils.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | 3 | /// Gets the current UTC time as a formatted string. 4 | /// 5 | /// The formatted string follows the format "Tue, 31 May 2022 13:45:00 GMT". 6 | /// 7 | /// # Returns 8 | /// 9 | /// A `String` containing the formatted UTC time. 10 | pub fn get_current_utc_string() -> String { 11 | let current_time = Utc::now(); 12 | let formatted_time = current_time.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); 13 | 14 | formatted_time 15 | } 16 | -------------------------------------------------------------------------------- /momoka-rs/src/verifier/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod proof; 2 | pub mod transactions; 3 | -------------------------------------------------------------------------------- /momoka-rs/src/verifier/transactions/common.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, str::FromStr}; 2 | 3 | use ethers::types::{ 4 | transaction::eip712::{EIP712Domain, Eip712, Eip712DomainType, TypedData}, 5 | Address, Signature, 6 | }; 7 | use serde::Serialize; 8 | use serde_json::Value; 9 | 10 | use crate::types::verifier_error::MomokaVerifierError; 11 | 12 | /// Recovers the address from a signed typed data using a given signature. 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `signature` - The signature to recover the address from. 17 | /// * `domain` - The EIP712 domain parameters. 18 | /// * `types` - The list of EIP712 domain types. 19 | /// * `value` - The value representing the typed data. 20 | /// * `primary_type_name` - The primary type name used for the typed data. 21 | /// 22 | /// # Returns 23 | /// 24 | /// The recovered address on success, or an error of type `MomokaVerifierError` if the 25 | /// operation fails. 26 | pub fn recovery_signed_typed_data( 27 | signature: &str, 28 | domain: &EIP712Domain, 29 | types: &[Eip712DomainType], 30 | value: &TValue, 31 | primary_type_name: String, 32 | ) -> Result { 33 | let mut types_map = BTreeMap::new(); 34 | types_map.insert(primary_type_name.to_owned(), types.to_vec()); 35 | 36 | let message: BTreeMap = serde_json::from_str( 37 | &serde_json::to_string(value) 38 | .map_err(|_| MomokaVerifierError::InvalidFormattedTypedData)?, 39 | ) 40 | .map_err(|_| MomokaVerifierError::InvalidFormattedTypedData)?; 41 | 42 | let typed_data: TypedData = TypedData { 43 | domain: domain.clone(), 44 | types: types_map, 45 | primary_type: primary_type_name, 46 | message, 47 | }; 48 | 49 | let hash = typed_data 50 | .encode_eip712() 51 | .map_err(|_| MomokaVerifierError::InvalidFormattedTypedData)?; 52 | 53 | let signature = Signature::from_str(signature) 54 | .map_err(|_| MomokaVerifierError::InvalidFormattedTypedData)?; 55 | 56 | let address = signature 57 | .recover(hash) 58 | .map_err(|_| MomokaVerifierError::InvalidFormattedTypedData)?; 59 | 60 | Ok(address) 61 | } 62 | -------------------------------------------------------------------------------- /momoka-rs/src/verifier/transactions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod comment; 2 | mod common; 3 | pub mod mirror; 4 | pub mod post; 5 | --------------------------------------------------------------------------------