├── .envrc ├── .gitmodules ├── .npmignore ├── src ├── types.ts ├── hex.ts ├── cartesify │ ├── WrappedPromise.ts │ ├── InputAddedListener.ts │ ├── FetchLikeClient.ts │ ├── Cartesify.ts │ ├── AxiosLikeClientV2.ts │ └── AxiosLikeClient.ts ├── models │ ├── input-transactor.ts │ └── config.ts ├── utils.ts ├── services │ └── InputTransactorService.ts ├── configs │ └── token-config.json └── index.ts ├── .prettierignore ├── .prettierrc ├── .vscode ├── settings.json └── tasks.json ├── wagmi.config.ts ├── DEV2DEV.md ├── flake.nix ├── tests ├── cartesify │ ├── AxiosLikeClient.test.ts │ ├── AxiosLikeClientV2.test.ts │ └── FetchClient.test.ts ├── services │ └── InputTransactorService.test.ts └── main.test.ts ├── flake.lock ├── package.json ├── .github └── workflows │ └── cartesify.yml ├── .gitignore ├── README.md ├── jest.config.ts └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cartesify-backend"] 2 | path = cartesify-backend 3 | url = git@github.com:Calindra/cartesify-backend.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | .parcel-cache 4 | /tests 5 | /backend-example 6 | /bin 7 | /cartesify-backend 8 | calindra-cartesify*.tgz 9 | /tests 10 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ObjectLike = Record;export interface Log { 2 | info(...args: unknown[]): void; 3 | error(...args: unknown[]): void; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | package.json 3 | package-lock.json 4 | tsconfig.json 5 | flake.lock 6 | pnpm-lock.yaml 7 | .vscode/ 8 | .prettierrc 9 | jest.config.ts 10 | wagmi.config.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": false, 6 | "useTabs": false, 7 | "arrowParens": "always", 8 | "printWidth": 120 9 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "brunodo", 4 | "cartesify", 5 | "Cartesify", 6 | "github", 7 | "nohup", 8 | "sunodo", 9 | "wagmi" 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "test", 7 | "group": "test", 8 | "problemMatcher": [], 9 | "label": "npm: test", 10 | "detail": "jest --detectOpenHandles", 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /wagmi.config.ts: -------------------------------------------------------------------------------- 1 | import hardhatDeploy from "@sunodo/wagmi-plugin-hardhat-deploy"; 2 | import { defineConfig } from "@wagmi/cli"; 3 | 4 | export default defineConfig({ 5 | out: "src/contracts.ts", 6 | plugins: [hardhatDeploy({ directory: "node_modules/@cartesi/rollups/export/abi", includes: [/InputBox/] })], 7 | }); 8 | -------------------------------------------------------------------------------- /src/hex.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLike } from "./types"; 2 | 3 | export class Hex { 4 | static hex2a(hex: string) { 5 | let str = ""; 6 | 7 | for (let i = 0; i < hex.length; i += 2) { 8 | let v = parseInt(hex.substring(i, i + 2), 16); 9 | if (v) str += String.fromCharCode(v); 10 | } 11 | return str; 12 | } 13 | 14 | static obj2hex(obj: ObjectLike): string { 15 | return "0x" + Buffer.from(JSON.stringify(obj)).toString("hex").toUpperCase(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/cartesify/WrappedPromise.ts: -------------------------------------------------------------------------------- 1 | export class WrappedPromise { 2 | promise: Promise 3 | reject?: (reason?: any) => void; 4 | resolve?: (value: CartesifyBackendReport | PromiseLike) => void; 5 | constructor() { 6 | this.promise = new Promise((resolve, reject) => { 7 | this.resolve = resolve; 8 | this.reject = reject; 9 | }) 10 | } 11 | } 12 | 13 | export interface CartesifyBackendReport { 14 | command: string 15 | success?: any 16 | error?: any 17 | } 18 | -------------------------------------------------------------------------------- /DEV2DEV.md: -------------------------------------------------------------------------------- 1 | # How to develop 2 | 3 | You need the Cartesi CLI or nonodo. Then run the frontend. 4 | 5 | Please use pnpm. 6 | 7 | ## With Cartesi CLI 8 | 9 | ```shell 10 | git clone git@github.com:Calindra/cartesify-rock-paper-scissors.git 11 | cd cartesify-rock-paper-scissors/backend 12 | cartesi build 13 | cartesi run 14 | ``` 15 | 16 | ## With nonodo 17 | Run nonodo 18 | ```shell 19 | nonodo 20 | ``` 21 | 22 | Clone and run the express example: 23 | ```shell 24 | git clone git@github.com:Calindra/cartesify-rock-paper-scissors.git 25 | cd cartesify-rock-paper-scissors/backend 26 | yarn 27 | cd expressjs 28 | node app.js 29 | ``` 30 | 31 | Run deroll 32 | 33 | ```shell 34 | yarn start 35 | ``` 36 | 37 | ## Frontend 38 | 39 | ```shell 40 | yarn test fetch --watchAll 41 | ``` 42 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Node.js development environment with PNPM"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = 10 | { nixpkgs, flake-utils, ... }: 11 | flake-utils.lib.eachDefaultSystem ( 12 | system: 13 | let 14 | pkgs = nixpkgs.legacyPackages.${system}; 15 | in 16 | { 17 | devShells.default = pkgs.mkShell { 18 | buildInputs = with pkgs; [ 19 | nodejs_22 20 | pnpm 21 | ]; 22 | 23 | shellHook = '' 24 | echo "Node.js and PNPM development environment" 25 | node --version 26 | pnpm --version 27 | ''; 28 | }; 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/models/input-transactor.ts: -------------------------------------------------------------------------------- 1 | import type { Signer } from "ethers" 2 | import type { TypedDataDomain, TypedDataDefinition, TypedDataParameter } from "viem" 3 | 4 | export type MessageField = TypedDataParameter; 5 | 6 | export type PrimaryType = "AvailMessage"; 7 | 8 | export type TypeDataTypes = Record 9 | 10 | export interface TypedData extends TypedDataDefinition { 11 | account: string; 12 | message: InputTransactorMessageWithNonce; 13 | } 14 | 15 | export interface WalletConfig { 16 | walletClient: Signer; 17 | connectedChainId: string; 18 | } 19 | 20 | export interface InputTransactorConfig { 21 | inputTransactorType: string; 22 | domain: TypedDataDomain; 23 | } 24 | 25 | export interface InputTransactorMessage { 26 | data: string; 27 | [key: string]: any; 28 | } 29 | 30 | export interface InputTransactorMessageWithNonce extends InputTransactorMessage { 31 | nonce: number; 32 | } -------------------------------------------------------------------------------- /tests/cartesify/AxiosLikeClient.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from "@jest/globals"; 2 | import { AxiosLikeClient } from "../../src/cartesify/AxiosLikeClient"; 3 | import { CartesiClientBuilder } from "../../src"; 4 | import { ethers } from "ethers"; 5 | 6 | describe.skip("AxiosLikeClient", () => { 7 | const endpoint = new URL("http://localhost:8545/"); 8 | 9 | it("should do a post", async () => { 10 | const provider = ethers.getDefaultProvider(endpoint.href); 11 | 12 | const cartesiClient = new CartesiClientBuilder() 13 | .withDappAddress('0x70ac08179605AF2D9e75782b8DEcDD3c22aA4D0C') 14 | .withEndpoint(endpoint) 15 | .withProvider(provider) 16 | .build(); 17 | const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' 18 | let walletWithProvider = new ethers.Wallet(privateKey, provider); 19 | 20 | cartesiClient.setSigner(walletWithProvider) 21 | 22 | const axios = AxiosLikeClient.create({ cartesiClient, baseURL: 'http://127.0.0.1:8383' }) 23 | const res = await axios.post('/hit', { amount: 123 }) as any 24 | console.log(res) 25 | expect(res.data).toBeDefined() 26 | }, 30000) 27 | 28 | }) 29 | -------------------------------------------------------------------------------- /src/models/config.ts: -------------------------------------------------------------------------------- 1 | import { AddressLike, Provider, Signer } from "ethers"; 2 | import { CartesiClient } from ".."; 3 | import { FetchOptions } from "../cartesify/FetchLikeClient"; 4 | import { TypedDataDomain } from "viem"; 5 | 6 | export interface Config { 7 | headers?: any; 8 | signer?: Signer; 9 | cartesiClient?: CartesiClient; 10 | } 11 | 12 | export interface AxiosSetupOptions { 13 | endpoints: { 14 | graphQL: URL; 15 | inspect: URL; 16 | }; 17 | provider?: Provider; 18 | signer?: Signer; 19 | dappAddress: AddressLike; 20 | baseURL?: string; 21 | } 22 | 23 | export interface InputTransactorOptions { 24 | endpoints: { 25 | graphQL: URL; 26 | inspect: URL; 27 | }; 28 | provider: Provider; 29 | signer: Signer; 30 | dappAddress: AddressLike; 31 | inputTransactorType: string; 32 | domain?: TypedDataDomain; 33 | } 34 | 35 | export interface DeleteConfig extends Config { 36 | data: Record; 37 | } 38 | 39 | export interface AxiosClient { 40 | get: (url: string, init?: FetchOptions) => Promise; 41 | post: (url: string, data?: Record, init?: Config) => Promise; 42 | put: (url: string, data?: Record, init?: Config) => Promise; 43 | patch: (url: string, data?: Record, init?: Config) => Promise; 44 | delete: (url: string, init?: DeleteConfig) => Promise; 45 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "./hex"; 2 | import { MessageField, PrimaryType } from "./models/input-transactor"; 3 | import type { ObjectLike } from "./types"; 4 | import { STATUS_CODES } from "node:http" 5 | 6 | export class Utils { 7 | static isObject(value: unknown): value is ObjectLike { 8 | return typeof value === "object" && value !== null; 9 | } 10 | 11 | static isArrayNonNullable(value: unknown): value is Array { 12 | return Array.isArray(value) && value.length > 0; 13 | } 14 | 15 | static hex2str(hex: string) { 16 | return Hex.hex2a(hex); 17 | } 18 | 19 | static hex2str2json(hex: string) { 20 | const str = Utils.hex2str(hex.replace(/^0x/, "")); 21 | return JSON.parse(str); 22 | } 23 | 24 | static onlyAllowXHeaders(headers: unknown) { 25 | if (!Utils.isObject(headers)) { 26 | return {}; 27 | } 28 | return Object.entries(headers).reduce>((acc, [key, value]) => { 29 | if (key.toLowerCase().startsWith("x-")) { 30 | acc[key] = value; 31 | } 32 | return acc; 33 | }, {}); 34 | } 35 | 36 | static httpStatusMap = STATUS_CODES; 37 | 38 | static messageMap = new Map([ 39 | [ 40 | "AvailMessage", 41 | [ 42 | { name: "app", type: "address" }, 43 | { name: "nonce", type: "uint32" }, 44 | { name: "max_gas_price", type: "uint128" }, 45 | { name: "data", type: "string" }, 46 | ] 47 | ], 48 | ]); 49 | 50 | static inputTransactorTypeMap = new Map([ 51 | [ 52 | "Avail", "AvailMessage" 53 | ], 54 | ]); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1732521221, 24 | "narHash": "sha256-2ThgXBUXAE1oFsVATK1ZX9IjPcS4nKFOAjhPNKuiMn0=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "4633a7c72337ea8fd23a4f2ba3972865e3ec685d", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@calindra/cartesify", 3 | "version": "1.1.0", 4 | "description": "Web3 Client to Send Input to dApp", 5 | "source": "src/index.ts", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "watch": "parcel watch", 10 | "build:parcel": "parcel build --dist-dir dist/", 11 | "build": "run-s wagmi build:parcel", 12 | "test": "jest --detectOpenHandles", 13 | "test:ci": "jest --detectOpenHandles --forceExit $(pwd)/tests", 14 | "wagmi": "wagmi generate", 15 | "clean:parcel": "rimraf .parcel-cache", 16 | "clean:build": "rimraf dist", 17 | "clean:package": "rimraf calindra-cartesify-*.tgz", 18 | "build:clean": "run-p clean:parcel clean:build clean:package", 19 | "lint": "prettier --check ." 20 | }, 21 | "author": "Calindra", 22 | "license": "MIT", 23 | "private": false, 24 | "devDependencies": { 25 | "@jest/globals": "^29.7.0", 26 | "@parcel/packager-ts": "^2.10.3", 27 | "@parcel/transformer-typescript-types": "^2.12.0", 28 | "@sunodo/wagmi-plugin-hardhat-deploy": "^0.3.0", 29 | "@types/debug": "^4.1.12", 30 | "@types/node": "^22", 31 | "@wagmi/cli": "^2.1.2", 32 | "http-request-mock": "^1.8.17", 33 | "jest": "^29.7.0", 34 | "jest-environment-jsdom": "^29.7.0", 35 | "nock": "^13.5.1", 36 | "npm-run-all": "^4.1.5", 37 | "parcel": "^2.10.1", 38 | "rimraf": "^6.0.1", 39 | "ts-jest": "^29.1.1", 40 | "ts-node": "^10.9.2", 41 | "typescript": "^5.2.2" 42 | }, 43 | "engines": { 44 | "node": ">=20.0.0" 45 | }, 46 | "dependencies": { 47 | "@cartesi/rollups": "^1.4.0", 48 | "axios": "^1.6.7", 49 | "debug": "^4.3.4", 50 | "ethers": "^6.11.1", 51 | "prettier": "^3.4.1", 52 | "viem": "^2.21.12" 53 | }, 54 | "directories": { 55 | "test": "tests" 56 | }, 57 | "volta": { 58 | "node": "20.12.0" 59 | }, 60 | "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab" 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/cartesify.yml: -------------------------------------------------------------------------------- 1 | name: Cartesify CI 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | jobs: 7 | build: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | submodules: "recursive" 14 | - name: Install Foundry 15 | uses: foundry-rs/foundry-toolchain@v1 16 | with: 17 | version: "nightly-2cdbfaca634b284084d0f86357623aef7a0d2ce3" 18 | 19 | - name: Install brunodo 20 | run: | 21 | npm i -g nonodo@1.2.8 22 | 23 | - name: Run nonodo in background 24 | run: | 25 | export PACKAGE_NONODO_VERSION=1.0.0 26 | nohup nonodo & 27 | 28 | # Setup .npmrc file to publish to npm 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: "22.x" 32 | registry-url: "https://registry.npmjs.org" 33 | - name: Run deroll in background 34 | run: | 35 | cd cartesify-backend/example 36 | npm ci 37 | nohup node app.js & 38 | cd - 39 | corepack pnpm install --frozen-lockfile 40 | corepack pnpm run wagmi 41 | corepack pnpm run test:ci 42 | corepack pnpm run build 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | 46 | publish: 47 | name: Publish 48 | runs-on: ubuntu-latest 49 | needs: [build] 50 | if: startsWith(github.ref, 'refs/tags/v') 51 | steps: 52 | - uses: actions/checkout@v4 53 | with: 54 | submodules: "recursive" 55 | 56 | # Setup .npmrc file to publish to npm 57 | - uses: actions/setup-node@v4 58 | with: 59 | node-version: "22.x" 60 | registry-url: "https://registry.npmjs.org" 61 | - name: Publish 62 | run: | 63 | corepack pnpm install --frozen-lockfile 64 | corepack pnpm run wagmi 65 | corepack pnpm run build 66 | corepack pnpm publish --access public --no-git-checks 67 | env: 68 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .sunodo 133 | .cartesi 134 | src/contracts.ts 135 | 136 | # pnpm describe on package.json via corepack 137 | package-lock.json 138 | 139 | .direnv -------------------------------------------------------------------------------- /tests/services/InputTransactorService.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import InputTransactorService from "../../src/services/InputTransactorService"; 3 | import { Address } from "viem"; 4 | import { ethers } from "ethers"; 5 | import { TypedData, WalletConfig } from "../../src/models/input-transactor"; 6 | 7 | describe.skip("InputTransactorService", () => { 8 | it("should return an object types", () => { 9 | const primaryType = "AvailMessage" 10 | const types: any = InputTransactorService.buildTypes(primaryType) 11 | 12 | const expectMessage = [ 13 | { name: "app", type: "address" }, 14 | { name: "nonce", type: "uint32" }, 15 | { name: "max_gas_price", type: "uint128" }, 16 | { name: "data", type: "string" } 17 | ] 18 | 19 | expect(typeof types).toBe("object"); 20 | expect(types).toHaveProperty("AvailMessage"); 21 | 22 | expect(types["AvailMessage"]).toEqual(expectMessage); 23 | }) 24 | 25 | it("should return nonce", async () => { 26 | const nonce: any = await InputTransactorService.getNonce("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f", '0x7a69') 27 | expect(nonce).toEqual(1); 28 | }) 29 | 30 | it("should assign", async () => { 31 | const provider = ethers.getDefaultProvider("http://localhost:8545"); 32 | const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' 33 | let signer = new ethers.Wallet(privateKey, provider); 34 | const walletConfig: WalletConfig = { 35 | walletClient: signer, 36 | connectedChainId: "0x7a69" 37 | } 38 | 39 | const account = await signer.getAddress() 40 | const typedData: TypedData = { 41 | account, 42 | domain: { 43 | name: "AvailM", 44 | version: "1", 45 | chainId: 31337, 46 | verifyingContract: 47 | "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" as Address, 48 | }, 49 | types: { 50 | AvailMessage: [ 51 | { name: "app", type: "address" }, 52 | { name: "nonce", type: "uint32" }, 53 | { name: "max_gas_price", type: "uint128" }, 54 | { name: "data", type: "string" }, 55 | ], 56 | }, 57 | primaryType: "AvailMessage", 58 | message: { 59 | app: "0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e", 60 | data: "0xHello", 61 | max_gas_price: "10", 62 | nonce: 1, 63 | primaryType: "AvailMessage", 64 | } 65 | } 66 | const response = await InputTransactorService.assingInputMessage(walletConfig, typedData) 67 | expect(typeof response).toBe("object"); 68 | expect(response).toHaveProperty("signature"); 69 | expect(response).toHaveProperty("typedData"); 70 | }) 71 | 72 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cartesify 2 | 3 | ## Introduction 4 | 5 | Cartesify is a powerful web3 client that allows you to interact with the Cartesi Machine. It enables you to send transactions, query data, and seamlessly interact with your backend using a REST-like approach. 6 | 7 | ## Getting Started 8 | 9 | The easiest way to get started with Cartesify is by simply cloning the template from: [cartesify-template](https://github.com/Calindra/cartesify-template). 10 | 11 | ### Installation 12 | 13 | To use our web3 client, follow these simple installation steps: 14 | 15 | ```shell 16 | npm install @calindra/cartesify 17 | ``` 18 | 19 | Pay attention: @calindra/cartesify is intended to be used with [@calindra/cartesify-backend](https://github.com/Calindra/cartesify-backend) 20 | 21 | ### Usage 22 | 23 | 1. Import and configure the Web3 library into your project: 24 | 25 | ```ts 26 | import { Cartesify } from "@calindra/cartesify"; 27 | 28 | const CARTESI_INSPECT_ENDPOINT = "http://localhost:8080/inspect"; 29 | 30 | // replace with the content of your dapp address (it could be found on dapp.json) 31 | const DAPP_ADDRESS = "0x70ac08179605AF2D9e75782b8DEcDD3c22aA4D0C"; 32 | 33 | const fetch = Cartesify.createFetch({ 34 | dappAddress: "0x70ac08179605AF2D9e75782b8DEcDD3c22aA4D0C", 35 | endpoints: { 36 | graphQL: new URL("http://localhost:8080/graphql"), 37 | inspect: new URL("http://localhost:8080/inspect"), 38 | }, 39 | }); 40 | ``` 41 | 42 | 2. Connect to MetaMask and get the signer: 43 | 44 | ```ts 45 | import { ethers } from "ethers"; 46 | 47 | async function getSigner() { 48 | try { 49 | await window.ethereum.request({ method: "eth_requestAccounts" }); 50 | const provider = new ethers.providers.Web3Provider(window.ethereum); 51 | return provider.getSigner(); 52 | } catch (error) { 53 | console.log(error); 54 | alert("Connecting to metamask failed."); 55 | } 56 | } 57 | ``` 58 | 59 | 3. Start interacting with the Cartesi Machine: 60 | 61 | ```ts 62 | const response = await fetch("http://127.0.0.1:8383/echo", { 63 | method: "POST", 64 | headers: { 65 | "Content-Type": "application/json", 66 | }, 67 | /** 68 | * The body always needs to be a string 69 | */ 70 | body: JSON.stringify({ any: "body" }), 71 | signer, // <- the signer 72 | }); 73 | console.log(response.ok); // will print true 74 | const json = await response.json(); 75 | console.log(json); // will print the backend response as json 76 | ``` 77 | 78 | ## Using the `sendMessage` Method in Cartesify 79 | 80 | This guide provides a step-by-step approach to using the `sendMessage` method in the Cartesify library. 81 | 82 | ### Step 1: Create the Input Transactor 83 | 84 | You need to create an input transactor using the createInputTransactor method. Provide the necessary parameters: 85 | 86 | ```ts 87 | const transactor = await Cartesify.createInputTransactor({ 88 | endpoints: { 89 | graphQL: new URL("https://example.com/graphql"), // Replace with your GraphQL endpoint 90 | inspect: new URL("https://example.com/inspect"), // Replace with your inspection endpoint 91 | }, 92 | provider: myProvider, // Your configured Ethereum provider 93 | signer: mySigner, // Your configured Ethereum signer 94 | dappAddress: "0xDappAddress", // Replace with your dApp address 95 | inputTransactorType: "Avail", // Specify your input transactor type 96 | domain: { 97 | name: "ExampleDomain", 98 | version: "1", 99 | chainId: 1, 100 | verifyingContract: "0xExampleContract", // Replace with your contract address 101 | }, // This field is optional. If not provided, a default configuration will be used. 102 | }); 103 | ``` 104 | 105 | ### Step 2: Prepare the Message 106 | 107 | Create the message object that will be sent to the dApp. Ensure it adheres to the required structure: 108 | 109 | ```ts 110 | const payload = { key: "value" }; // Your message data 111 | 112 | const message = { 113 | app: , // App identifier 114 | data: typeof payload === 'string' ? payload : JSON.stringify(payload), // Message data 115 | max_gas_price: "10", // Maximum gas price for the transaction 116 | }; 117 | 118 | ``` 119 | 120 | ### Step 3: Send the Message 121 | 122 | ```ts 123 | try { 124 | const result = await transactor.sendMessage(message); 125 | console.log("Message sent successfully:", result); 126 | } catch (error) { 127 | console.error("Failed to send message:", error); 128 | } 129 | ``` 130 | -------------------------------------------------------------------------------- /src/services/InputTransactorService.ts: -------------------------------------------------------------------------------- 1 | import { InputTransactorConfig, InputTransactorMessage, PrimaryType, TypedData, WalletConfig } from "../models/input-transactor" 2 | import { Utils } from "../utils" 3 | import configFile from "../configs/token-config.json" 4 | 5 | export default class InputTransactorService { 6 | 7 | 8 | static sendMessage = async (walletConfig: WalletConfig, inputTransactorConfig: InputTransactorConfig, message: InputTransactorMessage) => { 9 | try { 10 | const typedData: TypedData = await InputTransactorService.createTypedData(walletConfig, inputTransactorConfig, message) 11 | const signedMessage = await InputTransactorService.assingInputMessage(walletConfig, typedData) 12 | 13 | const response = await fetch('http://localhost:8080/transactions', { 14 | method: 'POST', 15 | body: JSON.stringify(signedMessage), 16 | headers: { 'Content-Type': 'application/json' } 17 | }); 18 | 19 | if (!response.ok) { 20 | throw new Error(`HTTP error! Status: ${response.status}`); 21 | } 22 | return response 23 | } catch (e) { 24 | console.error("submit to Espresso failed.", e) 25 | throw e 26 | } 27 | } 28 | 29 | static assingInputMessage = async (walletConfig: WalletConfig, typedData: TypedData) => { 30 | try { 31 | const { walletClient } = walletConfig 32 | const signature = await InputTransactorService.executeSign(walletClient, typedData) 33 | const signedMessage = { 34 | signature, 35 | typedData: btoa(JSON.stringify(typedData)) 36 | } 37 | 38 | return signedMessage 39 | } catch (e) { 40 | console.error("Error when try assign message. ", e) 41 | throw e 42 | } 43 | } 44 | 45 | static executeSign = async (instance: any, typedData: TypedData) => { 46 | const { domain, types, message } = typedData 47 | if ('_signTypedData' in instance && typeof instance._signTypedData === 'function') { 48 | return await instance._signTypedData(domain, types, message); 49 | } else if ('signTypedData' in instance && typeof instance.signTypedData === 'function') { 50 | return await instance.signTypedData(domain, types, message); 51 | } else { 52 | throw new Error('The instance is neither a Signer nor a JsonRpcSigner'); 53 | } 54 | } 55 | 56 | static createTypedData = async (walletConfig: WalletConfig, inputTransactorConfig: InputTransactorConfig, message: InputTransactorMessage): Promise => { 57 | try { 58 | let typedData: Partial = {} 59 | 60 | const { walletClient, connectedChainId } = walletConfig 61 | const { domain, inputTransactorType } = inputTransactorConfig 62 | const [account] = await walletClient.getAddress() 63 | const primaryType = Utils.inputTransactorTypeMap.get(inputTransactorType) 64 | 65 | if (!primaryType) { 66 | throw new Error("Invalid inputTransactorType"); 67 | } 68 | typedData.account = account 69 | typedData.types = InputTransactorService.buildTypes(primaryType) 70 | typedData.primaryType = primaryType 71 | typedData.domain = domain 72 | 73 | const nextNonce = await InputTransactorService.getNonce(account.toString(), connectedChainId) 74 | 75 | const updatedMessage = { 76 | ...message, 77 | nonce: nextNonce, 78 | } 79 | typedData.message = updatedMessage 80 | return typedData as TypedData 81 | } catch (e) { 82 | console.error("Failed to create typedData. ", e) 83 | throw e 84 | } 85 | } 86 | 87 | static buildTypes = (primaryType: PrimaryType) => { 88 | const messageContent = Utils.messageMap.get(primaryType) 89 | if (!messageContent) { 90 | throw new Error("Invalid primaryType") 91 | } 92 | const types = { 93 | [primaryType]: messageContent 94 | } 95 | return types 96 | } 97 | 98 | static getNonce = async (senderAccount: string, connectedChainId: string) => { 99 | try { 100 | const config: Record = configFile 101 | const url = `${config[connectedChainId].graphqlAPIURL}/graphql`; 102 | const query = ` 103 | {inputs(where: {msgSender: "${senderAccount}" type: "Espresso"}) { 104 | totalCount 105 | }}`; 106 | const response = await fetch(url, { 107 | method: 'POST', 108 | headers: { 109 | 'Content-Type': 'application/json' 110 | }, 111 | body: JSON.stringify({ query }) 112 | }); 113 | 114 | const responseData = await response.json(); 115 | const nextNonce = responseData.data.inputs.totalCount + 1; 116 | return nextNonce 117 | } catch (e) { 118 | console.error("Error: not found nonce. ", e) 119 | throw e 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/cartesify/InputAddedListener.ts: -------------------------------------------------------------------------------- 1 | import { CartesiClient } from ".."; 2 | import { Utils } from "../utils"; 3 | import { WrappedPromise } from "./WrappedPromise"; 4 | import debug from "debug"; 5 | 6 | /** 7 | * to see the logs run on terminal: 8 | * ``` 9 | * export DEBUG=cartesify:* 10 | * ``` 11 | */ 12 | const debugs = debug('cartesify:InputAddedListener') 13 | 14 | let listenerAdded = false 15 | 16 | const query = `query Report($index: Int!) { 17 | input(index: $index) { 18 | reports(last: 2) { 19 | edges { 20 | node { 21 | payload 22 | } 23 | } 24 | } 25 | } 26 | }` 27 | 28 | const defaultOptions: RequestInit = { 29 | "headers": { 30 | "accept": "*/*", 31 | "accept-language": "en-US,en;q=0.9,pt;q=0.8", 32 | "content-type": "application/json", 33 | "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Microsoft Edge\";v=\"120\"", 34 | "sec-ch-ua-mobile": "?0", 35 | "sec-ch-ua-platform": "\"macOS\"", 36 | "sec-fetch-dest": "empty", 37 | "sec-fetch-mode": "cors", 38 | "sec-fetch-site": "same-origin" 39 | }, 40 | "referrerPolicy": "strict-origin-when-cross-origin", 41 | "method": "POST", 42 | "mode": "cors", 43 | "credentials": "omit", 44 | } 45 | 46 | export class InputAddedListener { 47 | 48 | static requests: Record = {} 49 | 50 | endpointGraphQL: URL 51 | maxRetry = 30 52 | 53 | constructor(private cartesiClient: CartesiClient) { 54 | this.endpointGraphQL = cartesiClient.config.endpointGraphQL 55 | } 56 | 57 | async queryGraphQL(query: string, variables: Record) { 58 | const req = await fetch(this.endpointGraphQL, { 59 | ...defaultOptions, 60 | referrer: `${this.endpointGraphQL.toString()}`, 61 | body: JSON.stringify({ 62 | query, 63 | operationName: null, 64 | variables 65 | }), 66 | }); 67 | return await req.json() 68 | } 69 | 70 | getLastReportAsJSON(json: any) { 71 | if (json.data?.input.reports.edges.length > 0) { 72 | const lastEdge = json.data.input.reports.edges.length - 1 73 | const hex = json.data.input.reports.edges[lastEdge].node.payload 74 | return Utils.hex2str2json(hex) 75 | } 76 | } 77 | 78 | resolveOrRejectPromise(wPromise: WrappedPromise, lastReport: any) { 79 | if (lastReport.success) { 80 | wPromise.resolve!(lastReport) 81 | } else if (lastReport.error?.constructorName === "TypeError") { 82 | const typeError = new TypeError(lastReport.error.message) 83 | wPromise.reject!(typeError) 84 | } else if (lastReport.error) { 85 | wPromise.reject!(lastReport.error) 86 | } else { 87 | wPromise.reject!(new Error(`Unexpected cartesify response format from backend ${JSON.stringify(lastReport)}`)) 88 | } 89 | } 90 | 91 | async addListener() { 92 | const cartesiClient = this.cartesiClient; 93 | if (!cartesiClient) { 94 | throw new Error('You need to configure the Cartesi client') 95 | } 96 | if (listenerAdded) { 97 | return 98 | } 99 | listenerAdded = true 100 | const contract = await cartesiClient.getInputContract() 101 | contract.on("InputAdded", async (_dapp: string, inboxInputIndex: number, _sender: string, input: string) => { 102 | const start = Date.now() 103 | let attempt = 0; 104 | try { 105 | const payload = Utils.hex2str2json(input) 106 | const wPromise = InputAddedListener.requests[payload.requestId] 107 | if (!wPromise) { 108 | return 109 | } 110 | while (attempt < this.maxRetry) { 111 | try { 112 | attempt++; 113 | if (attempt > 1) { 114 | debugs(`waiting 1s to do the ${attempt} attempt. InputBoxIndex = ${inboxInputIndex}`) 115 | await new Promise((resolve) => setTimeout(resolve, 1000)) 116 | } 117 | const variables = { index: +inboxInputIndex.toString() } 118 | const gqlResponse = await this.queryGraphQL(query, variables) 119 | const lastReport = this.getLastReportAsJSON(gqlResponse) 120 | if (/^cartesify:/.test(lastReport?.command)) { 121 | this.resolveOrRejectPromise(wPromise, lastReport) 122 | return // exit loop and function 123 | } else { 124 | debugs(`its not a cartesify %O`, JSON.stringify(gqlResponse)) 125 | } 126 | } catch (e) { 127 | debugs('%O', e) 128 | } 129 | } 130 | wPromise.reject!(new Error(`Timeout after ${this.maxRetry} attempts`)) 131 | } catch (e) { 132 | debugs(e) 133 | } finally { 134 | debugs(`InputAdded: ${Date.now() - start}ms; attempts = ${attempt}`) 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/cartesify/FetchLikeClient.ts: -------------------------------------------------------------------------------- 1 | import { ContractTransactionResponse, Signer, ethers } from "ethers"; 2 | import { CartesiClient } from ".."; 3 | import { Utils } from "../utils"; 4 | import { WrappedPromise } from "./WrappedPromise"; 5 | import { InputAddedListener } from "./InputAddedListener"; 6 | 7 | export interface FetchOptions extends RequestInit { 8 | cartesiClient?: CartesiClient 9 | signer?: Signer 10 | } 11 | 12 | export type SetSigner = { 13 | setSigner(signer: Signer): void 14 | } 15 | 16 | export type FetchType = ( 17 | input: string | URL | globalThis.Request, 18 | init?: FetchOptions, 19 | ) => Promise 20 | 21 | export type FetchFun = FetchType & SetSigner; 22 | 23 | async function _fetch(url: string | URL | globalThis.Request, options?: FetchOptions) { 24 | if (options?.method === 'GET' || options?.method === undefined) { 25 | return doRequestWithInspect(url, options) 26 | } else if (options?.method === 'POST' || options?.method === 'PUT' || options?.method === 'PATCH' || options?.method === 'DELETE') { 27 | return doRequestWithAdvance(url, options) 28 | } 29 | throw new Error(`Method ${options?.method} not implemented.`); 30 | } 31 | 32 | async function doRequestWithAdvance(url: string | URL | globalThis.Request, options?: FetchOptions) { 33 | if (!options?.cartesiClient) { 34 | throw new Error('You need to configure the Cartesi client') 35 | } 36 | const cartesiClient = options.cartesiClient 37 | const { logger } = cartesiClient.config; 38 | try { 39 | new InputAddedListener(cartesiClient).addListener() 40 | const inputContract = await cartesiClient.getInputContract(); 41 | const requestId = `${Date.now()}:${Math.random()}` 42 | const wPromise = InputAddedListener.requests[requestId] = new WrappedPromise() 43 | // convert string to input bytes (if it's not already bytes-like) 44 | const inputBytes = ethers.toUtf8Bytes( 45 | JSON.stringify({ 46 | requestId, 47 | cartesify: { 48 | fetch: { 49 | url, 50 | options: { ...options, cartesiClient: undefined }, 51 | }, 52 | }, 53 | }) 54 | ); 55 | const dappAddress = await cartesiClient.getDappAddress(); 56 | 57 | // send transaction 58 | const tx = await inputContract.addInput(dappAddress, inputBytes) as ContractTransactionResponse; 59 | await tx.wait(1); 60 | const resp = (await wPromise.promise) as any 61 | const res = new Response(resp.success) 62 | return res 63 | } catch (e) { 64 | logger.error(`Error ${options?.method ?? 'GET'} ${url}`, e) 65 | throw e 66 | } 67 | } 68 | 69 | async function doRequestWithInspect(url: string | URL | globalThis.Request, options?: FetchOptions) { 70 | if (!options?.cartesiClient) { 71 | throw new Error('You need to configure the Cartesi client') 72 | } 73 | const that = options.cartesiClient as any; 74 | const { logger } = that.config; 75 | 76 | try { 77 | const inputJSON = JSON.stringify({ 78 | cartesify: { 79 | fetch: { 80 | url, 81 | options: { ...options, cartesiClient: undefined }, 82 | }, 83 | }, 84 | }); 85 | const jsonEncoded = encodeURIComponent(inputJSON); 86 | const urlInner = new URL(that.config.endpoint); 87 | urlInner.pathname += `/${jsonEncoded}`; 88 | const response = await fetch(urlInner.href, { 89 | method: "GET", 90 | headers: { 91 | "Content-Type": "application/json", 92 | Accept: "application/json", 93 | }, 94 | }); 95 | const result: unknown = await response.json(); 96 | 97 | if (Utils.isObject(result) && "reports" in result && Utils.isArrayNonNullable(result.reports)) { 98 | const lastReport = result.reports[result.reports.length - 1] 99 | if (Utils.isObject(lastReport) && "payload" in lastReport && typeof lastReport.payload === "string") { 100 | const payload = Utils.hex2str(lastReport.payload.replace(/^0x/, "")); 101 | const successOrError = JSON.parse(payload) 102 | if (successOrError.success) { 103 | return new Response(successOrError.success) 104 | } else if (successOrError.error) { 105 | if (successOrError.error?.constructorName === "TypeError") { 106 | throw new TypeError(successOrError.error.message) 107 | } else { 108 | throw successOrError.error 109 | } 110 | } 111 | } 112 | } 113 | throw new Error(`Wrong inspect response format.`) 114 | } catch (e) { 115 | logger.error(e); 116 | throw e; 117 | } 118 | 119 | } 120 | 121 | export { _fetch as fetch } 122 | 123 | class Response { 124 | 125 | ok: boolean = false 126 | status: number = 0 127 | type: string = "" 128 | headers = new Map() 129 | private rawData: string 130 | constructor(params: any) { 131 | this.ok = params.ok 132 | this.status = params.status 133 | this.type = params.type 134 | this.rawData = params.text 135 | if (params.headers) { 136 | this.headers = new Map(params.headers) 137 | } 138 | } 139 | 140 | async json() { 141 | return JSON.parse(this.rawData) 142 | } 143 | 144 | async text() { 145 | return this.rawData 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /src/cartesify/Cartesify.ts: -------------------------------------------------------------------------------- 1 | import { Network, Signer } from "ethers"; 2 | import { CartesiClient, CartesiClientBuilder } from ".."; 3 | import { AxiosLikeClient } from "./AxiosLikeClient"; 4 | import { FetchFun, FetchOptions, fetch as _fetch } from "./FetchLikeClient"; 5 | import { AxiosLikeClientV2 } from "./AxiosLikeClientV2"; 6 | import { Config, AxiosSetupOptions, DeleteConfig, AxiosClient, InputTransactorOptions } from "../models/config"; 7 | import { InputTransactorConfig, InputTransactorMessage, WalletConfig } from "../models/input-transactor"; 8 | import InputTransactorService from "../services/InputTransactorService"; 9 | import { Address, TypedDataDomain } from "viem"; 10 | export class Cartesify { 11 | 12 | axios: AxiosLikeClient 13 | 14 | constructor(cartesiClient: CartesiClient) { 15 | this.axios = new AxiosLikeClient(cartesiClient) 16 | } 17 | 18 | static createFetch(options: AxiosSetupOptions): FetchFun { 19 | const builder = new CartesiClientBuilder() 20 | .withDappAddress(options.dappAddress) 21 | .withEndpoint(options.endpoints.inspect) 22 | .withEndpointGraphQL(options.endpoints.graphQL) 23 | if (options.provider) { 24 | builder.withProvider(options.provider) 25 | } 26 | const cartesiClient = builder.build() 27 | if (options.signer) { 28 | cartesiClient.setSigner(options.signer) 29 | } 30 | const fetchFun = function (input: string | URL | globalThis.Request, init?: FetchOptions) { 31 | if (init?.signer) { 32 | cartesiClient.setSigner(init.signer) 33 | } 34 | return _fetch(input, { ...init, cartesiClient }) 35 | } 36 | fetchFun.setSigner = (signer: Signer) => { 37 | cartesiClient.setSigner(signer) 38 | } 39 | return fetchFun 40 | } 41 | 42 | static createAxios(options: AxiosSetupOptions): AxiosClient { 43 | const builder = new CartesiClientBuilder() 44 | .withDappAddress(options.dappAddress) 45 | .withEndpoint(options.endpoints.inspect) 46 | .withEndpointGraphQL(options.endpoints.graphQL); 47 | 48 | if (options.provider) { 49 | builder.withProvider(options.provider); 50 | } 51 | 52 | const cartesiClient = builder.build(); 53 | 54 | if (options.signer) { 55 | cartesiClient.setSigner(options.signer); 56 | } 57 | 58 | return { 59 | get: (url: string, init?: Config) => AxiosLikeClientV2.executeRequest(cartesiClient, options, url, "GET", init), 60 | post: (url: string, data?: Record, init?: Config) => AxiosLikeClientV2.executeRequest(cartesiClient, options, url, "POST", init, data), 61 | put: (url: string, data?: Record, init?: Config) => AxiosLikeClientV2.executeRequest(cartesiClient, options, url, "PUT", init, data), 62 | patch: (url: string, data?: Record, init?: Config) => AxiosLikeClientV2.executeRequest(cartesiClient, options, url, "PATCH", init, data), 63 | delete: (url: string, init?: DeleteConfig) => AxiosLikeClientV2.executeRequest(cartesiClient, options, url, "DELETE", init, init?.data) 64 | }; 65 | } 66 | 67 | static async createInputTransactor(options: InputTransactorOptions) { 68 | const builder = new CartesiClientBuilder() 69 | .withDappAddress(options.dappAddress) 70 | .withEndpoint(options.endpoints.inspect) 71 | .withEndpointGraphQL(options.endpoints.graphQL) 72 | if (options.provider) { 73 | builder.withProvider(options.provider) 74 | } 75 | const cartesiClient = builder.build() 76 | if (options.signer) { 77 | cartesiClient.setSigner(options.signer) 78 | } 79 | 80 | const { inputTransactorType, domain } = options 81 | 82 | const network = await options.signer.provider?.getNetwork(); 83 | if (!network || !network.chainId) { 84 | throw new Error("Failed to fetch network or chainId from the provider."); 85 | } 86 | 87 | const defaultDomain = await this.getDomain(domain, inputTransactorType, network); 88 | 89 | const inputTransactorConfig: InputTransactorConfig = { 90 | inputTransactorType: inputTransactorType, 91 | domain: defaultDomain 92 | } 93 | return { 94 | sendMessage: (message: InputTransactorMessage) => { 95 | const chainId = Number(network.chainId) 96 | const hexConnectedChainId = '0x' + chainId.toString(16) 97 | const wConfig: WalletConfig = { 98 | walletClient: options.signer, 99 | connectedChainId: hexConnectedChainId 100 | } 101 | return InputTransactorService.sendMessage(wConfig, inputTransactorConfig, message) 102 | } 103 | } 104 | } 105 | 106 | private static async getDomain(domain: TypedDataDomain | undefined, inputTransactorType: string, network: Network): Promise { 107 | try { 108 | if (domain) { 109 | return domain; 110 | } 111 | 112 | return { 113 | name: inputTransactorType, 114 | version: "1", 115 | chainId: Number(network.chainId), 116 | verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" as Address, 117 | } as TypedDataDomain; 118 | 119 | } catch (error) { 120 | console.error("Error generating default domain:", error); 121 | throw new Error("Unable to generate the default domain. Ensure the signer and network information are correct."); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/configs/token-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "0x7a69": { 3 | "token": "DummyETH", 4 | "label": "localhost", 5 | "rpcUrl": "http://localhost:8545", 6 | "graphqlAPIURL": "http://localhost:8080", 7 | "inspectAPIURL": "http://localhost:8080", 8 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 9 | "InputBoxAddress": "0x59b22D57D4f067708AB0c00552767405926dc768", 10 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 11 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 12 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 13 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 14 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 15 | }, 16 | "0xaa36a7": { 17 | "token": "SepETH", 18 | "label": "Sepolia Test Network", 19 | "rpcUrl": "https://eth-goerli.g.alchemy.com/v2/demo", 20 | "graphqlAPIURL": "http://localhost:8080", 21 | "inspectAPIURL": "http://localhost:8080", 22 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 23 | "InputBoxAddress": "0x58Df21fE097d4bE5dCf61e01d9ea3f6B81c2E1dB", 24 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 25 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 26 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 27 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 28 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 29 | }, 30 | "0x1a4": { 31 | "token": "GOP", 32 | "label": "Optimism Goerli Test Network", 33 | "rpcUrl": "https://opt-goerli.g.alchemy.com/v2/demo", 34 | "graphqlAPIURL": "http://localhost:8080", 35 | "inspectAPIURL": "http://localhost:8080", 36 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 37 | "InputBoxAddress": "0x59b22D57D4f067708AB0c00552767405926dc768", 38 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 39 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 40 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 41 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 42 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 43 | }, 44 | "0x66eed": { 45 | "token": "GAGO", 46 | "label": "Arbitrum Goerli Test Network", 47 | "rpcUrl": "https://goerli-rollup.arbitrum.io/rpc", 48 | "graphqlAPIURL": "http://localhost:8080", 49 | "inspectAPIURL": "http://localhost:8080", 50 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 51 | "InputBoxAddress": "0x59b22D57D4f067708AB0c00552767405926dc768", 52 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 53 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 54 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 55 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 56 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 57 | }, 58 | "0xa": { 59 | "token": "OP", 60 | "label": "Optimism Mainnet", 61 | "rpcUrl": "https://opt-mainnet.g.alchemy.com/v2/demo", 62 | "graphqlAPIURL": "http://localhost:8080", 63 | "inspectAPIURL": "http://localhost:8080", 64 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 65 | "InputBoxAddress": "0x59b22D57D4f067708AB0c00552767405926dc768", 66 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 67 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 68 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 69 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 70 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 71 | }, 72 | "0xa4b1": { 73 | "token": "AGO", 74 | "label": "Arbitrum One", 75 | "rpcUrl": "https://arb-mainnet.g.alchemy.com/v2/demo", 76 | "graphqlAPIURL": "http://localhost:8080", 77 | "inspectAPIURL": "http://localhost:8080", 78 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 79 | "InputBoxAddress": "0x59b22D57D4f067708AB0c00552767405926dc768", 80 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 81 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 82 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 83 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 84 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 85 | }, 86 | "0x1": { 87 | "token": "ETH", 88 | "label": "Ethereum Mainnet", 89 | "rpcUrl": "https://eth-mainnet.g.alchemy.com/v2/demo", 90 | "graphqlAPIURL": "http://localhost:8080", 91 | "inspectAPIURL": "http://localhost:8080", 92 | "DAppRelayAddress": "0xF5DE34d6BbC0446E2a45719E718efEbaaE179daE", 93 | "InputBoxAddress": "0x59b22D57D4f067708AB0c00552767405926dc768", 94 | "EtherPortalAddress": "0xFfdbe43d4c855BF7e0f105c400A50857f53AB044", 95 | "Erc20PortalAddress": "0x9C21AEb2093C32DDbC53eEF24B873BDCd1aDa1DB", 96 | "Erc721PortalAddress": "0x237F8DD094C0e47f4236f12b4Fa01d6Dae89fb87", 97 | "Erc1155SinglePortalAddress": "0x7CFB0193Ca87eB6e48056885E026552c3A941FC4", 98 | "Erc1155BatchPortalAddress": "0xedB53860A6B52bbb7561Ad596416ee9965B055Aa" 99 | } 100 | } -------------------------------------------------------------------------------- /tests/cartesify/AxiosLikeClientV2.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe, beforeAll } from "@jest/globals"; 2 | import { Cartesify } from "../../src"; 3 | import { ethers } from "ethers"; 4 | import {} from "../../src/cartesify/AxiosLikeClientV2" 5 | 6 | describe("AxiosLikeClientV2", () => { 7 | const TEST_TIMEOUT = 300000 8 | let axiosLikeClient: ReturnType; 9 | 10 | beforeAll(() => { 11 | const provider = ethers.getDefaultProvider("http://localhost:8545"); 12 | const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' 13 | let signer = new ethers.Wallet(privateKey, provider); 14 | axiosLikeClient = Cartesify.createAxios({ 15 | dappAddress: '0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e', 16 | endpoints: { 17 | graphQL: new URL("http://localhost:8080/graphql"), 18 | inspect: new URL("http://localhost:8080/inspect"), 19 | }, 20 | provider, 21 | signer 22 | }) 23 | }) 24 | 25 | it("should work with GET", async () => { 26 | const response = await axiosLikeClient.get("http://127.0.0.1:8383/health") 27 | expect(response.statusText.toLowerCase()).toBe('ok') 28 | const json = await response.data; 29 | expect(json.some).toEqual('response') 30 | }, TEST_TIMEOUT) 31 | 32 | it("should work with POST", async () => { 33 | const response = await axiosLikeClient.post("http://127.0.0.1:8383/echo", { any: 'body' }, { 34 | headers: { 35 | "Content-Type": "application/json", 36 | }, 37 | }) 38 | expect(response.statusText.toLowerCase()).toBe('ok') 39 | const json = await response.data; 40 | expect(json).toEqual({ myPost: { any: "body" } }) 41 | }, TEST_TIMEOUT) 42 | 43 | it("should work with PUT", async () => { 44 | const response = await axiosLikeClient.put("http://127.0.0.1:8383/update", { any: 'body' }, { 45 | headers: { 46 | "Content-Type": "application/json", 47 | }, 48 | }) 49 | expect(response.statusText.toLowerCase()).toBe('ok') 50 | const json = await response.data; 51 | expect(json).toEqual({ updateBody: { any: "body" } }) 52 | }, TEST_TIMEOUT) 53 | 54 | it("should work with PATCH", async () => { 55 | const response = await axiosLikeClient.patch("http://127.0.0.1:8383/patch", { any: 'body' }, { 56 | headers: { 57 | "Content-Type": "application/json", 58 | } 59 | }) 60 | expect(response.statusText.toLowerCase()).toBe('ok') 61 | const json = await response.data; 62 | expect(json).toEqual({ patchBody: { any: "body" } }) 63 | 64 | let contentType = "" 65 | if ("config" in response) { 66 | contentType = response.headers["content-type"] 67 | } else { 68 | contentType = response.headers.get('content-type') ?? "" 69 | } 70 | expect(contentType).toContain('application/json') 71 | }, TEST_TIMEOUT) 72 | 73 | it("should work with DELETE", async () => { 74 | const response = await axiosLikeClient.delete("http://127.0.0.1:8383/delete?foo=bar") 75 | expect(response.statusText.toLowerCase()).toBe('ok') 76 | const json = await response.data; 77 | expect(json).toEqual({ query: { foo: "bar" } }) 78 | }, TEST_TIMEOUT) 79 | 80 | it("should handle 404 doing POST", async () => { 81 | const response = await axiosLikeClient.post("http://127.0.0.1:8383/echoNotFound", { any: 'body' }, { 82 | headers: { 83 | "Content-Type": "application/json", 84 | } 85 | }) 86 | 87 | expect(response.statusText.toLowerCase()).toBe('not found') 88 | expect(response.status).toBe(404) 89 | }, TEST_TIMEOUT) 90 | 91 | it("should handle 'TypeError: fetch failed' doing POST. Connection refused", async () => { 92 | await expect(axiosLikeClient.post("http://127.0.0.1:12345/wrongPort", { any: 'body' }, { 93 | headers: { 94 | "Content-Type": "application/json", 95 | } 96 | })).rejects.toThrowError(new TypeError("fetch failed")); 97 | }, TEST_TIMEOUT) 98 | 99 | it("should handle 'Error: connect ECONNREFUSED 127.0.0.1:12345' doing GET. Connection refused", async () => { 100 | const error = await axiosLikeClient.get("http://127.0.0.1:12345/wrongPort").catch((e) => e) 101 | expect(error.name).toBe("Error") 102 | expect(error.message).toBe("connect ECONNREFUSED 127.0.0.1:12345") 103 | }, TEST_TIMEOUT) 104 | 105 | it("should send the msg_sender as x-msg_sender within the headers. Also send other metadata with 'x-' prefix", async () => { 106 | const response = await axiosLikeClient.post("http://127.0.0.1:8383/echo/headers", { any: 'body' }, { 107 | headers: { 108 | "Content-Type": "application/json", 109 | } 110 | }) 111 | expect(response.statusText.toLowerCase()).toBe('ok') 112 | const json = await response.data; 113 | expect(json.headers['x-msg_sender']).toEqual('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') 114 | expect(json.headers['x-block_number']).toMatch(/^[0-9]+$/) 115 | expect(json.headers['x-epoch_index']).toMatch(/^[0-9]+$/) 116 | expect(json.headers['x-input_index']).toMatch(/^[0-9]+$/) 117 | expect(json.headers['x-timestamp']).toMatch(/^[0-9]+$/) 118 | }, TEST_TIMEOUT) 119 | 120 | it("should send the headers doing GET", async () => { 121 | const response = await axiosLikeClient.get("http://127.0.0.1:8383/echo/headers", { 122 | headers: { "x-my-header": "some-value" }, 123 | }) 124 | 125 | expect(response.statusText.toLowerCase()).toBe("ok") 126 | if ("config" in response) { 127 | expect(response.config.headers["x-my-header"]).toEqual("some-value") 128 | } else { 129 | expect(response.headers.get("x-my-header")).toEqual("some-value") 130 | } 131 | }, TEST_TIMEOUT) 132 | 133 | }) 134 | -------------------------------------------------------------------------------- /tests/main.test.ts: -------------------------------------------------------------------------------- 1 | import mock from "http-request-mock"; 2 | import { expect, it, describe, beforeEach, afterEach, jest } from "@jest/globals"; 3 | import { CartesiClient, CartesiClientBuilder } from "../src"; 4 | import { Network, type Provider, ethers, ContractTransactionResponse } from "ethers"; 5 | import { Hex } from "../src/hex"; 6 | import type { InputBox } from "@cartesi/rollups"; 7 | import type { Log } from "../src/types"; 8 | import { randomBytes } from "node:crypto" 9 | 10 | function generateValidEth(): string { 11 | const rb = randomBytes(20); 12 | const address = `0x${rb.toString("hex")}`; 13 | 14 | return address; 15 | } 16 | 17 | describe.skip("CartesiClient", () => { 18 | const mocker = mock.setupForUnitTest("fetch"); 19 | 20 | let cartesiClient: CartesiClient; 21 | const endpoint = new URL("http://127.0.0.1:8545/inspect"); 22 | 23 | beforeEach(async () => { 24 | const provider = ethers.getDefaultProvider(endpoint.href); 25 | cartesiClient = new CartesiClientBuilder().withEndpoint(endpoint).withProvider(provider).build(); 26 | }); 27 | 28 | afterEach(() => { 29 | mocker.reset(); 30 | }); 31 | 32 | describe("inspect", () => { 33 | it("should return null if the response is not valid", async () => { 34 | // Arrange 35 | const payload = { action: "show_games" }; 36 | const wrongBody = { 37 | foo: "bar", 38 | }; 39 | mocker.get(endpoint.href, wrongBody, { 40 | times: 1, 41 | }); 42 | 43 | // Act 44 | const result = await cartesiClient.inspect(payload); 45 | 46 | // Assert 47 | expect(result).toBeNull(); 48 | }); 49 | 50 | it("should return the payload from the first report if the response is valid", async () => { 51 | // Arrange 52 | const payload = { action: "show_games" }; 53 | const games = { games: [1, 2, 3] }; 54 | const gamesPayload = Hex.obj2hex(games); 55 | mocker.get( 56 | endpoint.href, 57 | { 58 | reports: [{ payload: gamesPayload }], 59 | }, 60 | { 61 | times: 1, 62 | } 63 | ); 64 | 65 | // Act 66 | const result = await cartesiClient.inspect(payload); 67 | 68 | // Assert 69 | expect(result).toMatchObject(games); 70 | }); 71 | }); 72 | 73 | describe("advance", () => { 74 | describe("should error", () => { 75 | it("Error network if an exception is thrown", async () => { 76 | // Arrange 77 | const payload = { action: "new_player", name: "calindra" }; 78 | const logger: Log = { error: jest.fn(), info: console.log }; 79 | const address = generateValidEth(); 80 | 81 | const provider = { 82 | getNetwork: jest.fn<() => Promise>().mockRejectedValueOnce(new Error("network error")), 83 | } as any as Provider; 84 | 85 | const client = new CartesiClientBuilder() 86 | .withDappAddress(address) 87 | .withLogger(logger) //omit error log 88 | .withProvider(provider) 89 | .build(); 90 | // Act / Assert 91 | return expect(client.advance(payload)).rejects.toThrow("network error"); 92 | }); 93 | 94 | it("Error contract if an exception is thrown", async () => { 95 | // Arrange 96 | const payload = { action: "new_player", name: "calindra" }; 97 | const logger: Log = { error: jest.fn(), info: console.log }; 98 | const address = generateValidEth(); 99 | 100 | const provider: Pick = { 101 | getNetwork: jest 102 | .fn() 103 | .mockReturnValueOnce(Promise.resolve(new Network("homestead", 1))), 104 | }; 105 | 106 | const inputContract: Pick = { 107 | addInput: jest.fn().mockRejectedValueOnce(new Error("contract error")), 108 | }; 109 | 110 | const client = new CartesiClientBuilder() 111 | .withDappAddress(address) 112 | .withLogger(logger) //omit error log 113 | .withProvider(provider as Provider) 114 | .build(); 115 | jest.spyOn(client, "getInputContract").mockResolvedValue(inputContract as InputBox); 116 | // Act / Assert 117 | return expect(client.advance(payload)).rejects.toThrow("contract error"); 118 | }); 119 | }); 120 | 121 | it("should call successful", async () => { 122 | // Arrange 123 | const payload = { action: "new_player", name: "calindra" }; 124 | 125 | const address = generateValidEth(); 126 | 127 | const advance_endpoint = new URL("/advance", endpoint).href; 128 | const provider = ethers.getDefaultProvider(advance_endpoint); 129 | 130 | const inputContract: Pick = { 131 | addInput: jest.fn<() => Promise>().mockResolvedValueOnce({ 132 | hash: "mocked hash", 133 | wait: jest.fn<() => Promise>().mockResolvedValueOnce({} as any), 134 | } as any), 135 | }; 136 | 137 | jest.spyOn(provider, "getNetwork").mockResolvedValue(new Network("hardhat", 8545)); 138 | 139 | const client = new CartesiClientBuilder() 140 | .withEndpoint(advance_endpoint) 141 | .withProvider(provider) 142 | .withDappAddress(address) 143 | .build(); 144 | 145 | jest.spyOn(client, "getInputContract").mockResolvedValue(inputContract as InputBox); 146 | // Act / Assert 147 | return expect(client.advance(payload)).resolves.not.toThrow(); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /tests/cartesify/FetchClient.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe, beforeAll } from "@jest/globals"; 2 | import { Cartesify } from "../../src"; 3 | import { ethers } from "ethers"; 4 | import { FetchFun } from "../../src/cartesify/FetchLikeClient"; 5 | 6 | describe("fetch", () => { 7 | const TEST_TIMEOUT = 30000 8 | // const fetch2test = fetch 9 | let fetch2test: FetchFun 10 | 11 | beforeAll(() => { 12 | const provider = ethers.getDefaultProvider("http://localhost:8545"); 13 | const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' 14 | let signer = new ethers.Wallet(privateKey, provider); 15 | fetch2test = Cartesify.createFetch({ 16 | dappAddress: '0xab7528bb862fb57e8a2bcd567a2e929a0be56a5e', 17 | endpoints: { 18 | graphQL: new URL("http://localhost:8080/graphql"), 19 | inspect: new URL("http://localhost:8080/inspect"), 20 | }, 21 | provider, 22 | signer, 23 | }) 24 | }) 25 | 26 | it("should work with GET", async () => { 27 | const response = await fetch2test("http://127.0.0.1:8383/health") 28 | expect(response.ok).toBe(true) 29 | const json = await response.json(); 30 | expect(json.some).toEqual('response') 31 | }, TEST_TIMEOUT) 32 | 33 | it("should work with POST", async () => { 34 | const response = await fetch2test("http://127.0.0.1:8383/echo", { 35 | method: "POST", 36 | headers: { 37 | "Content-Type": "application/json", 38 | }, 39 | body: JSON.stringify({ any: 'body' }) 40 | }) 41 | expect(response.ok).toBe(true) 42 | const json = await response.json(); 43 | expect(json).toEqual({ myPost: { any: "body" } }) 44 | }, TEST_TIMEOUT) 45 | 46 | it("should work with PUT", async () => { 47 | const response = await fetch2test("http://127.0.0.1:8383/update", { 48 | method: "PUT", 49 | headers: { 50 | "Content-Type": "application/json", 51 | }, 52 | body: JSON.stringify({ any: 'body' }) 53 | }) 54 | expect(response.ok).toBe(true) 55 | const json = await response.json(); 56 | expect(json).toEqual({ updateBody: { any: "body" } }) 57 | }, TEST_TIMEOUT) 58 | 59 | it("should work with PATCH", async () => { 60 | const response = await fetch2test("http://127.0.0.1:8383/patch", { 61 | method: "PATCH", 62 | headers: { 63 | "Content-Type": "application/json", 64 | }, 65 | body: JSON.stringify({ any: 'body' }) 66 | }) 67 | expect(response.ok).toBe(true) 68 | const json = await response.json(); 69 | expect(json).toEqual({ patchBody: { any: "body" } }) 70 | expect(response.type).toBe("basic") 71 | expect(response.headers.get('content-type')).toContain('application/json') 72 | }, TEST_TIMEOUT) 73 | 74 | it("should work with DELETE", async () => { 75 | const response = await fetch2test("http://127.0.0.1:8383/delete?foo=bar", { 76 | method: "DELETE", 77 | }) 78 | expect(response.ok).toBe(true) 79 | const json = await response.json(); 80 | expect(json).toEqual({ query: { foo: "bar" } }) 81 | expect(response.type).toBe("basic") 82 | }, TEST_TIMEOUT) 83 | 84 | it("should handle 404 doing POST", async () => { 85 | const response = await fetch2test("http://127.0.0.1:8383/echoNotFound", { 86 | method: "POST", 87 | headers: { 88 | "Content-Type": "application/json", 89 | }, 90 | body: JSON.stringify({ any: 'body' }) 91 | }) 92 | expect(response.ok).toBe(false) 93 | expect(response.status).toBe(404) 94 | expect(await response.text()).toContain('
Cannot POST /echoNotFound {
 99 |         const error = await fetch2test("http://127.0.0.1:12345/wrongPort", {
100 |             method: "POST",
101 |             headers: {
102 |                 "Content-Type": "application/json",
103 |             },
104 |             body: JSON.stringify({ any: 'body' })
105 |         }).catch(e => e)
106 | 
107 |         expect(error.constructor.name).toBe("TypeError")
108 |         expect(error.message).toBe("fetch failed")
109 |     }, TEST_TIMEOUT)
110 | 
111 |     it("should handle 'TypeError: fetch failed' doing GET. Connection refused", async () => {
112 |         const error = await fetch2test("http://127.0.0.1:12345/wrongPort").catch(e => e)
113 |         expect(error.constructor.name).toBe("TypeError")
114 |         expect(error.message).toBe("fetch failed")
115 |     }, TEST_TIMEOUT)
116 | 
117 |     it("should send the msg_sender as x-msg_sender within the headers. Also send other metadata with 'x-' prefix", async () => {
118 |         const response = await fetch2test("http://127.0.0.1:8383/echo/headers", {
119 |             method: "POST",
120 |             headers: {
121 |                 "Content-Type": "application/json",
122 |             },
123 |             body: JSON.stringify({ any: 'body' })
124 |         })
125 |         expect(response.ok).toBe(true)
126 |         const json = await response.json();
127 |         expect(json.headers['x-msg_sender']).toEqual('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
128 |         expect(json.headers['x-block_number']).toMatch(/^[0-9]+$/)
129 |         expect(json.headers['x-epoch_index']).toMatch(/^[0-9]+$/)
130 |         expect(json.headers['x-input_index']).toMatch(/^[0-9]+$/)
131 |         expect(json.headers['x-timestamp']).toMatch(/^[0-9]+$/)
132 |     }, TEST_TIMEOUT)
133 | 
134 |     it("should send the headers doing GET", async () => {
135 |         const response = await fetch2test("http://127.0.0.1:8383/echo/headers", {
136 |             method: "GET",
137 |             headers: {
138 |                 "x-my-header": "some-value",
139 |             }
140 |         })
141 |         expect(response.ok).toBe(true)
142 |         const json = await response.json();
143 |         expect(json.headers['x-my-header']).toEqual('some-value')
144 |     }, TEST_TIMEOUT)
145 | })
146 | 


--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * For a detailed explanation regarding each configuration property, visit:
  3 |  * https://jestjs.io/docs/configuration
  4 |  */
  5 | 
  6 | import type {Config} from 'jest';
  7 | 
  8 | const config: Config = {
  9 |   // All imported modules in your tests should be mocked automatically
 10 |   // automock: false,
 11 | 
 12 |   // Stop running tests after `n` failures
 13 |   // bail: 0,
 14 | 
 15 |   // The directory where Jest should store its cached dependency information
 16 |   // cacheDirectory: "/tmp/jest_rs",
 17 | 
 18 |   // Automatically clear mock calls, instances, contexts and results before every test
 19 |   // clearMocks: false,
 20 | 
 21 |   // Indicates whether the coverage information should be collected while executing the test
 22 |   // collectCoverage: false,
 23 | 
 24 |   // An array of glob patterns indicating a set of files for which coverage information should be collected
 25 |   // collectCoverageFrom: undefined,
 26 | 
 27 |   // The directory where Jest should output its coverage files
 28 |   // coverageDirectory: undefined,
 29 | 
 30 |   // An array of regexp pattern strings used to skip coverage collection
 31 |   // coveragePathIgnorePatterns: [
 32 |   //   "/node_modules/"
 33 |   // ],
 34 | 
 35 |   // Indicates which provider should be used to instrument code for coverage
 36 |   coverageProvider: "v8",
 37 | 
 38 |   // A list of reporter names that Jest uses when writing coverage reports
 39 |   // coverageReporters: [
 40 |   //   "json",
 41 |   //   "text",
 42 |   //   "lcov",
 43 |   //   "clover"
 44 |   // ],
 45 | 
 46 |   // An object that configures minimum threshold enforcement for coverage results
 47 |   // coverageThreshold: undefined,
 48 | 
 49 |   // A path to a custom dependency extractor
 50 |   // dependencyExtractor: undefined,
 51 | 
 52 |   // Make calling deprecated APIs throw helpful error messages
 53 |   // errorOnDeprecated: false,
 54 | 
 55 |   // The default configuration for fake timers
 56 |   // fakeTimers: {
 57 |   //   "enableGlobally": false
 58 |   // },
 59 | 
 60 |   // Force coverage collection from ignored files using an array of glob patterns
 61 |   // forceCoverageMatch: [],
 62 | 
 63 |   // A path to a module which exports an async function that is triggered once before all test suites
 64 |   // globalSetup: undefined,
 65 | 
 66 |   // A path to a module which exports an async function that is triggered once after all test suites
 67 |   // globalTeardown: undefined,
 68 | 
 69 |   // A set of global variables that need to be available in all test environments
 70 |   // globals: {},
 71 | 
 72 |   // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
 73 |   // maxWorkers: "50%",
 74 | 
 75 |   // An array of directory names to be searched recursively up from the requiring module's location
 76 |   // moduleDirectories: [
 77 |   //   "node_modules"
 78 |   // ],
 79 | 
 80 |   // An array of file extensions your modules use
 81 |   // moduleFileExtensions: [
 82 |   //   "js",
 83 |   //   "mjs",
 84 |   //   "cjs",
 85 |   //   "jsx",
 86 |   //   "ts",
 87 |   //   "tsx",
 88 |   //   "json",
 89 |   //   "node"
 90 |   // ],
 91 | 
 92 |   // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
 93 |   // moduleNameMapper: {},
 94 | 
 95 |   // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
 96 |   // modulePathIgnorePatterns: [],
 97 | 
 98 |   // Activates notifications for test results
 99 |   // notify: false,
100 | 
101 |   // An enum that specifies notification mode. Requires { notify: true }
102 |   // notifyMode: "failure-change",
103 | 
104 |   // A preset that is used as a base for Jest's configuration
105 |   preset: "ts-jest",
106 | 
107 |   // Run tests from one or more projects
108 |   // projects: undefined,
109 | 
110 |   // Use this configuration option to add custom reporters to Jest
111 |   // reporters: undefined,
112 | 
113 |   // Automatically reset mock state before every test
114 |   // resetMocks: false,
115 | 
116 |   // Reset the module registry before running each individual test
117 |   // resetModules: false,
118 | 
119 |   // A path to a custom resolver
120 |   // resolver: undefined,
121 | 
122 |   // Automatically restore mock state and implementation before every test
123 |   // restoreMocks: false,
124 | 
125 |   // The root directory that Jest should scan for tests and modules within
126 |   // rootDir: undefined,
127 | 
128 |   // A list of paths to directories that Jest should use to search for files in
129 |   // roots: [
130 |   //   ""
131 |   // ],
132 | 
133 |   // Allows you to use a custom runner instead of Jest's default test runner
134 |   // runner: "jest-runner",
135 | 
136 |   // The paths to modules that run some code to configure or set up the testing environment before each test
137 |   // setupFiles: [],
138 | 
139 |   // A list of paths to modules that run some code to configure or set up the testing framework before each test
140 |   // setupFilesAfterEnv: [],
141 | 
142 |   // The number of seconds after which a test is considered as slow and reported as such in the results.
143 |   // slowTestThreshold: 5,
144 | 
145 |   // A list of paths to snapshot serializer modules Jest should use for snapshot testing
146 |   // snapshotSerializers: [],
147 | 
148 |   // The test environment that will be used for testing
149 |   testEnvironment: "node",
150 | 
151 |   // Options that will be passed to the testEnvironment
152 |   // testEnvironmentOptions: {},
153 | 
154 |   // Adds a location field to test results
155 |   // testLocationInResults: false,
156 | 
157 |   // The glob patterns Jest uses to detect test files
158 |   // testMatch: [
159 |   //   "**/__tests__/**/*.[jt]s?(x)",
160 |   //   "**/?(*.)+(spec|test).[tj]s?(x)"
161 |   // ],
162 | 
163 |   // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
164 |   // testPathIgnorePatterns: [
165 |   //   "/node_modules/"
166 |   // ],
167 | 
168 |   // The regexp pattern or array of patterns that Jest uses to detect test files
169 |   // testRegex: [],
170 | 
171 |   // This option allows the use of a custom results processor
172 |   // testResultsProcessor: undefined,
173 | 
174 |   // This option allows use of a custom test runner
175 |   // testRunner: "jest-circus/runner",
176 | 
177 |   // A map from regular expressions to paths to transformerqs
178 |   // transform: undefined,
179 | 
180 |   // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
181 |   // transformIgnorePatterns: [
182 |   //   "/node_modules/",
183 |   //   "\\.pnp\\.[^\\/]+$"
184 |   // ],
185 | 
186 |   // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
187 |   // unmockedModulePathPatterns: undefined,
188 | 
189 |   // Indicates whether each individual test should be reported during the run
190 |   // verbose: undefined,
191 | 
192 |   // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
193 |   // watchPathIgnorePatterns: [],
194 | 
195 |   // Whether to use watchman for file crawling
196 |   // watchman: true,
197 | };
198 | 
199 | export default config;
200 | 


--------------------------------------------------------------------------------
/src/cartesify/AxiosLikeClientV2.ts:
--------------------------------------------------------------------------------
  1 | import { Utils } from "../utils";
  2 | import axios, { type Method } from "axios";
  3 | import { InputAddedListener } from "./InputAddedListener";
  4 | import { WrappedPromise } from "./WrappedPromise";
  5 | import { ContractTransactionResponse, ethers } from "ethers";
  6 | import { CartesiClient } from "..";
  7 | import { Config, AxiosSetupOptions } from "../models/config";
  8 | 
  9 | interface Options {
 10 |     cartesiClient: CartesiClient
 11 |     method: Method
 12 | }
 13 | 
 14 | export class AxiosLikeClientV2 {
 15 | 
 16 |     private url: string | URL | globalThis.Request
 17 |     private options: Partial & Record
 18 |     static requests: Record = {}
 19 | 
 20 |     constructor(url: string | URL | globalThis.Request, options: any) {
 21 |         this.url = url
 22 |         this.options = options
 23 |     }
 24 | 
 25 |     async doRequestWithInspect() {
 26 |         if (!this.options?.cartesiClient) {
 27 |             throw new Error('You need to configure the Cartesi client')
 28 |         }
 29 |         const that = this.options.cartesiClient;
 30 |         const { logger } = that.config;
 31 |         try {
 32 |             const inputJSON = JSON.stringify({
 33 |                 cartesify: {
 34 |                     axios: {
 35 |                         url: this.url,
 36 |                         options: { ...this.options, cartesiClient: undefined },
 37 |                     },
 38 |                 },
 39 |             });
 40 |             const jsonEncoded = encodeURIComponent(inputJSON);
 41 |             const urlInner = new URL(that.config.endpoint);
 42 |             urlInner.pathname += `/${jsonEncoded}`;
 43 |             const extraHeaders = Utils.onlyAllowXHeaders(this.options.headers);
 44 |             const response = await axios.get(urlInner.href, {
 45 |                 headers: {
 46 |                     "Content-Type": "application/json",
 47 |                     Accept: "application/json",
 48 |                     ...extraHeaders,
 49 |                 },
 50 |             });
 51 |             const result = await response.data;
 52 | 
 53 |             if (Utils.isObject(result) && "reports" in result && Utils.isArrayNonNullable(result.reports)) {
 54 |                 const lastReport = result.reports[result.reports.length - 1]
 55 |                 if (Utils.isObject(lastReport) && "payload" in lastReport && typeof lastReport.payload === "string") {
 56 |                     const payload = Utils.hex2str(lastReport.payload.replace(/^0x/, ""));
 57 |                     const successOrError = JSON.parse(payload)
 58 |                     if (successOrError.success) {
 59 |                         return new AxiosResponse(successOrError.success, response.config)
 60 |                     } else if (successOrError.error) {
 61 |                         if (successOrError.error?.constructorName === "TypeError") {
 62 |                             throw new TypeError(successOrError.error.message)
 63 |                         } else {
 64 |                             throw successOrError.error
 65 |                         }
 66 |                     }
 67 |                 }
 68 |             }
 69 |             throw new Error(`Wrong inspect response format.`)
 70 |         } catch (e) {
 71 |             logger.error(e);
 72 |             throw e;
 73 |         }
 74 | 
 75 |     }
 76 | 
 77 |     async doRequestWithAdvance() {
 78 |         if (!this.options?.cartesiClient) {
 79 |             throw new Error('You need to configure the Cartesi client')
 80 |         }
 81 |         const cartesiClient = this.options.cartesiClient
 82 |         const { logger } = cartesiClient.config;
 83 |         try {
 84 |             new InputAddedListener(cartesiClient).addListener()
 85 |             const inputContract = await cartesiClient.getInputContract();
 86 |             const requestId = `${Date.now()}:${Math.random()}`
 87 |             const wPromise = InputAddedListener.requests[requestId] = new WrappedPromise()
 88 |             // convert string to input bytes (if it's not already bytes-like)
 89 |             const inputBytes = ethers.toUtf8Bytes(
 90 |                 JSON.stringify({
 91 |                     requestId,
 92 |                     cartesify: {
 93 |                         fetch: {
 94 |                             url: this.url,
 95 |                             options: { ...this.options, cartesiClient: undefined },
 96 |                         },
 97 |                     },
 98 |                 })
 99 |             );
100 |             const dappAddress = await cartesiClient.getDappAddress();
101 | 
102 |             // send transaction
103 |             const tx = await inputContract.addInput(dappAddress, inputBytes) as ContractTransactionResponse;
104 |             await tx.wait(1);
105 |             const resp = await wPromise.promise
106 |             const res = new Response(resp.success)
107 |             return res
108 |         } catch (e) {
109 |             logger.error(`Error ${this.options?.method ?? 'GET'} ${this.url}`, e)
110 |             throw e
111 |         }
112 |     }
113 | 
114 |     static async executeRequest(
115 |         cartesiClient: CartesiClient,
116 |         options: AxiosSetupOptions,
117 |         url: string,
118 |         method: string,
119 |         init?: Config,
120 |         data?: Record
121 |     ) {
122 |         const _url = url.startsWith(options.baseURL || '') ? url : `${options.baseURL || ''}${url}`;
123 | 
124 |         const axiosClient = AxiosLikeClientV2.createClient(cartesiClient, _url, method, init, data);
125 | 
126 |         if (method === "GET") {
127 |             return axiosClient.doRequestWithInspect();
128 |         } else {
129 |             return axiosClient.doRequestWithAdvance();
130 |         }
131 |     }
132 | 
133 | 
134 | 
135 |     static createClient(cartesiClient: CartesiClient, url: string, method: string, init?: Config, data?: Record) {
136 |         if (init?.signer) {
137 |             cartesiClient.setSigner(init.signer);
138 |         }
139 | 
140 |         const opts = {
141 |             body: JSON.stringify(data),
142 |             signer: init?.signer,
143 |             cartesiClient: cartesiClient,
144 |             headers: init?.headers,
145 |             method
146 |         };
147 | 
148 |         return new AxiosLikeClientV2(url, opts);
149 |     }
150 | }
151 | 
152 | 
153 | 
154 | 
155 | class AxiosResponse {
156 |     data: T;
157 |     status: number;
158 |     statusText: string;
159 |     headers: Record;
160 |     config: Record;
161 | 
162 |     constructor(params: {
163 |         data: T;
164 |         status: number;
165 |         statusText: string;
166 |         headers: Record;
167 |     }, config: Record) {
168 |         this.data = params.data;
169 |         this.status = params.status;
170 |         this.statusText = Utils.httpStatusMap[params.status] || "";
171 |         this.headers = params.headers || {};
172 |         this.config = config;;
173 |     }
174 | }
175 | 
176 | 
177 | class Response {
178 |     status: number = 0
179 |     headers = new Map()
180 |     data: Record
181 |     statusText: string
182 |     constructor(params: any) {
183 |         this.status = params.status
184 |         try {
185 |             this.data = typeof params.text === 'string' ? JSON.parse(params.text) : params.text;
186 |         } catch (error) {
187 |             console.error("Failed to parse response data:", error);
188 |             this.data = {}; // Define como um objeto vazio se não conseguir fazer parse
189 |         }
190 |         this.statusText = Utils.httpStatusMap[params.status] || ""
191 |         if (params.headers) {
192 |             this.headers = new Map(params.headers)
193 |         }
194 |     }
195 | }
196 | 
197 | 


--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
  1 | import { IInputBox__factory, type InputBox } from "@cartesi/rollups";
  2 | import {
  3 |   ethers,
  4 |   type Signer,
  5 |   type Provider,
  6 |   Wallet,
  7 |   type AddressLike,
  8 |   resolveAddress,
  9 |   type ContractTransactionResponse,
 10 | } from "ethers";
 11 | import { Utils } from "./utils";
 12 | import { Hex } from "./hex";
 13 | import type { ObjectLike, Log } from "./types";
 14 | import { Cartesify } from "./cartesify/Cartesify";
 15 | import { inputBoxAddress } from "./contracts";
 16 | 
 17 | export { Utils, Cartesify };
 18 | 
 19 | export interface CartesiConstructor {
 20 |   /**
 21 |    * The endpoint of the Cartesi Rollups server
 22 |    */
 23 |   endpoint: URL;
 24 | 
 25 |   /**
 26 |    * The endpoint of the Cartesi graphQL
 27 |    */
 28 |   endpointGraphQL: URL;
 29 | 
 30 |   /**
 31 |    * AddressLike, type used by ethers to string
 32 |    */
 33 |   dapp_address: AddressLike;
 34 |   signer: Signer;
 35 |   wallet?: Wallet;
 36 |   provider: Provider;
 37 |   logger: Log;
 38 |   inputBoxAddress: string;
 39 | }
 40 | 
 41 | export class CartesiClientBuilder {
 42 |   private endpoint: URL;
 43 |   private endpointGraphQL: URL;
 44 |   private dappAddress: AddressLike;
 45 |   private signer: Signer;
 46 |   private wallet?: Wallet;
 47 |   private provider: Provider;
 48 |   private logger: Log;
 49 |   private inputBoxAddress: string;
 50 | 
 51 |   constructor() {
 52 |     this.endpoint = new URL("http://localhost:8080");
 53 |     this.endpointGraphQL = new URL("http://localhost:8080/graphql");
 54 |     this.dappAddress = "";
 55 |     this.provider = ethers.getDefaultProvider(this.endpoint.href);
 56 |     this.signer = new ethers.VoidSigner("0x", this.provider);
 57 |     this.inputBoxAddress = inputBoxAddress;
 58 |     this.logger = {
 59 |       info: console.log,
 60 |       error: console.error,
 61 |     };
 62 |   }
 63 | 
 64 |   withEndpoint(endpoint: URL | string): CartesiClientBuilder {
 65 |     this.endpoint = new URL(endpoint);
 66 |     return this;
 67 |   }
 68 | 
 69 |   withEndpointGraphQL(endpoint: URL | string): CartesiClientBuilder {
 70 |     this.endpointGraphQL = new URL(endpoint);
 71 |     return this;
 72 |   }
 73 | 
 74 |   withDappAddress(address: AddressLike): CartesiClientBuilder {
 75 |     this.dappAddress = address;
 76 |     return this;
 77 |   }
 78 | 
 79 |   withInputBoxAddress(address: string): CartesiClientBuilder {
 80 |     this.inputBoxAddress = address;
 81 |     return this;
 82 |   }
 83 | 
 84 |   withSigner(signer: Signer): CartesiClientBuilder {
 85 |     this.signer = signer;
 86 |     return this;
 87 |   }
 88 | 
 89 |   withWallet(wallet: Wallet): CartesiClientBuilder {
 90 |     this.wallet = wallet;
 91 |     return this;
 92 |   }
 93 | 
 94 |   withProvider(provider: Provider): CartesiClientBuilder {
 95 |     this.provider = provider;
 96 |     return this;
 97 |   }
 98 | 
 99 |   withLogger(logger: Log): CartesiClientBuilder {
100 |     this.logger = logger;
101 |     return this;
102 |   }
103 | 
104 |   build(): CartesiClient {
105 |     return new CartesiClient({
106 |       endpoint: this.endpoint,
107 |       dapp_address: this.dappAddress,
108 |       signer: this.signer,
109 |       wallet: this.wallet,
110 |       provider: this.provider,
111 |       logger: this.logger,
112 |       inputBoxAddress: this.inputBoxAddress,
113 |       endpointGraphQL: this.endpointGraphQL,
114 |     });
115 |   }
116 | }
117 | 
118 | export class CartesiClient {
119 |   private static inputContract?: InputBox;
120 | 
121 |   constructor(readonly config: CartesiConstructor) {}
122 | 
123 |   /**
124 |    * Convert AddressLike, type used by ethers to string
125 |    */
126 |   async getDappAddress(): Promise {
127 |     try {
128 |       return resolveAddress(this.config.dapp_address);
129 |     } catch (e) {
130 |       return this.config.dapp_address as string;
131 |     }
132 |   }
133 | 
134 |   setSigner(signer: Signer): void {
135 |     if (this.config.signer !== signer) {
136 |       CartesiClient.inputContract = undefined;
137 |       this.config.signer = signer;
138 |     }
139 |   }
140 | 
141 |   setProvider(provider: Provider): void {
142 |     this.config.provider = provider;
143 |   }
144 | 
145 |   /**
146 |    * Singleton to create contract
147 |    */
148 |   async getInputContract(): Promise {
149 |     if (!CartesiClient.inputContract) {
150 |       // const address = InputBoxContractAddress;
151 |       const address = this.config.inputBoxAddress;
152 |       CartesiClient.inputContract = IInputBox__factory.connect(address, this.config.signer);
153 |     }
154 |     return CartesiClient.inputContract;
155 |   }
156 | 
157 |   /**
158 |    * Inspect the machine state and try to get the first report and parse the payload.
159 |    *
160 |    * @param payload The data to be sent to the Cartesi Machine, transform to payload
161 |    * used to request reports
162 |    */
163 |   async inspect(payload: T): Promise {
164 |     try {
165 |       const inputJSON = JSON.stringify({ input: payload });
166 |       const jsonEncoded = encodeURIComponent(inputJSON);
167 | 
168 |       const url = new URL(this.config.endpoint);
169 |       url.pathname += `/${jsonEncoded}`;
170 | 
171 |       this.config.logger.info("Inspecting endpoint: ", url.href);
172 | 
173 |       const response = await fetch(url.href, {
174 |         method: "GET",
175 |         headers: {
176 |           "Content-Type": "application/json",
177 |           Accept: "application/json",
178 |         },
179 |       });
180 |       const result: unknown = await response.json();
181 | 
182 |       if (Utils.isObject(result) && "reports" in result && Utils.isArrayNonNullable(result.reports)) {
183 |         const firstReport = result.reports.at(0);
184 | 
185 |         if (Utils.isObject(firstReport) && "payload" in firstReport && typeof firstReport.payload === "string") {
186 |           const payload = Hex.hex2a(firstReport.payload.replace(/^0x/, ""));
187 |           return JSON.parse(payload);
188 |         }
189 |       }
190 |     } catch (e) {
191 |       this.config.logger.error(e);
192 |     }
193 | 
194 |     return null;
195 |   }
196 | 
197 |   /**
198 |    * Send InputBox
199 |    * @param payload The data to be sent to the Cartesi Machine, transform to payload
200 |    */
201 |   async advance(payload: T) {
202 |     const { logger } = this.config;
203 | 
204 |     try {
205 |       const { provider, signer } = this.config;
206 |       logger.info("getting network", provider);
207 |       const network = await provider.getNetwork();
208 |       logger.info("getting signer address", signer);
209 |       const signerAddress = await signer.getAddress();
210 | 
211 |       logger.info(`connected to chain ${network.chainId}`);
212 |       logger.info(`using account "${signerAddress}"`);
213 | 
214 |       // connect to rollups,
215 |       const inputContract = await this.getInputContract();
216 | 
217 |       // use message from command line option, or from user prompt
218 |       logger.info(`sending "${JSON.stringify(payload)}"`);
219 | 
220 |       // convert string to input bytes (if it's not already bytes-like)
221 |       const inputBytes = ethers.toUtf8Bytes(
222 |         JSON.stringify({
223 |           input: payload,
224 |         }),
225 |       );
226 | 
227 |       const dappAddress = await this.getDappAddress();
228 | 
229 |       // send transaction
230 |       const tx = await inputContract.addInput(dappAddress, inputBytes);
231 |       logger.info(`transaction: ${tx.hash}`);
232 |       logger.info("waiting for confirmation...");
233 |       const receipt = await tx.wait(1);
234 |       logger.info(JSON.stringify(receipt));
235 |     } catch (e) {
236 |       logger.error(e);
237 | 
238 |       if (e instanceof Error) {
239 |         throw e;
240 |       }
241 | 
242 |       throw new Error("Error on advance", { cause: e });
243 |     }
244 |   }
245 | }
246 | 


--------------------------------------------------------------------------------
/src/cartesify/AxiosLikeClient.ts:
--------------------------------------------------------------------------------
  1 | import { ContractTransactionResponse, ethers } from "ethers";
  2 | import { CartesiClient } from "..";
  3 | import { Utils } from "../utils";
  4 | import { WrappedPromise } from "./WrappedPromise";
  5 | 
  6 | export interface AxiosBuilder {
  7 |     baseURL?: string
  8 |     cartesiClient: CartesiClient
  9 | }
 10 | 
 11 | export class AxiosLikeClient {
 12 |     static requests: Record = {}
 13 | 
 14 |     static listenerAdded = false
 15 |     private baseURL?: string
 16 | 
 17 |     constructor(private cartesiClient: CartesiClient) {
 18 | 
 19 |     }
 20 | 
 21 |     static create(opts: AxiosBuilder) {
 22 |         const axiosLike = new AxiosLikeClient(opts.cartesiClient)
 23 |         axiosLike.baseURL = opts.baseURL
 24 |         axiosLike.addListener().catch(e => {
 25 |             console.error('AddListener error', e)
 26 |         })
 27 |         return axiosLike
 28 |     }
 29 | 
 30 |     async post(url: string, data?: any) {
 31 |         const cartesiClient = this.cartesiClient;
 32 |         if (!cartesiClient) {
 33 |             throw new Error('You need to configure the Cartesi client')
 34 |         }
 35 |         const { logger } = cartesiClient.config;
 36 | 
 37 |         try {
 38 |             const { provider, signer } = cartesiClient.config;
 39 |             logger.info("getting network", provider);
 40 |             const network = await provider.getNetwork();
 41 |             logger.info("getting signer address", signer);
 42 |             const signerAddress = await signer.getAddress();
 43 | 
 44 |             logger.info(`connected to chain ${network.chainId}`);
 45 |             logger.info(`using account "${signerAddress}"`);
 46 | 
 47 |             // connect to rollup,
 48 |             const inputContract = await cartesiClient.getInputContract();
 49 | 
 50 |             // use message from command line option, or from user prompt
 51 |             logger.info(`sending "${JSON.stringify(data)}"`);
 52 | 
 53 |             const requestId = `${Date.now()}:${Math.random()}`
 54 |             const wPromise = AxiosLikeClient.requests[requestId] = new WrappedPromise()
 55 |             // convert string to input bytes (if it's not already bytes-like)
 56 |             const inputBytes = ethers.toUtf8Bytes(
 57 |                 JSON.stringify({
 58 |                     requestId,
 59 |                     cartesify: {
 60 |                         axios: {
 61 |                             data,
 62 |                             url: `${this.baseURL || ''}${url}`,
 63 |                             method: "POST"
 64 |                         },
 65 |                     },
 66 |                 })
 67 |             );
 68 | 
 69 |             const dappAddress = await cartesiClient.getDappAddress();
 70 |             logger.info(`dappAddress: ${dappAddress} typeof ${typeof dappAddress}`);
 71 | 
 72 |             // send transaction
 73 |             const tx = await inputContract.addInput(dappAddress, inputBytes) as ContractTransactionResponse;
 74 |             logger.info(`transaction: ${tx.hash}`);
 75 |             logger.info("waiting for confirmation...");
 76 |             const receipt = await tx.wait(1);
 77 |             logger.info(JSON.stringify(receipt));
 78 |             return await wPromise.promise
 79 |         } catch (e) {
 80 |             logger.error(e);
 81 |             if (e instanceof Error) {
 82 |                 throw e;
 83 |             }
 84 |             throw new Error("Error on advance");
 85 |         }
 86 |     }
 87 | 
 88 | 
 89 |     async get(urlPath: string) {
 90 |         const that = this.cartesiClient as any;
 91 |         const { logger } = that.config;
 92 | 
 93 |         try {
 94 |             const inputJSON = JSON.stringify({
 95 |                 cartesify: {
 96 |                     axios: {
 97 |                         url: `${this.baseURL || ''}${urlPath}`,
 98 |                         method: "GET"
 99 |                     },
100 |                 },
101 |             });
102 |             const jsonEncoded = encodeURIComponent(inputJSON);
103 | 
104 |             const url = new URL(that.config.endpoint);
105 |             url.pathname += `/${jsonEncoded}`;
106 | 
107 |             logger.info("Inspecting endpoint: ", url.href);
108 | 
109 |             const response = await fetch(url.href, {
110 |                 method: "GET",
111 |                 headers: {
112 |                     "Content-Type": "application/json",
113 |                     Accept: "application/json",
114 |                 },
115 |             });
116 |             const result: unknown = await response.json();
117 | 
118 |             if (Utils.isObject(result) && "reports" in result && Utils.isArrayNonNullable(result.reports)) {
119 |                 const firstReport = result.reports.at(0);
120 |                 if (Utils.isObject(firstReport) && "payload" in firstReport && typeof firstReport.payload === "string") {
121 |                     const payload = Utils.hex2str(firstReport.payload.replace(/^0x/, ""));
122 |                     return { data: JSON.parse(payload) };
123 |                 }
124 |             }
125 |         } catch (e) {
126 |             logger.error(e);
127 |         }
128 |         return null;
129 |     }
130 | 
131 |     private async addListener() {
132 |         const MAX_RETRY = 20
133 |         const cartesiClient = this.cartesiClient;
134 |         if (!cartesiClient) {
135 |             throw new Error('You need to configure the Cartesi client')
136 |         }
137 |         if (AxiosLikeClient.listenerAdded) {
138 |             return
139 |         }
140 |         AxiosLikeClient.listenerAdded = true
141 |         const { logger } = cartesiClient.config;
142 |         const contract = await cartesiClient.getInputContract()
143 |         contract.on("InputAdded", async (_dapp: string, inboxInputIndex: number, _sender: string, input: string) => {
144 |             try {
145 |                 const str = Utils.hex2str(input.replace(/0x/, ''))
146 |                 const payload = JSON.parse(str)
147 |                 const wPromise = AxiosLikeClient.requests[payload.requestId]
148 |                 if (!wPromise) {
149 |                     return
150 |                 }
151 |                 let i = 0;
152 |                 while (i < MAX_RETRY) {
153 |                     try {
154 |                         i++;
155 |                         logger.info(`attempt ${i}...`)
156 |                         const req = await fetch("http://localhost:8080/graphql", {
157 |                             "headers": {
158 |                                 "accept": "*/*",
159 |                                 "accept-language": "en-US,en;q=0.9,pt;q=0.8",
160 |                                 "content-type": "application/json",
161 |                                 "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Microsoft Edge\";v=\"120\"",
162 |                                 "sec-ch-ua-mobile": "?0",
163 |                                 "sec-ch-ua-platform": "\"macOS\"",
164 |                                 "sec-fetch-dest": "empty",
165 |                                 "sec-fetch-mode": "cors",
166 |                                 "sec-fetch-site": "same-origin"
167 |                             },
168 |                             "referrer": "http://localhost:8080/graphql",
169 |                             "referrerPolicy": "strict-origin-when-cross-origin",
170 |                             "body": `{\"operationName\":null,\"variables\":{},\"query\":\"{\\n  input(index: ${inboxInputIndex}) {\\n    reports(first: 10) {\\n      edges {\\n        node {\\n          payload\\n        }\\n      }\\n    }\\n  }\\n}\\n\"}`,
171 |                             "method": "POST",
172 |                             "mode": "cors",
173 |                             "credentials": "omit"
174 |                         });
175 |                         const json = await req.json()
176 |                         if (json.data?.input.reports.edges.length > 0) {
177 |                             const hex = json.data.input.reports.edges[0].node.payload.replace(/0x/, '')
178 |                             const strRes = Utils.hex2str(hex)
179 |                             const successOrError = JSON.parse(strRes)
180 |                             if (successOrError.success) {
181 |                                 wPromise.resolve!(successOrError.success)
182 |                             } else {
183 |                                 wPromise.reject!(successOrError.error)
184 |                             }
185 |                             break;
186 |                         }
187 |                         await new Promise((resolve) => setTimeout(resolve, 1000))
188 |                     } catch (e) {
189 |                         console.error(e)
190 |                     }
191 |                 }
192 |             } catch (e) {
193 |                 console.error(e)
194 |             }
195 |         })
196 |     }
197 | 
198 | }
199 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "compilerOptions": {
  3 |     /* Visit https://aka.ms/tsconfig to read more about this file */
  4 |     /* Projects */
  5 |     // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
  6 |     // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
  7 |     // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
  8 |     // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
  9 |     // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
 10 |     // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
 11 |     /* Language and Environment */
 12 |     "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
 13 |     // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
 14 |     // "jsx": "preserve",                                /* Specify what JSX code is generated. */
 15 |     "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
 16 |     // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
 17 |     // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
 18 |     // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
 19 |     // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
 20 |     // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
 21 |     // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
 22 |     // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
 23 |     // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
 24 |     /* Modules */
 25 |     "module": "commonjs", /* Specify what module code is generated. */
 26 |     // "rootDir": "./",                                  /* Specify the root folder within your source files. */
 27 |     // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
 28 |     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
 29 |     // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
 30 |     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
 31 |     // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
 32 |     // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
 33 |     // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
 34 |     // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
 35 |     // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
 36 |     // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
 37 |     // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
 38 |     // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
 39 |     "resolveJsonModule": true,                        /* Enable importing .json files. */
 40 |     // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
 41 |     // "noResolve": true,                                /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
 42 |     /* JavaScript Support */
 43 |     // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
 44 |     // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
 45 |     // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
 46 |     /* Emit */
 47 |     // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
 48 |     // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
 49 |     // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
 50 |     // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
 51 |     // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
 52 |     // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
 53 |     // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
 54 |     // "removeComments": true,                           /* Disable emitting comments. */
 55 |     // "noEmit": true,                                   /* Disable emitting files from a compilation. */
 56 |     // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
 57 |     // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
 58 |     // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
 59 |     // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
 60 |     // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
 61 |     // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
 62 |     // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
 63 |     // "newLine": "crlf",                                /* Set the newline character for emitting files. */
 64 |     // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
 65 |     // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
 66 |     // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
 67 |     // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
 68 |     // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
 69 |     // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
 70 |     /* Interop Constraints */
 71 |     "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
 72 |     // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
 73 |     // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
 74 |     "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
 75 |     // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
 76 |     "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
 77 |     /* Type Checking */
 78 |     "strict": true, /* Enable all strict type-checking options. */
 79 |     // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
 80 |     // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
 81 |     // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
 82 |     // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
 83 |     // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
 84 |     // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
 85 |     // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
 86 |     // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
 87 |     // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
 88 |     // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
 89 |     // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
 90 |     // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
 91 |     // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
 92 |     // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
 93 |     // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
 94 |     // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
 95 |     // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
 96 |     // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
 97 |     /* Completeness */
 98 |     // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
 99 |     "skipLibCheck": true /* Skip type checking all .d.ts files. */
100 |   }
101 | }
102 | 


--------------------------------------------------------------------------------