├── .github └── workflows │ ├── ci.yml │ └── helm-publish.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── Rocket.toml ├── app ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icon.png │ ├── index.html │ ├── manifest.json │ ├── mstile-150x150.png │ ├── robots.txt │ └── safari-pinned-tab.svg ├── src │ ├── AbiApp.tsx │ ├── App.tsx │ ├── components │ │ ├── Copyable.tsx │ │ ├── Providers.tsx │ │ ├── SecondaryButton.tsx │ │ └── shared.tsx │ ├── constants.ts │ ├── context │ │ └── theme.ts │ ├── features │ │ ├── editor │ │ │ ├── components │ │ │ │ ├── AbiEditorView.tsx │ │ │ │ ├── ActionOverlay.tsx │ │ │ │ ├── EditorView.tsx │ │ │ │ ├── ExampleDropdown.tsx │ │ │ │ ├── JsonEditor.tsx │ │ │ │ ├── LogView.tsx │ │ │ │ ├── SolidityEditor.tsx │ │ │ │ ├── SwayEditor.tsx │ │ │ │ └── ToolchainDropdown.tsx │ │ │ ├── examples │ │ │ │ ├── examples.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── solidity │ │ │ │ │ ├── counter.ts │ │ │ │ │ ├── erc20.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── voting.ts │ │ │ │ └── sway │ │ │ │ │ ├── counter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── liquiditypool.ts │ │ │ │ │ ├── multiasset.ts │ │ │ │ │ └── singleasset.ts │ │ │ └── hooks │ │ │ │ ├── useCompile.tsx │ │ │ │ ├── useGist.tsx │ │ │ │ ├── useLog.tsx │ │ │ │ └── useTranspile.tsx │ │ ├── interact │ │ │ ├── components │ │ │ │ ├── CallButton.tsx │ │ │ │ ├── ComplexParameterInput.tsx │ │ │ │ ├── ContractInterface.tsx │ │ │ │ ├── DryrunSwitch.tsx │ │ │ │ ├── FunctionCallAccordion.tsx │ │ │ │ ├── FunctionForm.tsx │ │ │ │ ├── FunctionInterface.tsx │ │ │ │ ├── FunctionParameters.tsx │ │ │ │ ├── FunctionToolbar.tsx │ │ │ │ ├── InteractionDrawer.tsx │ │ │ │ ├── ParameterInput.tsx │ │ │ │ └── ResponseCard.tsx │ │ │ ├── hooks │ │ │ │ ├── useCallFunction.ts │ │ │ │ ├── useContract.ts │ │ │ │ └── useContractFunctions.ts │ │ │ └── utils │ │ │ │ ├── abi.ts │ │ │ │ ├── getTypeInfo.test.ts │ │ │ │ ├── getTypeInfo.ts │ │ │ │ └── modifyJsonStringify.ts │ │ └── toolbar │ │ │ ├── components │ │ │ ├── AbiActionToolbar.tsx │ │ │ ├── ActionToolbar.tsx │ │ │ ├── CompileButton.tsx │ │ │ ├── DeploymentButton.tsx │ │ │ └── SwitchThemeButton.tsx │ │ │ └── hooks │ │ │ ├── useConnectIfNotAlready.ts │ │ │ └── useDeployContract.ts │ ├── hooks │ │ └── useIsMobile.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── utils │ │ ├── localStorage.ts │ │ ├── metrics.test.ts │ │ ├── metrics.ts │ │ ├── queryClient.ts │ │ └── types.ts └── tsconfig.json ├── deployment ├── Dockerfile ├── charts │ ├── Chart.yaml │ ├── templates │ │ ├── _helpers.tpl │ │ └── sway-playground-deploy.yaml │ └── values.yaml ├── ingress │ └── eks │ │ └── sway-playground-ingress.yaml └── scripts │ ├── .env │ ├── sway-playground-delete.sh │ ├── sway-playground-deploy.sh │ ├── sway-playground-ingress-delete.sh │ └── sway-playground-ingress-deploy.sh ├── helm └── sway-playground │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── projects └── swaypad │ ├── Forc.lock │ ├── Forc.toml │ └── src │ └── main.sw └── src ├── compilation ├── mod.rs ├── swaypad.rs └── tooling.rs ├── cors.rs ├── error.rs ├── gist.rs ├── main.rs ├── transpilation ├── mod.rs └── solidity.rs ├── types.rs └── util.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | RUSTFLAGS: -D warnings 11 | REGISTRY: ghcr.io 12 | RUST_VERSION: 1.70.0 13 | NODE_VERSION: '16' 14 | 15 | jobs: 16 | cancel-previous-runs: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Cancel Previous Runs 20 | uses: styfle/cancel-workflow-action@0.9.1 21 | with: 22 | access_token: ${{ github.token }} 23 | 24 | cargo-fmt-check: 25 | needs: cancel-previous-runs 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: ${{ env.RUST_VERSION }} 32 | - name: Check Formatting 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: fmt 36 | args: --all --verbose -- --check 37 | 38 | cargo-clippy: 39 | needs: cancel-previous-runs 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions-rs/toolchain@v1 44 | with: 45 | toolchain: ${{ env.RUST_VERSION }} 46 | - uses: Swatinem/rust-cache@v1 47 | - name: Check Clippy Linter 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: clippy 51 | args: --all-features --all-targets -- -D warnings 52 | 53 | cargo-check: 54 | needs: cancel-previous-runs 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: ${{ env.RUST_VERSION }} 62 | - uses: Swatinem/rust-cache@v1 63 | - name: Run tests 64 | uses: actions-rs/cargo@v1 65 | with: 66 | command: check 67 | args: --verbose 68 | 69 | cargo-test: 70 | needs: cancel-previous-runs 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v2 74 | - uses: actions-rs/toolchain@v1 75 | with: 76 | profile: minimal 77 | toolchain: ${{ env.RUST_VERSION }} 78 | - uses: Swatinem/rust-cache@v1 79 | - name: Run tests 80 | uses: actions-rs/cargo@v1 81 | with: 82 | command: test 83 | args: --verbose --all --all-features 84 | 85 | build-and-publish-image: 86 | if: github.ref == 'refs/heads/master' 87 | needs: 88 | - cargo-fmt-check 89 | - cargo-clippy 90 | - cargo-check 91 | - cargo-test 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Checkout repository 95 | uses: actions/checkout@v2 96 | 97 | - name: Docker meta 98 | id: meta 99 | uses: docker/metadata-action@v3 100 | with: 101 | images: | 102 | ghcr.io/fuellabs/sway-playground 103 | tags: | 104 | type=ref,event=branch 105 | type=sha,prefix= 106 | type=semver,pattern={{raw}} 107 | 108 | - name: Set up Docker Buildx 109 | uses: docker/setup-buildx-action@v1 110 | 111 | - name: Log in to the ghcr.io registry 112 | uses: docker/login-action@v1 113 | with: 114 | registry: ${{ env.REGISTRY }} 115 | username: ${{ github.repository_owner }} 116 | password: ${{ secrets.GITHUB_TOKEN }} 117 | 118 | - name: Build and push the image to ghcr.io 119 | uses: docker/build-push-action@v2 120 | with: 121 | context: . 122 | file: deployment/Dockerfile 123 | push: true 124 | tags: ${{ steps.meta.outputs.tags }} 125 | labels: ${{ steps.meta.outputs.labels }} 126 | cache-from: type=gha 127 | cache-to: type=gha,mode=max 128 | 129 | eslint-check: 130 | needs: cancel-previous-runs 131 | runs-on: ubuntu-latest 132 | steps: 133 | - uses: actions/checkout@v2 134 | - name: Run eslint 135 | run: | 136 | cd app && npm install 137 | npm run lint-check 138 | 139 | prettier-check: 140 | needs: cancel-previous-runs 141 | runs-on: ubuntu-latest 142 | steps: 143 | - uses: actions/checkout@v2 144 | - name: Run prettier 145 | run: | 146 | cd app && npm install 147 | npm run format-check 148 | 149 | frontend-build-and-test: 150 | if: github.ref != 'refs/heads/master' 151 | needs: cancel-previous-runs 152 | runs-on: ubuntu-latest 153 | # Local docker image registry 154 | services: 155 | registry: 156 | image: registry:2 157 | ports: 158 | - 5000:5000 159 | env: 160 | LOCAL_REGISTRY: localhost:5000 161 | LOCAL_TAG: sway-playground:local 162 | steps: 163 | - name: Checkout repository 164 | uses: actions/checkout@v2 165 | 166 | - name: Set up Docker Buildx 167 | uses: docker/setup-buildx-action@v1 168 | with: 169 | driver-opts: network=host 170 | 171 | - name: Log in to the local registry 172 | uses: docker/login-action@v1 173 | with: 174 | registry: ${{ env.LOCAL_REGISTRY }} 175 | username: ${{ github.repository_owner }} 176 | password: ${{ secrets.GITHUB_TOKEN }} 177 | 178 | - name: Build image and push to local registry 179 | uses: docker/build-push-action@v2 180 | with: 181 | context: . 182 | file: deployment/Dockerfile 183 | push: true 184 | tags: ${{ env.LOCAL_REGISTRY }}/${{ env.LOCAL_TAG }} 185 | 186 | - name: Run the service in docker 187 | run: | 188 | docker run -d -p 8080:8080 ${{ env.LOCAL_REGISTRY }}/${{ env.LOCAL_TAG }} 189 | 190 | - name: NPM build and test 191 | env: 192 | CI: true 193 | run: | 194 | cd app 195 | npm ci 196 | npm run build 197 | npm run test 198 | 199 | deploy: 200 | if: github.ref == 'refs/heads/master' 201 | needs: 202 | - build-and-publish-image 203 | runs-on: buildjet-4vcpu-ubuntu-2204 204 | steps: 205 | - name: Set Environment Variables 206 | run: | 207 | tag=(`echo $GITHUB_SHA | cut -c1-7`) 208 | echo "IMAGE_TAG=`echo $tag`" >> $GITHUB_ENV 209 | 210 | - name: Deploy Sway Playground Backend Environment 211 | uses: benc-uk/workflow-dispatch@v1 212 | with: 213 | workflow: Deploy Sway Playground on k8s 214 | repo: FuelLabs/fuel-deployment 215 | ref: refs/heads/master 216 | token: ${{ secrets.REPO_TOKEN }} 217 | inputs: '{ "cluster": "${{ env.CONFIG }}", "deployment-version": "${{ github.sha }}", "image-version": "${{ env.IMAGE_TAG }}" }' 218 | env: 219 | CONFIG: 'fuel-prod-1' 220 | -------------------------------------------------------------------------------- /.github/workflows/helm-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Build and publish Helm Chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'helm/sway-playground/Chart.yaml' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | helm-release: 15 | name: Build Helm Chart 16 | runs-on: buildjet-4vcpu-ubuntu-2204 17 | if: | 18 | (github.event_name == 'release' && github.event.action == 'published') || 19 | github.ref == 'refs/heads/master' || github.event_name == 'pull_request' 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Check out code 26 | uses: actions/checkout@v3 27 | 28 | - name: Package and Push Charts 29 | uses: bsord/helm-push@v4.1.0 30 | with: 31 | useOCIRegistry: true 32 | registry-url: oci://ghcr.io/fuellabs/helmcharts 33 | username: ${{ github.repository_owner }} 34 | access-token: ${{ secrets.GITHUB_TOKEN }} 35 | force: true 36 | chart-folder: ./helm/sway-playground -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | out 3 | target 4 | .vercel 5 | build 6 | node_modules 7 | tmp 8 | .vscode/* 9 | .DS_Store -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sway-playground" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | nanoid = "0.4.0" 8 | fs_extra = "1.2.0" 9 | hex = "0.4.3" 10 | tokio = { version = "1", features = ["full"] } 11 | serde_json = "1.0.91" 12 | regex = "1.7.0" 13 | rocket = { version = "0.5.0-rc.2", features = ["tls", "json"] } 14 | serde = { version = "1.0", features = ["derive"] } 15 | octocrab = "0.38.0" 16 | thiserror = "1.0.60" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sway Playground 2 | 3 | [![docs](https://docs.rs/forc/badge.svg)](https://docs.rs/forc/) 4 | [![discord](https://img.shields.io/badge/chat%20on-discord-orange?&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/xfpK4Pe) 5 | 6 | Sway Playground enables developers to build simple sway contracts in the browser with no installation of tools. This tool is inspired by the Ethereum remix tool or the Rust Playground. 7 | 8 | ## Try it Now! 9 | 10 | [sway-playground.org](https://sway-playground.org) 11 | 12 | ## How it Works 13 | 14 | Sway Playground has a simple multi-threaded Rocket backend server which creates a temp project per compile request, builds the project, removes the temp files and returns the output. 15 | 16 | The frontend uses React and typescript with Ace editor. 17 | 18 | ## Sway Documentation 19 | 20 | For user documentation, including installing release builds, see the Sway Book: . 21 | 22 | ## Building from Source 23 | 24 | This section is for developing the Sway Playground. For developing contracts and using Sway, see the above documentation section. 25 | 26 | ### Dependencies 27 | 28 | Sway Playground is built in Rust and Javascript. To begin, install the Rust toolchain following instructions at . Then configure your Rust toolchain to use Rust `stable`: 29 | 30 | ```sh 31 | rustup default stable 32 | ``` 33 | 34 | If not already done, add the Cargo bin directory to your `PATH` by adding the following line to `~/.profile` and restarting the shell session. 35 | 36 | ```sh 37 | export PATH="${HOME}/.cargo/bin:${PATH}" 38 | ``` 39 | 40 | ### Building Sway Playground 41 | 42 | Clone the repository and build the Sway toolchain: 43 | 44 | ```sh 45 | git clone git@github.com:FuelLabs/sway-playground.git 46 | cd sway-playground 47 | cargo build 48 | ``` 49 | 50 | Confirm the Sway Playground built successfully: 51 | 52 | ```sh 53 | cargo run --bin sway-playground 54 | ``` 55 | 56 | ### Running the Sway Compiler Server 57 | 58 | The server is a simple Hyper server for now. 59 | 60 | ```sh 61 | cargo run 62 | ``` 63 | 64 | Alternatively, it can be run locally with Docker, as it is in the deployed environment. 65 | 66 | ```sh 67 | # forc is not fully supported on arm linux, see https://github.com/FuelLabs/sway/issues/5760 68 | docker buildx build --platform linux/amd64 -f deployment/Dockerfile . 69 | docker run -p 8080:8080 -d 70 | ``` 71 | 72 | ### Running the Frontend 73 | 74 | The frontend is just a simple static frontend and can be hosted anywhere. 75 | 76 | ```sh 77 | cd app 78 | npm start 79 | ``` 80 | 81 | This will open http://localhost:3000 in your browser. By default, it will use the production backend endpoint. 82 | 83 | To test against the backend running locally, you can use the environment variable `REACT_APP_LOCAL_SERVER` when you start the app, like this: 84 | 85 | ```sh 86 | REACT_APP_LOCAL_SERVER=true npm start 87 | ``` 88 | 89 | ## Contributing to Sway 90 | 91 | We welcome contributions to Sway Playground, for general contributing guidelines please consult the Sway Contributing Documentation for now. 92 | 93 | Please see the [Contributing To Sway](https://docs.fuel.network/docs/sway/reference/contributing_to_sway/) section of the Sway book for guidelines and instructions to help you get started. 94 | 95 | ## Todo 96 | 97 | - UI design in line with other Fuel apps. 98 | - Ace Editor support for Sway syntax highlighting. 99 | - Ensuring IO non-blocking (not sure if the server is truly non-blocking and multi-threaded), might need tokio IO. 100 | - Better CI to always make available the latest stable version of Sway. 101 | - Support for deploying and testing contracts. 102 | - React unit tests. 103 | -------------------------------------------------------------------------------- /Rocket.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | address = "0.0.0.0" 3 | port = 8080 4 | workers = 16 5 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.0", 7 | "@emotion/styled": "^11.11.0", 8 | "@fuel-ui/css": "^0.23", 9 | "@fuel-ui/react": "^0.23.3", 10 | "@fuels/connectors": "0.5.0", 11 | "@fuels/react": "0.36.0", 12 | "@mui/base": "^5.0.0-beta.2", 13 | "@mui/icons-material": "^5.11.16", 14 | "@mui/lab": "^5.0.0-alpha.46", 15 | "@mui/material": "^5.13.2", 16 | "@tanstack/react-query": "5.35.1", 17 | "@vercel/analytics": "^1.2.2", 18 | "ace-builds": "^1.22.0", 19 | "ace-mode-solidity": "^0.1.1", 20 | "ansicolor": "^1.1.100", 21 | "await-timeout": "^1.1.1", 22 | "fuels": "0.96.1", 23 | "react": "^18.2.0", 24 | "react-ace": "^10.1.0", 25 | "react-dom": "^18.2.0", 26 | "react-router-dom": "^6.23.0", 27 | "react-scripts": "^5.0.1", 28 | "typescript": "^5.4.5", 29 | "web-vitals": "^2.1.4" 30 | }, 31 | "devDependencies": { 32 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 33 | "@testing-library/jest-dom": "^5.16.5", 34 | "@testing-library/react": "^13.4.0", 35 | "@testing-library/user-event": "^13.5.0", 36 | "@types/await-timeout": "^0.3.3", 37 | "@types/jest": "^27.5.2", 38 | "@types/node": "^16.18.32", 39 | "@types/react": "^18.2.6", 40 | "@types/react-dom": "^18.2.4", 41 | "eslint-config-prettier": "^9.1.0", 42 | "eslint-plugin-prettier": "^5.1.3", 43 | "prettier": "^3.3.1", 44 | "source-map-explorer": "^2.5.3", 45 | "source-map-loader": "^4.0.1" 46 | }, 47 | "overrides": { 48 | "react-scripts": { 49 | "typescript": "^5" 50 | } 51 | }, 52 | "scripts": { 53 | "analyze": "source-map-explorer 'build/static/js/*.js'", 54 | "start": "DISABLE_ESLINT_PLUGIN=true react-scripts start", 55 | "build": "react-scripts build", 56 | "test": "react-scripts test", 57 | "eject": "react-scripts eject", 58 | "lint": "eslint --fix 'src/**/*.{ts,tsx}'", 59 | "lint-check": "eslint 'src/**/*.{ts,tsx}'", 60 | "format": "prettier --write 'src/**/*.{ts,tsx}'", 61 | "format-check": "prettier --check 'src/**/*.{ts,tsx}' --max-warnings=0" 62 | }, 63 | "eslintConfig": { 64 | "plugins": [ 65 | "@typescript-eslint", 66 | "prettier" 67 | ], 68 | "extends": [ 69 | "react-app", 70 | "react-app/jest", 71 | "eslint:recommended", 72 | "plugin:@typescript-eslint/recommended", 73 | "plugin:prettier/recommended" 74 | ] 75 | }, 76 | "browserslist": { 77 | "production": [ 78 | ">0.2%", 79 | "not dead", 80 | "not op_mini all" 81 | ], 82 | "development": [ 83 | "last 1 chrome version", 84 | "last 1 firefox version", 85 | "last 1 safari version" 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /app/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffc40d 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/icon.png -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Sway Playground 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Sway Playground", 3 | "name": "Sway Playground", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "favicon.ico", 17 | "sizes": "64x64 32x32 24x24 16x16", 18 | "type": "image/x-icon" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /app/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/sway-playground/39549a0e4d28f71967ebe0cdfb40e4bcae599baf/app/public/mstile-150x150.png -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 126 | 128 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /app/src/AbiApp.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import LogView from "./features/editor/components/LogView"; 3 | import { loadAbi, saveAbi, saveSwayCode } from "./utils/localStorage"; 4 | import InteractionDrawer from "./features/interact/components/InteractionDrawer"; 5 | import { useLog } from "./features/editor/hooks/useLog"; 6 | import { Analytics } from "@vercel/analytics/react"; 7 | import useTheme from "./context/theme"; 8 | import AbiActionToolbar from "./features/toolbar/components/AbiActionToolbar"; 9 | import AbiEditorView from "./features/editor/components/AbiEditorView"; 10 | 11 | const DRAWER_WIDTH = "40vw"; 12 | 13 | function AbiApp() { 14 | // The current sway code in the editor. 15 | const [abiCode, setAbiCode] = useState(loadAbi()); 16 | 17 | // Functions for reading and writing to the log output. 18 | const [log, updateLog] = useLog(); 19 | 20 | // The contract ID of the deployed contract. 21 | const [contractId, setContractId] = useState(""); 22 | 23 | // An error message to display to the user. 24 | const [drawerOpen, setDrawerOpen] = useState(false); 25 | 26 | // The theme color for the app. 27 | const { themeColor } = useTheme(); 28 | 29 | // Update the ABI in localstorage when the editor changes. 30 | useEffect(() => { 31 | saveAbi(abiCode); 32 | }, [abiCode]); 33 | 34 | const onSwayCodeChange = useCallback( 35 | (code: string) => { 36 | saveSwayCode(code); 37 | setAbiCode(code); 38 | }, 39 | [setAbiCode], 40 | ); 41 | 42 | return ( 43 |
50 | 51 |
60 | 66 | 67 |
68 | 74 | 75 |
76 | ); 77 | } 78 | 79 | export default AbiApp; 80 | -------------------------------------------------------------------------------- /app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import ActionToolbar from "./features/toolbar/components/ActionToolbar"; 3 | import LogView from "./features/editor/components/LogView"; 4 | import { useCompile } from "./features/editor/hooks/useCompile"; 5 | import { DeployState } from "./utils/types"; 6 | import { 7 | loadSolidityCode, 8 | loadSwayCode, 9 | saveSolidityCode, 10 | saveSwayCode, 11 | } from "./utils/localStorage"; 12 | import InteractionDrawer from "./features/interact/components/InteractionDrawer"; 13 | import { useLog } from "./features/editor/hooks/useLog"; 14 | import { 15 | Toolchain, 16 | isToolchain, 17 | } from "./features/editor/components/ToolchainDropdown"; 18 | import { useTranspile } from "./features/editor/hooks/useTranspile"; 19 | import EditorView from "./features/editor/components/EditorView"; 20 | import { Analytics, track } from "@vercel/analytics/react"; 21 | import { useGist } from "./features/editor/hooks/useGist"; 22 | import { useSearchParams } from "react-router-dom"; 23 | import Copyable from "./components/Copyable"; 24 | import useTheme from "./context/theme"; 25 | 26 | const DRAWER_WIDTH = "40vw"; 27 | 28 | function App() { 29 | // The current sway code in the editor. 30 | const [swayCode, setSwayCode] = useState(loadSwayCode()); 31 | 32 | // The current solidity code in the editor. 33 | const [solidityCode, setSolidityCode] = useState(loadSolidityCode()); 34 | 35 | // An error message to display to the user. 36 | const [showSolidity, setShowSolidity] = useState(false); 37 | 38 | // The most recent code that the user has requested to compile. 39 | const [codeToCompile, setCodeToCompile] = useState( 40 | undefined, 41 | ); 42 | 43 | // The most recent code that the user has requested to transpile. 44 | const [codeToTranspile, setCodeToTranspile] = useState( 45 | undefined, 46 | ); 47 | 48 | // Whether or not the current code in the editor has been compiled. 49 | const [isCompiled, setIsCompiled] = useState(false); 50 | 51 | // The toolchain to use for compilation. 52 | const [toolchain, setToolchain] = useState("testnet"); 53 | 54 | // The deployment state 55 | const [deployState, setDeployState] = useState(DeployState.NOT_DEPLOYED); 56 | 57 | // Functions for reading and writing to the log output. 58 | const [log, updateLog] = useLog(); 59 | 60 | // The contract ID of the deployed contract. 61 | const [contractId, setContractId] = useState(""); 62 | 63 | // An error message to display to the user. 64 | const [drawerOpen, setDrawerOpen] = useState(false); 65 | 66 | // The query parameters for the current URL. 67 | const [searchParams] = useSearchParams(); 68 | 69 | // The theme color for the app. 70 | const { themeColor } = useTheme(); 71 | 72 | // If showSolidity is toggled on, reset the compiled state. 73 | useEffect(() => { 74 | if (showSolidity) { 75 | setIsCompiled(false); 76 | } 77 | }, [showSolidity]); 78 | 79 | // Load the query parameters from the URL and set the state accordingly. Gists are loaded in useGist. 80 | useEffect(() => { 81 | if (searchParams.get("transpile") === "true") { 82 | setShowSolidity(true); 83 | } 84 | const toolchainParam = searchParams.get("toolchain"); 85 | 86 | if (isToolchain(toolchainParam)) { 87 | setToolchain(toolchainParam); 88 | } 89 | }, [searchParams, setShowSolidity, setToolchain]); 90 | 91 | const onSwayCodeChange = useCallback( 92 | (code: string) => { 93 | saveSwayCode(code); 94 | setSwayCode(code); 95 | setIsCompiled(false); 96 | }, 97 | [setSwayCode], 98 | ); 99 | 100 | const onSolidityCodeChange = useCallback( 101 | (code: string) => { 102 | saveSolidityCode(code); 103 | setSolidityCode(code); 104 | setIsCompiled(false); 105 | }, 106 | [setSolidityCode], 107 | ); 108 | 109 | // Loading shared code by query parameter and get a function for creating sharable permalinks. 110 | const { newGist } = useGist(onSwayCodeChange, onSolidityCodeChange); 111 | 112 | const setError = useCallback( 113 | (error: string | undefined) => { 114 | updateLog(error); 115 | }, 116 | [updateLog], 117 | ); 118 | 119 | const onShareClick = useCallback(async () => { 120 | track("Share Click", { toolchain }); 121 | const response = await newGist(swayCode, { 122 | contract: solidityCode, 123 | language: "solidity", 124 | }); 125 | if (response) { 126 | const permalink = `${window.location.origin}/?toolchain=${toolchain}&transpile=${showSolidity}&gist=${response.id}`; 127 | updateLog([ 128 | , 134 | , 140 | ]); 141 | } 142 | }, [newGist, swayCode, solidityCode, updateLog, toolchain, showSolidity]); 143 | 144 | const onCompileClick = useCallback(() => { 145 | track("Compile Click", { toolchain }); 146 | if (showSolidity) { 147 | // Transpile the Solidity code before compiling. 148 | track("Transpile"); 149 | setCodeToTranspile(solidityCode); 150 | } else { 151 | setCodeToCompile(swayCode); 152 | } 153 | }, [ 154 | showSolidity, 155 | swayCode, 156 | solidityCode, 157 | setCodeToCompile, 158 | setCodeToTranspile, 159 | toolchain, 160 | ]); 161 | 162 | useTranspile( 163 | codeToTranspile, 164 | setCodeToCompile, 165 | onSwayCodeChange, 166 | setError, 167 | updateLog, 168 | ); 169 | useCompile(codeToCompile, setError, setIsCompiled, updateLog, toolchain); 170 | 171 | return ( 172 |
179 | 192 |
201 | 210 | 211 |
212 | 218 | 219 |
220 | ); 221 | } 222 | 223 | export default App; 224 | -------------------------------------------------------------------------------- /app/src/components/Copyable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconButton from "@mui/material/IconButton"; 3 | import Tooltip from "@mui/material/Tooltip"; 4 | import ContentCopyIcon from "@mui/icons-material/ContentCopy"; 5 | import useTheme from "../context/theme"; 6 | 7 | export interface CopyableProps { 8 | value: string; 9 | label: string; 10 | tooltip: string; 11 | href?: boolean; 12 | } 13 | 14 | async function handleCopy(value: string) { 15 | await navigator.clipboard.writeText(value); 16 | } 17 | 18 | function Copyable({ value, label, tooltip, href }: CopyableProps) { 19 | const { themeColor } = useTheme(); 20 | 21 | return ( 22 |
handleCopy(value)} 25 | > 26 | 27 | 28 | {href ? ( 29 | 35 | {label} 36 | 37 | ) : ( 38 | {label} 39 | )} 40 | 41 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | 49 | export default Copyable; 50 | -------------------------------------------------------------------------------- /app/src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { globalCss } from "@fuel-ui/css"; 2 | import type { ReactNode } from "react"; 3 | import { QueryClientProvider } from "@tanstack/react-query"; 4 | import { queryClient } from "../utils/queryClient"; 5 | import { FuelProvider } from "@fuels/react"; 6 | import { defaultConnectors } from "@fuels/connectors"; 7 | import ThemeProvider from "@mui/material/styles/ThemeProvider"; 8 | import useTheme from "../context/theme"; 9 | 10 | type ProvidersProps = { 11 | children: ReactNode; 12 | }; 13 | 14 | export function Providers({ children }: ProvidersProps) { 15 | const { muiTheme, theme } = useTheme(); 16 | 17 | return ( 18 | 19 | 20 | 26 | {globalCss()()} 27 | {children} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/src/components/SecondaryButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@mui/material/Button"; 3 | import Tooltip from "@mui/material/Tooltip"; 4 | import useTheme from "../context/theme"; 5 | 6 | export interface SecondaryButtonProps { 7 | onClick: () => void; 8 | text: string; 9 | endIcon?: React.ReactNode; 10 | disabled?: boolean; 11 | tooltip?: string; 12 | style?: React.CSSProperties; 13 | header?: boolean; 14 | } 15 | function SecondaryButton({ 16 | onClick, 17 | text, 18 | endIcon, 19 | disabled, 20 | tooltip, 21 | style, 22 | header, 23 | }: SecondaryButtonProps) { 24 | if (header) { 25 | style = { 26 | ...style, 27 | minWidth: "105px", 28 | height: "40px", 29 | marginRight: "15px", 30 | marginBottom: "10px", 31 | }; 32 | } 33 | 34 | const { themeColor } = useTheme(); 35 | 36 | return ( 37 | 38 | 39 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default SecondaryButton; 66 | -------------------------------------------------------------------------------- /app/src/components/shared.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import CircularProgress from "@mui/material/CircularProgress"; 3 | import Copyable from "./Copyable"; 4 | import { lightColors } from "@fuel-ui/css"; 5 | import { ColorName } from "../context/theme"; 6 | 7 | export const StyledBorder = styled.div<{ 8 | themeColor: (name: ColorName) => string; 9 | }>` 10 | border: 4px solid ${(props) => props.themeColor("gray4")}; 11 | border-radius: 5px; 12 | `; 13 | 14 | export const ButtonSpinner = () => ( 15 | 23 | ); 24 | 25 | export const CopyableHex = ({ 26 | hex, 27 | tooltip, 28 | }: { 29 | hex: string; 30 | tooltip: string; 31 | }) => { 32 | const formattedHex = hex.slice(0, 6) + "..." + hex.slice(-4, hex.length); 33 | return ; 34 | }; 35 | -------------------------------------------------------------------------------- /app/src/constants.ts: -------------------------------------------------------------------------------- 1 | const SERVER_API = 2 | process.env.REACT_APP_SERVER_API || 3 | "https://api.sway-playground.fuel.network"; 4 | 5 | export const FUEL_GREEN = "#00f58c"; 6 | export const LOCAL_SERVER_URI = "http://0.0.0.0:8080"; 7 | export const SERVER_URI = process.env.REACT_APP_LOCAL_SERVER 8 | ? LOCAL_SERVER_URI 9 | : SERVER_API; 10 | -------------------------------------------------------------------------------- /app/src/context/theme.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { darkColors, lightColors } from "@fuel-ui/css"; 3 | import { useFuelTheme } from "@fuel-ui/react"; 4 | import createTheme from "@mui/material/styles/createTheme"; 5 | 6 | interface ColorMapping { 7 | light: string; 8 | dark: string; 9 | } 10 | export type ColorName = 11 | | "black1" 12 | | "white1" 13 | | "white2" 14 | | "white3" 15 | | "white4" 16 | | "gray1" 17 | | "gray2" 18 | | "gray3" 19 | | "gray4" 20 | | "gray5" 21 | | "gray6" 22 | | "sgreen1" 23 | | "disabled1" 24 | | "disabled2" 25 | | "disabled3"; 26 | 27 | const COLORS: Record = { 28 | black1: { 29 | light: darkColors.black, 30 | dark: lightColors.gray4, 31 | }, 32 | white1: { 33 | light: lightColors.whiteA5, 34 | dark: darkColors.scalesGreen2, 35 | }, 36 | white2: { 37 | light: lightColors.white, 38 | dark: darkColors.scalesGray1, 39 | }, 40 | white3: { 41 | light: "#00000099", 42 | dark: darkColors.gray12, 43 | }, 44 | white4: { 45 | light: lightColors.gray2, 46 | dark: darkColors.black, 47 | }, 48 | gray1: { 49 | light: darkColors.gray6, 50 | dark: darkColors.gray11, 51 | }, 52 | gray2: { 53 | light: darkColors.gray10, 54 | dark: lightColors.gray10, 55 | }, 56 | gray3: { 57 | light: darkColors.scalesGray1, 58 | dark: lightColors.white, 59 | }, 60 | gray4: { 61 | light: lightColors.gray7, 62 | dark: darkColors.scalesGray1, 63 | }, 64 | gray5: { 65 | light: lightColors.gray1, 66 | dark: darkColors.scalesGray2, 67 | }, 68 | gray6: { 69 | light: "lightgrey", 70 | dark: darkColors.gray3, 71 | }, 72 | sgreen1: { 73 | light: lightColors.scalesGreen3, 74 | dark: darkColors.scalesGreen2, 75 | }, 76 | disabled1: { 77 | light: "", 78 | dark: darkColors.gray8, 79 | }, 80 | disabled2: { 81 | light: lightColors.scalesGreen4, 82 | dark: darkColors.scalesGreen3, 83 | }, 84 | disabled3: { 85 | light: "", 86 | dark: darkColors.gray1, 87 | }, 88 | }; 89 | 90 | type Theme = "light" | "dark"; 91 | 92 | export default function useTheme() { 93 | const { current: currentTheme, setTheme } = useFuelTheme(); 94 | 95 | const themeColor = useCallback( 96 | (name: ColorName) => COLORS[name][currentTheme as Theme], 97 | [currentTheme], 98 | ); 99 | const editorTheme = useMemo( 100 | () => (currentTheme === "light" ? "chrome" : "tomorrow_night"), 101 | [currentTheme], 102 | ); 103 | const muiTheme = createTheme({ 104 | palette: { 105 | mode: currentTheme as Theme, 106 | }, 107 | }); 108 | return { 109 | theme: currentTheme as Theme, 110 | editorTheme, 111 | setTheme, 112 | themeColor, 113 | muiTheme, 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /app/src/features/editor/components/AbiEditorView.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useIsMobile } from "../../../hooks/useIsMobile"; 3 | import JsonEditor from "./JsonEditor"; 4 | import { TextField } from "@mui/material"; 5 | 6 | export interface AbiEditorViewProps { 7 | abiCode: string; 8 | onAbiCodeChange: (value: string) => void; 9 | contractId: string; 10 | setContractId: (contractId: string) => void; 11 | } 12 | 13 | function AbiEditorView({ 14 | abiCode, 15 | onAbiCodeChange, 16 | contractId, 17 | setContractId, 18 | }: AbiEditorViewProps) { 19 | const isMobile = useIsMobile(); 20 | 21 | return ( 22 |
23 | setContractId(e.target.value)} 30 | style={{ width: "100%", marginBottom: "10px" }} 31 | /> 32 |
44 | 45 |
46 |
47 | ); 48 | } 49 | 50 | export default AbiEditorView; 51 | -------------------------------------------------------------------------------- /app/src/features/editor/components/ActionOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ToolchainDropdown, { Toolchain } from "./ToolchainDropdown"; 3 | import ExampleDropdown from "./ExampleDropdown"; 4 | import { EXAMPLE_CONTRACTS } from "../examples"; 5 | 6 | export type EditorLanguage = "sway" | "solidity"; 7 | 8 | export interface ActionOverlayProps { 9 | handleSelectExample: (example: string) => void; 10 | toolchain?: Toolchain; 11 | setToolchain?: (toolchain: Toolchain) => void; 12 | editorLanguage: EditorLanguage; 13 | } 14 | 15 | function ActionOverlay({ 16 | handleSelectExample, 17 | toolchain, 18 | setToolchain, 19 | editorLanguage, 20 | }: ActionOverlayProps) { 21 | return ( 22 |
23 |
32 |
40 | {toolchain && setToolchain && ( 41 | 46 | )} 47 | 51 |
52 |
53 |
54 | ); 55 | } 56 | 57 | export default ActionOverlay; 58 | -------------------------------------------------------------------------------- /app/src/features/editor/components/EditorView.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SolidityEditor from "./SolidityEditor"; 3 | import SwayEditor from "./SwayEditor"; 4 | import { Toolchain } from "./ToolchainDropdown"; 5 | import { useIsMobile } from "../../../hooks/useIsMobile"; 6 | 7 | export interface EditorViewProps { 8 | swayCode: string; 9 | onSwayCodeChange: (value: string) => void; 10 | solidityCode: string; 11 | onSolidityCodeChange: (value: string) => void; 12 | toolchain: Toolchain; 13 | setToolchain: (toolchain: Toolchain) => void; 14 | showSolidity: boolean; 15 | } 16 | 17 | function EditorView({ 18 | swayCode, 19 | solidityCode, 20 | onSolidityCodeChange, 21 | onSwayCodeChange, 22 | toolchain, 23 | setToolchain, 24 | showSolidity, 25 | }: EditorViewProps) { 26 | const isMobile = useIsMobile(); 27 | 28 | return ( 29 |
41 | {showSolidity && ( 42 | 43 | )} 44 | 50 |
51 | ); 52 | } 53 | 54 | export default EditorView; 55 | -------------------------------------------------------------------------------- /app/src/features/editor/components/ExampleDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import Tooltip from "@mui/material/Tooltip"; 3 | import MenuItem from "@mui/material/MenuItem"; 4 | import FormControl from "@mui/material/FormControl/FormControl"; 5 | import Select, { SelectChangeEvent } from "@mui/material/Select/Select"; 6 | import InputLabel from "@mui/material/InputLabel/InputLabel"; 7 | 8 | export interface ExampleMenuItem { 9 | label: string; 10 | code: string; 11 | } 12 | 13 | export interface ExampleDropdownProps { 14 | handleSelect: (example: string) => void; 15 | examples: ExampleMenuItem[]; 16 | style?: React.CSSProperties; 17 | } 18 | 19 | function ExampleDropdown({ 20 | handleSelect, 21 | examples, 22 | style, 23 | }: ExampleDropdownProps) { 24 | const [currentExample, setCurrentExample] = React.useState({ 25 | label: "", 26 | code: "", 27 | }); 28 | 29 | const onChange = useCallback( 30 | (event: SelectChangeEvent) => { 31 | const index = event.target.value as unknown as number; 32 | const example = examples[index]; 33 | if (example) { 34 | setCurrentExample(example); 35 | handleSelect(example.code); 36 | } 37 | }, 38 | [handleSelect, setCurrentExample, examples], 39 | ); 40 | 41 | return ( 42 | 43 | Example 44 | 45 | 46 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default ExampleDropdown; 68 | -------------------------------------------------------------------------------- /app/src/features/editor/components/JsonEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AceEditor from "react-ace"; 3 | import "ace-builds/webpack-resolver"; 4 | import "ace-builds/src-noconflict/mode-json"; 5 | import "ace-builds/src-noconflict/theme-chrome"; 6 | import "ace-builds/src-noconflict/theme-tomorrow_night_bright"; 7 | import { StyledBorder } from "../../../components/shared"; 8 | import useTheme from "../../../context/theme"; 9 | 10 | export interface JsonEditorProps { 11 | code: string; 12 | onChange: (value: string) => void; 13 | } 14 | 15 | function JsonEditor({ code, onChange }: JsonEditorProps) { 16 | const { editorTheme, themeColor } = useTheme(); 17 | 18 | return ( 19 | 20 | 33 | 34 | ); 35 | } 36 | 37 | export default JsonEditor; 38 | -------------------------------------------------------------------------------- /app/src/features/editor/components/LogView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import useTheme from "../../../context/theme"; 3 | import { StyledBorder } from "../../../components/shared"; 4 | 5 | export interface LogViewProps { 6 | results: React.ReactElement[]; 7 | } 8 | 9 | function LogView({ results }: LogViewProps) { 10 | const { themeColor } = useTheme(); 11 | 12 | const scrollRef = useRef(null); 13 | 14 | // Scroll to the bottom of the results when they change. 15 | useEffect(() => { 16 | if (scrollRef.current) { 17 | scrollRef.current.scrollIntoView({ behavior: "smooth" }); 18 | } 19 | }, [results]); 20 | 21 | return ( 22 | 33 |
34 |         {results.map((element, index) => (
35 |           
{element}
36 | ))} 37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export default LogView; 44 | -------------------------------------------------------------------------------- /app/src/features/editor/components/SolidityEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AceEditor from "react-ace"; 3 | import "ace-builds/webpack-resolver"; 4 | import "ace-builds/src-noconflict/mode-rust"; 5 | import "ace-builds/src-noconflict/theme-chrome"; 6 | import "ace-builds/src-noconflict/theme-tomorrow_night_bright"; 7 | import { StyledBorder } from "../../../components/shared"; 8 | import "ace-mode-solidity/build/remix-ide/mode-solidity"; 9 | import ActionOverlay from "./ActionOverlay"; 10 | import { useIsMobile } from "../../../hooks/useIsMobile"; 11 | import useTheme from "../../../context/theme"; 12 | 13 | export interface SolidityEditorProps { 14 | code: string; 15 | onChange: (value: string) => void; 16 | } 17 | 18 | function SolidityEditor({ code, onChange }: SolidityEditorProps) { 19 | const isMobile = useIsMobile(); 20 | 21 | const { editorTheme, themeColor } = useTheme(); 22 | 23 | return ( 24 | 32 | 33 | 46 | 47 | ); 48 | } 49 | 50 | export default SolidityEditor; 51 | -------------------------------------------------------------------------------- /app/src/features/editor/components/SwayEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AceEditor from "react-ace"; 3 | import "ace-builds/webpack-resolver"; 4 | import "ace-builds/src-noconflict/mode-rust"; 5 | import "ace-builds/src-noconflict/theme-chrome"; 6 | import "ace-builds/src-noconflict/theme-tomorrow_night_bright"; 7 | import { StyledBorder } from "../../../components/shared"; 8 | import ActionOverlay from "./ActionOverlay"; 9 | import { Toolchain } from "./ToolchainDropdown"; 10 | import useTheme from "../../../context/theme"; 11 | 12 | export interface SwayEditorProps { 13 | code: string; 14 | onChange: (value: string) => void; 15 | toolchain: Toolchain; 16 | setToolchain: (toolchain: Toolchain) => void; 17 | } 18 | 19 | function SwayEditor({ 20 | code, 21 | onChange, 22 | toolchain, 23 | setToolchain, 24 | }: SwayEditorProps) { 25 | const { editorTheme, themeColor } = useTheme(); 26 | 27 | return ( 28 | 29 | 35 | 48 | 49 | ); 50 | } 51 | 52 | export default SwayEditor; 53 | -------------------------------------------------------------------------------- /app/src/features/editor/components/ToolchainDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Tooltip from "@mui/material/Tooltip"; 3 | import Select from "@mui/material/Select"; 4 | import MenuItem from "@mui/material/MenuItem"; 5 | import FormControl from "@mui/material/FormControl"; 6 | import InputLabel from "@mui/material/InputLabel/InputLabel"; 7 | 8 | const ToolchainNames = ["testnet", "mainnet", "latest", "nightly"] as const; 9 | export type Toolchain = (typeof ToolchainNames)[number]; 10 | 11 | export function isToolchain(value: string | null): value is Toolchain { 12 | const found = ToolchainNames.find((name) => name === value); 13 | return !!value && found !== undefined; 14 | } 15 | 16 | export interface ToolchainDropdownProps { 17 | toolchain: Toolchain; 18 | setToolchain: (toolchain: Toolchain) => void; 19 | style?: React.CSSProperties; 20 | } 21 | 22 | function ToolchainDropdown({ 23 | toolchain, 24 | setToolchain, 25 | style, 26 | }: ToolchainDropdownProps) { 27 | return ( 28 | 29 | Toolchain 30 | 31 | 32 | 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | export default ToolchainDropdown; 54 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/examples.test.ts: -------------------------------------------------------------------------------- 1 | import { EXAMPLE_CONTRACTS } from "."; 2 | import { LOCAL_SERVER_URI } from "../../../constants"; 3 | import { ExampleMenuItem } from "../components/ExampleDropdown"; 4 | 5 | describe(`test examples`, () => { 6 | describe(`transpile solidity examples`, () => { 7 | const uri = `${LOCAL_SERVER_URI}/transpile`; 8 | 9 | it.each( 10 | EXAMPLE_CONTRACTS["solidity"].map(({ label, code }: ExampleMenuItem) => [ 11 | label, 12 | code, 13 | ]), 14 | )("%s", async (_label, code) => { 15 | // Call server 16 | const request = new Request(uri, { 17 | method: "POST", 18 | body: JSON.stringify({ 19 | contract: code, 20 | language: "solidity", 21 | }), 22 | }); 23 | 24 | const response = await fetch(request); 25 | const { error, swayContract } = await response.json(); 26 | 27 | expect(error).toBeUndefined(); 28 | expect(swayContract).toContain("contract;"); 29 | }); 30 | }); 31 | 32 | describe(`compile sway examples`, () => { 33 | const uri = `${LOCAL_SERVER_URI}/compile`; 34 | 35 | it.each( 36 | EXAMPLE_CONTRACTS["sway"].map(({ label, code }: ExampleMenuItem) => [ 37 | label, 38 | code, 39 | ]), 40 | )( 41 | "%s", 42 | async (_label, code) => { 43 | // Call server 44 | const request = new Request(uri, { 45 | method: "POST", 46 | body: JSON.stringify({ 47 | contract: code, 48 | toolchain: "testnet", 49 | }), 50 | }); 51 | 52 | const response = await fetch(request); 53 | const { error, abi, bytecode, storageSlots, forcVersion } = 54 | await response.json(); 55 | 56 | expect(error).toBeUndefined(); 57 | expect(abi).toBeDefined(); 58 | expect(bytecode).toBeDefined(); 59 | expect(storageSlots).toBeDefined(); 60 | expect(forcVersion).toBeDefined(); 61 | }, 62 | 40000, 63 | ); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/index.ts: -------------------------------------------------------------------------------- 1 | import { EditorLanguage } from "../components/ActionOverlay"; 2 | import { ExampleMenuItem } from "../components/ExampleDropdown"; 3 | import { EXAMPLE_SOLIDITY_CONTRACTS } from "./solidity"; 4 | import { EXAMPLE_SWAY_CONTRACTS } from "./sway"; 5 | 6 | export const EXAMPLE_CONTRACTS: Record = { 7 | sway: EXAMPLE_SWAY_CONTRACTS, 8 | solidity: EXAMPLE_SOLIDITY_CONTRACTS, 9 | }; 10 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/solidity/counter.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SOLIDITY_CONTRACT_COUNTER = `pragma solidity ^0.8.24; 2 | 3 | contract Counter { 4 | uint64 count; 5 | 6 | function get() public view returns (uint64) { 7 | return count; 8 | } 9 | 10 | function increment() public { 11 | count += 1; 12 | } 13 | }`; 14 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/solidity/erc20.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SOLIDITY_CONTRACT_ERC20 = `pragma solidity ^0.8.24; 2 | /// token.sol -- ERC20 implementation with minting and burning. 3 | /// Based on DSToken contract with many modifications. 4 | 5 | contract ERC20 { 6 | address public owner; 7 | bool public stopped; 8 | uint256 public totalSupply; 9 | mapping (address => uint256) public balanceOf; 10 | mapping (address => mapping (address => uint256)) public allowance; 11 | string public symbol; 12 | uint8 public decimals = 18; // standard token precision. override to customize 13 | string public name = ""; // Optional token name 14 | 15 | constructor(string memory symbol_) public { 16 | symbol = symbol_; 17 | owner = msg.sender; 18 | } 19 | 20 | event Approval(address indexed src, address indexed guy, uint wad); 21 | event Transfer(address indexed src, address indexed dst, uint wad); 22 | event Mint(address indexed guy, uint wad); 23 | event Burn(address indexed guy, uint wad); 24 | event Stop(); 25 | event Start(); 26 | 27 | function approve(address guy, uint wad) public returns (bool) { 28 | require(!stopped, "ds-stop-is-stopped"); 29 | allowance[msg.sender][guy] = wad; 30 | 31 | emit Approval(msg.sender, guy, wad); 32 | 33 | return true; 34 | } 35 | 36 | function transfer(address dst, uint wad) external returns (bool) { 37 | return transferFrom(msg.sender, dst, wad); 38 | } 39 | 40 | function transferFrom(address src, address dst, uint wad) 41 | public 42 | returns (bool) 43 | { 44 | require(!stopped, "ds-stop-is-stopped"); 45 | if (src != msg.sender && allowance[src][msg.sender] != 0xFFFFFFFFFFFFFFFF) { 46 | require(allowance[src][msg.sender] >= wad, "ds-token-insufficient-approval"); 47 | allowance[src][msg.sender] = allowance[src][msg.sender] - wad; 48 | } 49 | 50 | require(balanceOf[src] >= wad, "ds-token-insufficient-balance"); 51 | balanceOf[src] = balanceOf[src] - wad; 52 | balanceOf[dst] = balanceOf[dst] + wad; 53 | 54 | emit Transfer(src, dst, wad); 55 | 56 | return true; 57 | } 58 | 59 | function mint(address guy, uint wad) public { 60 | require(!stopped, "ds-stop-is-stopped"); 61 | require(msg.sender == owner, "ds-auth"); 62 | balanceOf[guy] = balanceOf[guy] + wad; 63 | totalSupply = totalSupply + wad; 64 | emit Mint(guy, wad); 65 | } 66 | 67 | function burn(address guy, uint wad) public { 68 | require(!stopped, "ds-stop-is-stopped"); 69 | require(msg.sender == owner, "ds-auth"); 70 | if (guy != msg.sender && allowance[guy][msg.sender] != 0xFFFFFFFFFFFFFFFF) { 71 | require(allowance[guy][msg.sender] >= wad, "ds-token-insufficient-approval"); 72 | allowance[guy][msg.sender] = allowance[guy][msg.sender] - wad; 73 | } 74 | 75 | require(balanceOf[guy] >= wad, "ds-token-insufficient-balance"); 76 | balanceOf[guy] = balanceOf[guy] - wad; 77 | totalSupply = totalSupply - wad; 78 | emit Burn(guy, wad); 79 | } 80 | 81 | function stop() public { 82 | require(msg.sender == owner, "ds-auth"); 83 | stopped = true; 84 | emit Stop(); 85 | } 86 | 87 | function start() public { 88 | require(msg.sender == owner, "ds-auth"); 89 | stopped = false; 90 | emit Start(); 91 | } 92 | 93 | function setName(string memory name_) public { 94 | require(msg.sender == owner, "ds-auth"); 95 | name = name_; 96 | } 97 | 98 | function transferOwnership(address owner_) public { 99 | require(msg.sender == owner, "ds-auth"); 100 | owner = owner_; 101 | } 102 | }`; 103 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/solidity/index.ts: -------------------------------------------------------------------------------- 1 | import { ExampleMenuItem } from "../../components/ExampleDropdown"; 2 | import { EXAMPLE_SOLIDITY_CONTRACT_COUNTER } from "./counter"; 3 | import { EXAMPLE_SOLIDITY_CONTRACT_ERC20 } from "./erc20"; 4 | import { EXAMPLE_SOLIDITY_CONTRACT_VOTING } from "./voting"; 5 | 6 | export const EXAMPLE_SOLIDITY_CONTRACTS: ExampleMenuItem[] = [ 7 | { label: "Counter.sol", code: EXAMPLE_SOLIDITY_CONTRACT_COUNTER }, 8 | { label: "ERC20.sol", code: EXAMPLE_SOLIDITY_CONTRACT_ERC20 }, 9 | { label: "Voting.sol", code: EXAMPLE_SOLIDITY_CONTRACT_VOTING }, 10 | ]; 11 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/solidity/voting.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SOLIDITY_CONTRACT_VOTING = `pragma solidity ^0.8.24; 2 | 3 | contract Voting { 4 | uint64 public numVoters; 5 | mapping(bytes32 => uint256) public votes; 6 | mapping(address => mapping(bytes32 => bool)) public hasVoted; 7 | mapping(address => bool) public voters; 8 | 9 | constructor(address[] memory voters_) public { 10 | for (numVoters = 0; numVoters < voters_.length; numVoters++) { 11 | voters[voters_[numVoters]] = true; 12 | } 13 | } 14 | 15 | function vote(bytes32 topic) public returns (bool) { 16 | require(voters[msg.sender], "is-voter"); 17 | require(hasVoted[msg.sender][topic] == false, "has-voted"); 18 | 19 | votes[topic] += 1; 20 | hasVoted[msg.sender][topic] = true; 21 | 22 | return true; 23 | } 24 | }`; 25 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/sway/counter.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SWAY_CONTRACT_COUNTER = `contract; 2 | 3 | abi Counter { 4 | #[storage(read, write)] 5 | fn increment(amount: u64) -> u64; 6 | 7 | #[storage(read)] 8 | fn get() -> u64; 9 | } 10 | 11 | storage { 12 | counter: u64 = 0, 13 | } 14 | 15 | impl Counter for Contract { 16 | #[storage(read, write)] 17 | fn increment(amount: u64) -> u64 { 18 | let incremented = storage.counter.read() + amount; 19 | storage.counter.write(incremented); 20 | incremented 21 | } 22 | 23 | #[storage(read)] 24 | fn get() -> u64 { 25 | storage.counter.read() 26 | } 27 | }`; 28 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/sway/index.ts: -------------------------------------------------------------------------------- 1 | import { ExampleMenuItem } from "../../components/ExampleDropdown"; 2 | import { EXAMPLE_SWAY_CONTRACT_COUNTER } from "./counter"; 3 | import { EXAMPLE_SWAY_CONTRACT_LIQUIDITY_POOL } from "./liquiditypool"; 4 | import { EXAMPLE_SWAY_CONTRACT_SINGLEASSET } from "./singleasset"; 5 | import { EXAMPLE_SWAY_CONTRACT_MULTIASSET } from "./multiasset"; 6 | 7 | export const EXAMPLE_SWAY_CONTRACTS: ExampleMenuItem[] = [ 8 | { label: "Counter.sw", code: EXAMPLE_SWAY_CONTRACT_COUNTER }, 9 | { label: "LiquidityPool.sw", code: EXAMPLE_SWAY_CONTRACT_LIQUIDITY_POOL }, 10 | { label: "SingleAsset.sw", code: EXAMPLE_SWAY_CONTRACT_SINGLEASSET }, 11 | { label: "MultiAsset.sw", code: EXAMPLE_SWAY_CONTRACT_MULTIASSET }, 12 | ]; 13 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/sway/liquiditypool.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SWAY_CONTRACT_LIQUIDITY_POOL = `contract; 2 | 3 | use std::{ 4 | asset::{ 5 | mint_to, 6 | transfer, 7 | }, 8 | call_frames::msg_asset_id, 9 | constants::DEFAULT_SUB_ID, 10 | context::msg_amount 11 | }; 12 | 13 | abi LiquidityPool { 14 | fn deposit(recipient: Address); 15 | fn withdraw(recipient: Address); 16 | } 17 | 18 | const BASE_ASSET: AssetId = AssetId::from(0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c); 19 | 20 | impl LiquidityPool for Contract { 21 | fn deposit(recipient: Address) { 22 | assert(msg_asset_id() == BASE_ASSET); 23 | assert(msg_amount() > 0); 24 | 25 | // Mint two times the amount. 26 | let amount_to_mint = msg_amount() * 2; 27 | 28 | // Mint some LP assets based upon the amount of the base asset. 29 | mint_to(Identity::Address(recipient), DEFAULT_SUB_ID, amount_to_mint); 30 | } 31 | 32 | fn withdraw(recipient: Address) { 33 | let asset_id = AssetId::default(); 34 | assert(msg_asset_id() == asset_id); 35 | assert(msg_amount() > 0); 36 | 37 | // Amount to withdraw. 38 | let amount_to_transfer = msg_amount() / 2; 39 | 40 | // Transfer base asset to recipient. 41 | transfer(Identity::Address(recipient), BASE_ASSET, amount_to_transfer); 42 | } 43 | }`; 44 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/sway/multiasset.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SWAY_CONTRACT_MULTIASSET = `// ERC1155 equivalent in Sway. 2 | contract; 3 | 4 | use standards::src5::{AccessError, SRC5, State}; 5 | use standards::src20::{SetDecimalsEvent, SetNameEvent, SetSymbolEvent, SRC20, TotalSupplyEvent}; 6 | use standards::src3::SRC3; 7 | use std::{ 8 | asset::{ 9 | burn, 10 | mint_to, 11 | }, 12 | call_frames::msg_asset_id, 13 | context::this_balance, 14 | hash::{ 15 | Hash, 16 | }, 17 | storage::storage_string::*, 18 | string::String, 19 | }; 20 | 21 | storage { 22 | total_assets: u64 = 0, 23 | total_supply: StorageMap = StorageMap {}, 24 | name: StorageMap = StorageMap {}, 25 | symbol: StorageMap = StorageMap {}, 26 | decimals: StorageMap = StorageMap {}, 27 | owner: State = State::Uninitialized, 28 | } 29 | 30 | abi MultiAsset { 31 | #[storage(read, write)] 32 | fn constructor(owner_: Identity); 33 | 34 | #[storage(read, write)] 35 | fn set_name(asset: AssetId, name: String); 36 | 37 | #[storage(read, write)] 38 | fn set_symbol(asset: AssetId, symbol: String); 39 | 40 | #[storage(read, write)] 41 | fn set_decimals(asset: AssetId, decimals: u8); 42 | } 43 | 44 | impl MultiAsset for Contract { 45 | #[storage(read, write)] 46 | fn constructor(owner_: Identity) { 47 | require( 48 | storage 49 | .owner 50 | .read() == State::Uninitialized, 51 | "owner-initialized", 52 | ); 53 | storage.owner.write(State::Initialized(owner_)); 54 | } 55 | 56 | #[storage(read, write)] 57 | fn set_name(asset: AssetId, name: String) { 58 | require_access_owner(); 59 | storage.name.insert(asset, StorageString {}); 60 | storage.name.get(asset).write_slice(name); 61 | SetNameEvent::new(asset, Some(name), msg_sender().unwrap()).log(); 62 | } 63 | 64 | #[storage(read, write)] 65 | fn set_symbol(asset: AssetId, symbol: String) { 66 | require_access_owner(); 67 | storage.symbol.insert(asset, StorageString {}); 68 | storage.symbol.get(asset).write_slice(symbol); 69 | SetSymbolEvent::new(asset, Some(symbol), msg_sender().unwrap()).log(); 70 | } 71 | 72 | #[storage(read, write)] 73 | fn set_decimals(asset: AssetId, decimals: u8) { 74 | require_access_owner(); 75 | storage.decimals.insert(asset, decimals); 76 | SetDecimalsEvent::new(asset, decimals, msg_sender().unwrap()).log(); 77 | } 78 | } 79 | 80 | #[storage(read)] 81 | fn require_access_owner() { 82 | require( 83 | storage 84 | .owner 85 | .read() == State::Initialized(msg_sender().unwrap()), 86 | AccessError::NotOwner, 87 | ); 88 | } 89 | 90 | impl SRC20 for Contract { 91 | #[storage(read)] 92 | fn total_assets() -> u64 { 93 | storage.total_assets.read() 94 | } 95 | 96 | #[storage(read)] 97 | fn total_supply(asset: AssetId) -> Option { 98 | storage.total_supply.get(asset).try_read() 99 | } 100 | 101 | #[storage(read)] 102 | fn name(asset: AssetId) -> Option { 103 | storage.name.get(asset).read_slice() 104 | } 105 | 106 | #[storage(read)] 107 | fn symbol(asset: AssetId) -> Option { 108 | storage.symbol.get(asset).read_slice() 109 | } 110 | 111 | #[storage(read)] 112 | fn decimals(asset: AssetId) -> Option { 113 | storage.decimals.get(asset).try_read() 114 | } 115 | } 116 | 117 | impl SRC3 for Contract { 118 | #[storage(read, write)] 119 | fn mint(recipient: Identity, sub_id: Option, amount: u64) { 120 | require_access_owner(); 121 | let sub_id = match sub_id { 122 | Some(id) => id, 123 | None => SubId::zero(), 124 | }; 125 | let asset_id = AssetId::new(ContractId::this(), sub_id); 126 | let supply = storage.total_supply.get(asset_id).try_read(); 127 | if supply.is_none() { 128 | storage 129 | .total_assets 130 | .write(storage.total_assets.try_read().unwrap_or(0) + 1); 131 | } 132 | let current_supply = supply.unwrap_or(0); 133 | storage 134 | .total_supply 135 | .insert(asset_id, current_supply + amount); 136 | mint_to(recipient, sub_id, amount); 137 | TotalSupplyEvent::new(asset_id, current_supply + amount, msg_sender().unwrap()).log(); 138 | } 139 | 140 | #[payable] 141 | #[storage(read, write)] 142 | fn burn(sub_id: SubId, amount: u64) { 143 | require_access_owner(); 144 | let asset_id = AssetId::new(ContractId::this(), sub_id); 145 | require(this_balance(asset_id) >= amount, "not-enough-coins"); 146 | 147 | let supply = storage.total_supply.get(asset_id).try_read(); 148 | let current_supply = supply.unwrap_or(0); 149 | storage 150 | .total_supply 151 | .insert(asset_id, current_supply - amount); 152 | burn(sub_id, amount); 153 | TotalSupplyEvent::new(asset_id, current_supply - amount, msg_sender().unwrap()).log(); 154 | } 155 | } 156 | `; 157 | -------------------------------------------------------------------------------- /app/src/features/editor/examples/sway/singleasset.ts: -------------------------------------------------------------------------------- 1 | export const EXAMPLE_SWAY_CONTRACT_SINGLEASSET = `// ERC20 equivalent in Sway. 2 | contract; 3 | 4 | use standards::src3::SRC3; 5 | use standards::src5::{AccessError, SRC5, State}; 6 | use standards::src20::{SetDecimalsEvent, SetNameEvent, SetSymbolEvent, SRC20, TotalSupplyEvent}; 7 | use std::{ 8 | asset::{ 9 | burn, 10 | mint_to, 11 | }, 12 | call_frames::{ 13 | msg_asset_id, 14 | }, 15 | constants::DEFAULT_SUB_ID, 16 | context::msg_amount, 17 | string::String, 18 | }; 19 | 20 | abi SingleAsset { 21 | #[storage(read, write)] 22 | fn constructor(owner_: Identity); 23 | } 24 | 25 | configurable { 26 | DECIMALS: u8 = 9u8, 27 | NAME: str[7] = __to_str_array("MyAsset"), 28 | SYMBOL: str[5] = __to_str_array("MYTKN"), 29 | } 30 | 31 | storage { 32 | total_supply: u64 = 0, 33 | owner: State = State::Uninitialized, 34 | } 35 | 36 | impl SRC20 for Contract { 37 | #[storage(read)] 38 | fn total_assets() -> u64 { 39 | 1 40 | } 41 | 42 | #[storage(read)] 43 | fn total_supply(asset: AssetId) -> Option { 44 | if asset == AssetId::default() { 45 | Some(storage.total_supply.read()) 46 | } else { 47 | None 48 | } 49 | } 50 | 51 | #[storage(read)] 52 | fn name(asset: AssetId) -> Option { 53 | if asset == AssetId::default() { 54 | Some(String::from_ascii_str(from_str_array(NAME))) 55 | } else { 56 | None 57 | } 58 | } 59 | 60 | #[storage(read)] 61 | fn symbol(asset: AssetId) -> Option { 62 | if asset == AssetId::default() { 63 | Some(String::from_ascii_str(from_str_array(SYMBOL))) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | #[storage(read)] 70 | fn decimals(asset: AssetId) -> Option { 71 | if asset == AssetId::default() { 72 | Some(DECIMALS) 73 | } else { 74 | None 75 | } 76 | } 77 | } 78 | 79 | #[storage(read)] 80 | fn require_access_owner() { 81 | require( 82 | storage 83 | .owner 84 | .read() == State::Initialized(msg_sender().unwrap()), 85 | AccessError::NotOwner, 86 | ); 87 | } 88 | 89 | impl SingleAsset for Contract { 90 | #[storage(read, write)] 91 | fn constructor(owner_: Identity) { 92 | require( 93 | storage 94 | .owner 95 | .read() == State::Uninitialized, 96 | "owner-initialized", 97 | ); 98 | storage.owner.write(State::Initialized(owner_)); 99 | } 100 | } 101 | 102 | impl SRC5 for Contract { 103 | #[storage(read)] 104 | fn owner() -> State { 105 | storage.owner.read() 106 | } 107 | } 108 | 109 | impl SRC3 for Contract { 110 | #[storage(read, write)] 111 | fn mint(recipient: Identity, sub_id: Option, amount: u64) { 112 | require(sub_id.is_some() && sub_id.unwrap() == DEFAULT_SUB_ID, "incorrect-sub-id"); 113 | require_access_owner(); 114 | 115 | let current_supply = storage.total_supply.read(); 116 | storage 117 | .total_supply 118 | .write(current_supply + amount); 119 | mint_to(recipient, DEFAULT_SUB_ID, amount); 120 | TotalSupplyEvent::new(AssetId::default(), current_supply + amount, msg_sender().unwrap()).log(); 121 | } 122 | 123 | #[payable] 124 | #[storage(read, write)] 125 | fn burn(sub_id: SubId, amount: u64) { 126 | require(sub_id == DEFAULT_SUB_ID, "incorrect-sub-id"); 127 | require(msg_amount() >= amount, "incorrect-amount-provided"); 128 | require( 129 | msg_asset_id() == AssetId::default(), 130 | "incorrect-asset-provided", 131 | ); 132 | require_access_owner(); 133 | 134 | let current_supply = storage.total_supply.read(); 135 | storage 136 | .total_supply 137 | .write(current_supply - amount); 138 | burn(DEFAULT_SUB_ID, amount); 139 | TotalSupplyEvent::new(AssetId::default(), current_supply - amount, msg_sender().unwrap()).log(); 140 | } 141 | } 142 | 143 | abi EmitSRC20Events { 144 | fn emit_src20_events(); 145 | } 146 | 147 | impl EmitSRC20Events for Contract { 148 | fn emit_src20_events() { 149 | // Metadata that is stored as a configurable should only be emitted once. 150 | let asset = AssetId::default(); 151 | let sender = msg_sender().unwrap(); 152 | let name = Some(String::from_ascii_str(from_str_array(NAME))); 153 | let symbol = Some(String::from_ascii_str(from_str_array(SYMBOL))); 154 | 155 | SetNameEvent::new(asset, name, sender).log(); 156 | SetSymbolEvent::new(asset, symbol, sender).log(); 157 | SetDecimalsEvent::new(asset, DECIMALS, sender).log(); 158 | } 159 | } 160 | `; 161 | -------------------------------------------------------------------------------- /app/src/features/editor/hooks/useCompile.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import ansicolor from "ansicolor"; 3 | import React, { useState, useEffect } from "react"; 4 | import { 5 | saveAbi, 6 | saveBytecode, 7 | saveStorageSlots, 8 | } from "../../../utils/localStorage"; 9 | import { CopyableHex } from "../../../components/shared"; 10 | import { Toolchain } from "../components/ToolchainDropdown"; 11 | import { SERVER_URI } from "../../../constants"; 12 | import { track } from "@vercel/analytics/react"; 13 | 14 | function toResults( 15 | prefixedBytecode: string, 16 | abi: string, 17 | ): React.ReactElement[] { 18 | return [ 19 |
20 | Bytecode:
21 | 22 |
23 |
24 |
, 25 |
26 | ABI: 27 |
28 | {abi} 29 |
, 30 | ]; 31 | } 32 | 33 | export function useCompile( 34 | code: string | undefined, 35 | onError: (error: string | undefined) => void, 36 | setIsCompiled: (isCompiled: boolean) => void, 37 | setResults: (entry: React.ReactElement[]) => void, 38 | toolchain: Toolchain, 39 | ) { 40 | const [serverError, setServerError] = useState(false); 41 | const [version, setVersion] = useState(); 42 | 43 | useEffect(() => { 44 | if (!code) { 45 | setResults([<>Click 'Compile' to build your code.]); 46 | return; 47 | } 48 | if (!code?.length) { 49 | setResults([<>Add some code to compile.]); 50 | return; 51 | } 52 | setResults([<>Compiling Sway contract...]); 53 | 54 | const request = new Request(`${SERVER_URI}/compile`, { 55 | method: "POST", 56 | body: JSON.stringify({ 57 | contract: code, 58 | toolchain, 59 | }), 60 | }); 61 | 62 | fetch(request) 63 | .then((response) => { 64 | if (response.status < 400) { 65 | return response.json(); 66 | } else { 67 | track("Compile Error", { 68 | source: "network", 69 | status: response.status, 70 | }); 71 | setServerError(true); 72 | } 73 | }) 74 | .then((response) => { 75 | const { error, forcVersion } = response; 76 | if (error) { 77 | // Preserve the ANSI color codes from the compiler output. 78 | const parsedAnsi = ansicolor.parse(error); 79 | const results = parsedAnsi.spans.map((span, i) => { 80 | const { text, css } = span; 81 | const Span = styled.span` 82 | ${css} 83 | `; 84 | return {text}; 85 | }); 86 | setResults(results); 87 | setVersion(forcVersion); 88 | saveAbi(""); 89 | saveBytecode(""); 90 | saveStorageSlots(""); 91 | } else { 92 | const { abi, bytecode, storageSlots, forcVersion } = response; 93 | const prefixedBytecode = `0x${bytecode}`; 94 | saveAbi(abi); 95 | saveBytecode(prefixedBytecode); 96 | saveStorageSlots(storageSlots); 97 | setResults(toResults(prefixedBytecode, abi)); 98 | setVersion(forcVersion); 99 | } 100 | }) 101 | .catch(() => { 102 | track("Compile Error", { source: "network" }); 103 | setServerError(true); 104 | }); 105 | setIsCompiled(true); 106 | }, [code, setIsCompiled, setResults, toolchain]); 107 | 108 | useEffect(() => { 109 | if (serverError) { 110 | onError( 111 | "There was an unexpected error compiling your contract. Please try again.", 112 | ); 113 | } 114 | }, [serverError, onError]); 115 | 116 | useEffect(() => { 117 | if (version) { 118 | setResults([
Compiled with {version}
]); 119 | setVersion(undefined); 120 | } 121 | }, [setResults, version]); 122 | } 123 | -------------------------------------------------------------------------------- /app/src/features/editor/hooks/useGist.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import { SERVER_URI } from "../../../constants"; 3 | import { track } from "@vercel/analytics/react"; 4 | import { EditorLanguage } from "../components/ActionOverlay"; 5 | import { useSearchParams } from "react-router-dom"; 6 | 7 | interface GistMeta { 8 | url: string; 9 | id: string; 10 | } 11 | 12 | interface ContractCode { 13 | contract: string; 14 | language: EditorLanguage; 15 | } 16 | 17 | interface GistResponse { 18 | gist: GistMeta; 19 | swayContract: string; 20 | transpileContract: ContractCode; 21 | error?: string; 22 | } 23 | 24 | export function useGist( 25 | onSwayCodeChange: (swayCode: string) => void, 26 | onSolidityCodeChange: (solidityCode: string) => void, 27 | ): { 28 | newGist: ( 29 | swayContract: string, 30 | transpileContract: ContractCode, 31 | ) => Promise; 32 | } { 33 | // The search parameters for the current URL. 34 | const [searchParams] = useSearchParams(); 35 | const [gist, setGist] = useState(null); 36 | 37 | useEffect(() => { 38 | const gist_id = searchParams.get("gist"); 39 | if (gist_id) { 40 | const request = new Request(`${SERVER_URI}/gist/${gist_id}`, { 41 | method: "GET", 42 | }); 43 | 44 | fetch(request) 45 | .then((response) => { 46 | if (response.status < 400) { 47 | return response.json(); 48 | } else { 49 | track("Get Gist Error", { 50 | source: "network", 51 | status: response.status, 52 | }); 53 | } 54 | }) 55 | .then((response: GistResponse) => { 56 | const { error } = response; 57 | if (error) { 58 | track("Get Gist Error", { source: "server" }); 59 | } else { 60 | setGist(response); 61 | } 62 | }) 63 | .catch(() => { 64 | track("Get Gist Error", { source: "network" }); 65 | }); 66 | } 67 | }, [searchParams, setGist]); 68 | 69 | // Update the editor code when the gist is loaded. 70 | useEffect(() => { 71 | if (gist) { 72 | onSwayCodeChange(gist.swayContract); 73 | onSolidityCodeChange(gist.transpileContract.contract); 74 | } 75 | }, [gist, onSwayCodeChange, onSolidityCodeChange]); 76 | 77 | const newGist = useCallback( 78 | async (sway_contract: string, transpile_contract: ContractCode) => { 79 | const request = new Request(`${SERVER_URI}/gist`, { 80 | method: "POST", 81 | body: JSON.stringify({ 82 | sway_contract, 83 | transpile_contract, 84 | }), 85 | }); 86 | 87 | const res = await fetch(request) 88 | .then((response) => { 89 | if (response.status < 400) { 90 | return response.json(); 91 | } else { 92 | track("New Gist Error", { 93 | source: "network", 94 | status: response.status, 95 | }); 96 | } 97 | }) 98 | .then((response: { gist: GistMeta; error: string | undefined }) => { 99 | const { error, gist } = response; 100 | if (error) { 101 | track("New Gist Error", { source: "server" }); 102 | } else { 103 | return gist; 104 | } 105 | }) 106 | .catch(() => { 107 | track("New Gist Error", { source: "network" }); 108 | }); 109 | 110 | return res ?? undefined; 111 | }, 112 | [], 113 | ); 114 | 115 | return { newGist }; 116 | } 117 | -------------------------------------------------------------------------------- /app/src/features/editor/hooks/useLog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import { Divider } from "@mui/material"; 3 | import useTheme from "../../../context/theme"; 4 | 5 | const RESULT_LINE_LIMIT = 500; 6 | 7 | export function useLog(): [ 8 | React.ReactElement[], 9 | (entry?: string | React.ReactElement[]) => void, 10 | ] { 11 | // The complete results to show in the compilation output section. 12 | const [results, setResults] = useState([]); 13 | 14 | // The most recent results to add to the compilation output section. 15 | const [resultsToAdd, setResultsToAdd] = useState(); 16 | const resultsToAddRef = React.useRef(); 17 | 18 | const { themeColor } = useTheme(); 19 | 20 | const updateLog = useCallback( 21 | (entry?: string | React.ReactElement[]) => { 22 | if (entry) { 23 | setResultsToAdd( 24 | typeof entry === "string" ? [
{entry}
] : entry, 25 | ); 26 | } 27 | }, 28 | [setResultsToAdd], 29 | ); 30 | 31 | // Update the results to show only if there are new results to add. 32 | useEffect(() => { 33 | if (resultsToAdd && resultsToAdd !== resultsToAddRef.current) { 34 | resultsToAddRef.current = resultsToAdd; 35 | const newResults = [...results]; 36 | if (newResults.length > 0) { 37 | newResults.push( 38 | 41 | {new Date().toLocaleString()} 42 | , 43 | ); 44 | } 45 | newResults.push(...resultsToAdd); 46 | if (newResults.length > RESULT_LINE_LIMIT) { 47 | setResults(newResults.slice(newResults.length - RESULT_LINE_LIMIT)); 48 | } else { 49 | setResults(newResults); 50 | } 51 | } 52 | }, [results, resultsToAdd, resultsToAddRef, setResults, themeColor]); 53 | 54 | return [results, updateLog]; 55 | } 56 | -------------------------------------------------------------------------------- /app/src/features/editor/hooks/useTranspile.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import ansicolor from "ansicolor"; 3 | import React, { useState, useEffect } from "react"; 4 | import { SERVER_URI } from "../../../constants"; 5 | import { track } from "@vercel/analytics/react"; 6 | 7 | export function useTranspile( 8 | code: string | undefined, 9 | setCodeToCompile: (code: string | undefined) => void, 10 | onSwayCodeChange: (code: string) => void, 11 | onError: (error: string | undefined) => void, 12 | setResults: (entry: React.ReactElement[]) => void, 13 | ) { 14 | const [serverError, setServerError] = useState(false); 15 | 16 | useEffect(() => { 17 | if (!code) { 18 | return; 19 | } 20 | setResults([ 21 | <> 22 | Transpiling Solidity code with{" "} 23 | charcoal... 24 | , 25 | <> 26 | WARNING: no support for delegatecall, this, ASM, strings. Coming soon in 27 | future releases. 28 | , 29 | ]); 30 | 31 | const request = new Request(`${SERVER_URI}/transpile`, { 32 | method: "POST", 33 | body: JSON.stringify({ 34 | contract: code, 35 | language: "solidity", 36 | }), 37 | }); 38 | 39 | fetch(request) 40 | .then((response) => { 41 | if (response.status < 400) { 42 | return response.json(); 43 | } else { 44 | track("Transpile Error", { 45 | source: "network", 46 | status: response.status, 47 | }); 48 | setServerError(true); 49 | } 50 | }) 51 | .then((response) => { 52 | const { error, swayContract } = response; 53 | if (error) { 54 | // Preserve the ANSI color codes from the compiler output. 55 | const parsedAnsi = ansicolor.parse(error); 56 | const results = parsedAnsi.spans.map((span, i) => { 57 | const { text, css } = span; 58 | const Span = styled.span` 59 | ${css} 60 | `; 61 | return {text}; 62 | }); 63 | setResults(results); 64 | } else { 65 | // Tell the useCompile hook to start compiling. 66 | onSwayCodeChange(swayContract); 67 | setCodeToCompile(swayContract); 68 | setResults([<>Successfully transpiled Solidity contract to Sway.]); 69 | } 70 | }) 71 | .catch(() => { 72 | track("Transpile Error", { source: "network" }); 73 | setServerError(true); 74 | }); 75 | }, [code, setResults, onSwayCodeChange, setCodeToCompile]); 76 | 77 | useEffect(() => { 78 | if (serverError) { 79 | onError( 80 | "There was an unexpected error transpiling your contract. Please try again.", 81 | ); 82 | } 83 | }, [serverError, onError]); 84 | } 85 | -------------------------------------------------------------------------------- /app/src/features/interact/components/CallButton.tsx: -------------------------------------------------------------------------------- 1 | import { useCallFunction } from "../hooks/useCallFunction"; 2 | import { CallType } from "../../../utils/types"; 3 | import { CallableParamValue } from "./FunctionParameters"; 4 | import SecondaryButton from "../../../components/SecondaryButton"; 5 | import { track } from "@vercel/analytics/react"; 6 | import { useCallback } from "react"; 7 | 8 | interface CallButtonProps { 9 | contractId: string; 10 | functionName: string; 11 | parameters: CallableParamValue[]; 12 | callType: CallType; 13 | setResponse: (response: string | Error) => void; 14 | updateLog: (entry: string) => void; 15 | } 16 | 17 | export function CallButton({ 18 | contractId, 19 | functionName, 20 | parameters, 21 | callType, 22 | setResponse, 23 | updateLog, 24 | }: CallButtonProps) { 25 | const functionMutation = useCallFunction({ 26 | contractId, 27 | functionName, 28 | parameters, 29 | callType, 30 | setResponse, 31 | updateLog, 32 | }); 33 | 34 | const onFunctionClick = useCallback(() => { 35 | track("Function Call Click", { callType }); 36 | setResponse(""); 37 | functionMutation.mutate(); 38 | }, [callType, functionMutation, setResponse]); 39 | 40 | return ( 41 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/src/features/interact/components/ComplexParameterInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react"; 2 | import AceEditor from "react-ace"; 3 | import "ace-builds/webpack-resolver"; 4 | import "ace-builds/src-noconflict/mode-json"; 5 | import "ace-builds/src-noconflict/theme-chrome"; 6 | import "ace-builds/src-noconflict/theme-tomorrow_night_bright"; 7 | import useTheme from "../../../context/theme"; 8 | import { StyledBorder } from "../../../components/shared"; 9 | import { 10 | CallableParamValue, 11 | InputInstance, 12 | ObjectParamValue, 13 | VectorParamValue, 14 | } from "./FunctionParameters"; 15 | 16 | export interface ComplexParameterInputProps { 17 | value: string; 18 | input: InputInstance; 19 | onChange: (value: string) => void; 20 | } 21 | 22 | function ComplexParameterInput({ 23 | value, 24 | input, 25 | onChange, 26 | }: ComplexParameterInputProps) { 27 | // Construct the default object based to show in the JSON editor. 28 | const defaultObjectOrVector = useMemo(() => { 29 | const getDefaultObject = (input: InputInstance): ObjectParamValue => { 30 | return input.components 31 | ? Object.fromEntries( 32 | input.components.map((nested: InputInstance) => { 33 | return [nested.name, getDefaultValue(nested)]; 34 | }), 35 | ) 36 | : {}; 37 | }; 38 | 39 | const getDefaultVector = (input: InputInstance): VectorParamValue => { 40 | return input.components?.map(getDefaultValue) ?? []; 41 | }; 42 | 43 | const getDefaultValue = (input: InputInstance): CallableParamValue => { 44 | switch (input.type.literal) { 45 | case "string": 46 | return ""; 47 | case "number": 48 | return 0; 49 | case "bool": 50 | return false; 51 | case "enum": 52 | case "option": 53 | case "object": 54 | return getDefaultObject(input); 55 | case "vector": 56 | return getDefaultVector(input); 57 | } 58 | }; 59 | 60 | return getDefaultValue(input); 61 | }, [input]); 62 | 63 | useEffect(() => { 64 | const defaultValue = JSON.stringify(defaultObjectOrVector, null, 4); 65 | if (!value) { 66 | onChange(defaultValue); 67 | } 68 | }, [defaultObjectOrVector, onChange, value]); 69 | 70 | const lines = useMemo( 71 | () => (value ? value.split("\n").length + 1 : 2), 72 | [value], 73 | ); 74 | 75 | const { editorTheme, themeColor } = useTheme(); 76 | 77 | return ( 78 | 79 | 91 | 92 | ); 93 | } 94 | 95 | export default ComplexParameterInput; 96 | -------------------------------------------------------------------------------- /app/src/features/interact/components/ContractInterface.tsx: -------------------------------------------------------------------------------- 1 | import { useContractFunctions } from "../hooks/useContractFunctions"; 2 | import { FunctionInterface } from "./FunctionInterface"; 3 | import { AbiHelper } from "../utils/abi"; 4 | import { useMemo, useState } from "react"; 5 | import { CopyableHex } from "../../../components/shared"; 6 | import { FunctionFragment } from "fuels"; 7 | 8 | const FUNCTION_COUNT_LIMIT = 1000; 9 | interface ContractInterfaceProps { 10 | contractId: string; 11 | updateLog: (entry: string) => void; 12 | } 13 | 14 | export function ContractInterface({ 15 | contractId, 16 | updateLog, 17 | }: ContractInterfaceProps) { 18 | // Key: contract.id + functionName 19 | // Value: API response 20 | const [responses, setResponses] = useState>( 21 | {}, 22 | ); 23 | 24 | const { contract, functionNames } = useContractFunctions(contractId); 25 | 26 | function isType(item: T | undefined): item is T { 27 | return !!item; 28 | } 29 | 30 | const functionFragments: FunctionFragment[] = functionNames 31 | .slice(0, FUNCTION_COUNT_LIMIT) 32 | .map((functionName) => contract?.interface.functions[functionName]) 33 | .filter(isType); 34 | 35 | const abiHelper = useMemo(() => { 36 | return new AbiHelper(contract?.interface?.jsonAbi); 37 | }, [contract]); 38 | 39 | const functionInterfaces = useMemo( 40 | () => 41 | functionFragments.map((functionFragment, index) => ( 42 |
43 | 50 | setResponses({ 51 | ...responses, 52 | [contractId + functionFragment.name]: response, 53 | }) 54 | } 55 | updateLog={updateLog} 56 | /> 57 |
58 | )), 59 | [contractId, functionFragments, responses, abiHelper, updateLog], 60 | ); 61 | 62 | return ( 63 |
64 |
65 |
72 | Contract Interface 73 |
74 | 75 |
76 | 77 | {functionInterfaces} 78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /app/src/features/interact/components/DryrunSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FormControlLabel from "@mui/material/FormControlLabel"; 3 | import Switch from "@mui/material/Switch"; 4 | import useTheme from "../../../context/theme"; 5 | 6 | export interface DryrunSwitchProps { 7 | dryrun: boolean; 8 | onChange: () => void; 9 | } 10 | 11 | function DryrunSwitch({ dryrun, onChange }: DryrunSwitchProps) { 12 | const { themeColor } = useTheme(); 13 | 14 | return ( 15 | 27 | {dryrun ? "DRY RUN" : "LIVE"} 28 | 29 | } 30 | control={} 31 | /> 32 | ); 33 | } 34 | 35 | export default DryrunSwitch; 36 | -------------------------------------------------------------------------------- /app/src/features/interact/components/FunctionCallAccordion.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Accordion from "@mui/material/Accordion"; 3 | import AccordionSummary from "@mui/material/AccordionSummary"; 4 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; 5 | import AccordionDetails from "@mui/material/AccordionDetails"; 6 | import FormLabel from "@mui/material/FormLabel"; 7 | import { InputInstance, ParamTypeLiteral } from "./FunctionParameters"; 8 | import { FunctionForm } from "./FunctionForm"; 9 | import { ResponseCard } from "./ResponseCard"; 10 | import useTheme from "../../../context/theme"; 11 | import { darkColors } from "@fuel-ui/css"; 12 | import styled from "@emotion/styled"; 13 | 14 | export interface FunctionCallAccordionProps { 15 | contractId: string; 16 | functionName: string; 17 | inputInstances: InputInstance[]; 18 | outputType?: ParamTypeLiteral; 19 | response?: string | Error; 20 | setResponse: (response: string | Error) => void; 21 | updateLog: (entry: string) => void; 22 | } 23 | 24 | const StyledAccordion = styled(Accordion)<{ theme: string }>` 25 | ${(props) => 26 | props.theme === "dark" 27 | ? ` 28 | background: ${darkColors.scalesGray1}; 29 | border: 1px solid ${darkColors.gray6}; 30 | ` 31 | : ""}; 32 | `; 33 | 34 | export function FunctionCallAccordion({ 35 | contractId, 36 | functionName, 37 | inputInstances, 38 | outputType, 39 | response, 40 | setResponse, 41 | updateLog, 42 | }: FunctionCallAccordionProps) { 43 | const { theme, themeColor } = useTheme(); 44 | 45 | return ( 46 | 47 | } 49 | > 50 | 53 | {functionName} 54 | 55 | 56 | 57 | 64 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/src/features/interact/components/FunctionForm.tsx: -------------------------------------------------------------------------------- 1 | import FunctionToolbar from "./FunctionToolbar"; 2 | import { 3 | CallableParamValue, 4 | FunctionParameters, 5 | InputInstance, 6 | SimpleParamValue, 7 | } from "./FunctionParameters"; 8 | import React, { useMemo, useState } from "react"; 9 | 10 | interface FunctionFormProps { 11 | contractId: string; 12 | functionName: string; 13 | inputInstances: InputInstance[]; 14 | setResponse: (response: string | Error) => void; 15 | updateLog: (entry: string) => void; 16 | } 17 | 18 | export function FunctionForm({ 19 | contractId, 20 | setResponse, 21 | functionName, 22 | inputInstances, 23 | updateLog, 24 | }: FunctionFormProps) { 25 | const [paramValues, setParamValues] = useState( 26 | Array(inputInstances.length), 27 | ); 28 | 29 | // Parse complex parameters stored as strings. 30 | const transformedParams: CallableParamValue[] = useMemo(() => { 31 | return paramValues.map((paramValue, index) => { 32 | const input = inputInstances[index]; 33 | const literal = input.type.literal; 34 | if ( 35 | typeof paramValue === "string" && 36 | ["vector", "object", "option", "enum"].includes(literal) 37 | ) { 38 | try { 39 | const parsed = JSON.parse(paramValue); 40 | 41 | // For Options, SDK expects to receive the value of "Some" or undefined for "None". 42 | if (literal === "option") { 43 | return parsed["Some"]; 44 | } 45 | return parsed; 46 | } catch (e) { 47 | // We shouldn't get here, but if we do, the server will return an error. 48 | } 49 | } 50 | return paramValue; 51 | }); 52 | }, [inputInstances, paramValues]); 53 | 54 | return ( 55 |
56 | 63 | 64 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/src/features/interact/components/FunctionInterface.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionFragment } from "fuels"; 2 | import { useCallback, useMemo } from "react"; 3 | import { InputInstance } from "./FunctionParameters"; 4 | import { FunctionCallAccordion } from "./FunctionCallAccordion"; 5 | import { getTypeInfo } from "../utils/getTypeInfo"; 6 | import React from "react"; 7 | import { AbiHelper } from "../utils/abi"; 8 | 9 | export interface FunctionInterfaceProps { 10 | contractId: string; 11 | abiHelper: AbiHelper; 12 | functionFragment: FunctionFragment | undefined; 13 | functionName: string; 14 | response?: string | Error; 15 | setResponse: (response: string | Error) => void; 16 | updateLog: (entry: string) => void; 17 | } 18 | 19 | export function FunctionInterface({ 20 | contractId, 21 | abiHelper, 22 | functionFragment, 23 | functionName, 24 | response, 25 | setResponse, 26 | updateLog, 27 | }: FunctionInterfaceProps) { 28 | const toInputInstance = useCallback( 29 | (typeId: string | number, name: string): InputInstance => { 30 | const { concreteType, metadataType } = abiHelper.getTypesById(typeId); 31 | const typeInfo = getTypeInfo(concreteType, abiHelper); 32 | 33 | switch (typeInfo.literal) { 34 | case "vector": 35 | return { 36 | name, 37 | type: typeInfo, 38 | components: [ 39 | toInputInstance(concreteType?.typeArguments?.at(0) ?? "", ""), 40 | ], 41 | }; 42 | case "object": 43 | return { 44 | name, 45 | type: typeInfo, 46 | components: metadataType?.components?.map((c) => 47 | toInputInstance(c.typeId, c.name), 48 | ), 49 | }; 50 | case "option": 51 | case "enum": 52 | return { 53 | name, 54 | type: typeInfo, 55 | components: [ 56 | toInputInstance( 57 | metadataType?.components?.at(0)?.typeId ?? "", 58 | metadataType?.components?.at(0)?.name ?? "", 59 | ), 60 | ], 61 | }; 62 | default: 63 | return { 64 | name, 65 | type: typeInfo, 66 | }; 67 | } 68 | }, 69 | [abiHelper], 70 | ); 71 | 72 | const outputType = useMemo(() => { 73 | const outputTypeId = functionFragment?.jsonFn?.output; 74 | if (outputTypeId !== undefined) { 75 | const sdkType = abiHelper.getConcreteTypeById(outputTypeId); 76 | return sdkType ? getTypeInfo(sdkType, abiHelper).literal : undefined; 77 | } 78 | }, [functionFragment?.jsonFn?.output, abiHelper]); 79 | 80 | const inputInstances: InputInstance[] = useMemo( 81 | () => 82 | functionFragment?.jsonFn?.inputs.map((input) => 83 | toInputInstance(input.concreteTypeId, input.name), 84 | ) ?? [], 85 | [functionFragment?.jsonFn?.inputs, toInputInstance], 86 | ); 87 | 88 | return ( 89 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /app/src/features/interact/components/FunctionParameters.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import TableContainer from "@mui/material/TableContainer"; 3 | import TableHead from "@mui/material/TableHead"; 4 | import Table from "@mui/material/Table"; 5 | import Paper from "@mui/material/Paper"; 6 | import TableRow from "@mui/material/TableRow"; 7 | import TableCell from "@mui/material/TableCell"; 8 | import TableBody from "@mui/material/TableBody"; 9 | import ParameterInput from "./ParameterInput"; 10 | import { TypeInfo } from "../utils/getTypeInfo"; 11 | 12 | export type ParamTypeLiteral = 13 | | "number" 14 | | "bool" 15 | | "string" 16 | | "object" 17 | | "option" 18 | | "enum" 19 | | "vector"; 20 | export type SimpleParamValue = number | boolean | string; 21 | export type ObjectParamValue = Record< 22 | string, 23 | SimpleParamValue | Record | VectorParamValue 24 | >; 25 | export type VectorParamValue = Array; 26 | export type CallableParamValue = 27 | | SimpleParamValue 28 | | ObjectParamValue 29 | | VectorParamValue; 30 | 31 | export interface InputInstance { 32 | name: string; 33 | type: TypeInfo; 34 | components?: InputInstance[]; 35 | } 36 | 37 | interface FunctionParametersProps { 38 | inputInstances: InputInstance[]; 39 | functionName: string; 40 | paramValues: SimpleParamValue[]; 41 | setParamValues: (values: SimpleParamValue[]) => void; 42 | } 43 | 44 | export function FunctionParameters({ 45 | inputInstances, 46 | functionName, 47 | paramValues, 48 | setParamValues, 49 | }: FunctionParametersProps) { 50 | const setParamAtIndex = React.useCallback( 51 | (index: number, value: SimpleParamValue) => { 52 | const newParamValues = [...paramValues]; 53 | newParamValues[index] = value; 54 | setParamValues(newParamValues); 55 | }, 56 | [paramValues, setParamValues], 57 | ); 58 | 59 | if (!inputInstances.length) { 60 | return ; 61 | } 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | Name 69 | Type 70 | Value 71 | 72 | 73 | 74 | {inputInstances.map((input, index) => ( 75 | 79 | 80 | {input.name} 81 | 82 | {input.type.swayType} 83 | 84 | 88 | setParamAtIndex(index, value) 89 | } 90 | /> 91 | 92 | 93 | ))} 94 | 95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /app/src/features/interact/components/FunctionToolbar.tsx: -------------------------------------------------------------------------------- 1 | import { CallButton } from "./CallButton"; 2 | import FormGroup from "@mui/material/FormGroup"; 3 | import React from "react"; 4 | import Toolbar from "@mui/material/Toolbar"; 5 | import DryrunSwitch from "./DryrunSwitch"; 6 | import { CallableParamValue } from "./FunctionParameters"; 7 | import useTheme from "../../../context/theme"; 8 | import { useContract } from "../hooks/useContract"; 9 | 10 | interface FunctionToolbarProps { 11 | contractId: string; 12 | functionName: string; 13 | parameters: CallableParamValue[]; 14 | setResponse: (response: string | Error) => void; 15 | updateLog: (entry: string) => void; 16 | } 17 | 18 | function FunctionToolbar({ 19 | contractId, 20 | functionName, 21 | parameters, 22 | setResponse, 23 | updateLog, 24 | }: FunctionToolbarProps) { 25 | const [dryrun, setDryrun] = React.useState(true); 26 | const { contract } = useContract(contractId); 27 | const { themeColor } = useTheme(); 28 | 29 | const title = parameters.length ? "Parameters" : "No Parameters"; 30 | 31 | const isReadOnly = !!contract?.functions[functionName]?.isReadOnly(); 32 | 33 | return ( 34 | 35 |
{title}
36 | 44 | {!isReadOnly && ( 45 | setDryrun(!dryrun)} /> 46 | )} 47 |
48 | 56 |
57 |
58 |
59 | ); 60 | } 61 | 62 | export default FunctionToolbar; 63 | -------------------------------------------------------------------------------- /app/src/features/interact/components/InteractionDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Drawer from "@mui/material/Drawer"; 3 | import { ContractInterface } from "./ContractInterface"; 4 | import useTheme from "../../../context/theme"; 5 | 6 | export interface InteractionDrawerProps { 7 | isOpen: boolean; 8 | width: string; 9 | contractId: string; 10 | updateLog: (entry: string) => void; 11 | } 12 | 13 | function InteractionDrawer({ 14 | isOpen, 15 | width, 16 | contractId, 17 | updateLog, 18 | }: InteractionDrawerProps) { 19 | const { themeColor } = useTheme(); 20 | return ( 21 | 39 |
44 | {isOpen && ( 45 | 46 | )} 47 |
48 |
49 | ); 50 | } 51 | 52 | export default InteractionDrawer; 53 | -------------------------------------------------------------------------------- /app/src/features/interact/components/ParameterInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { InputInstance, SimpleParamValue } from "./FunctionParameters"; 3 | import TextField from "@mui/material/TextField"; 4 | import ToggleButton from "@mui/material/ToggleButton"; 5 | import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; 6 | import ComplexParameterInput from "./ComplexParameterInput"; 7 | 8 | export interface ParameterInputProps { 9 | input: InputInstance; 10 | value: SimpleParamValue; 11 | onChange: (value: SimpleParamValue) => void; 12 | } 13 | 14 | function ParameterInput({ input, value, onChange }: ParameterInputProps) { 15 | switch (input.type.literal) { 16 | case "string": 17 | return ( 18 | ) => { 21 | onChange(event.target.value); 22 | }} 23 | /> 24 | ); 25 | case "number": 26 | return ( 27 | ) => { 31 | onChange(Number.parseFloat(event.target.value)); 32 | }} 33 | /> 34 | ); 35 | case "bool": 36 | return ( 37 | onChange(!value)} 43 | > 44 | true 45 | false 46 | 47 | ); 48 | case "vector": 49 | case "enum": 50 | case "option": 51 | case "object": 52 | return ( 53 | 58 | ); 59 | } 60 | } 61 | 62 | export default ParameterInput; 63 | -------------------------------------------------------------------------------- /app/src/features/interact/components/ResponseCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import Card from "@mui/material/Card"; 3 | import CardContent from "@mui/material/CardContent"; 4 | import { ParamTypeLiteral } from "./FunctionParameters"; 5 | import useTheme from "../../../context/theme"; 6 | 7 | interface ResponseCardProps { 8 | response?: string | Error; 9 | outputType?: ParamTypeLiteral; 10 | style?: React.CSSProperties; 11 | } 12 | 13 | export function ResponseCard({ 14 | response, 15 | outputType, 16 | style, 17 | }: ResponseCardProps) { 18 | const formattedResponse = useMemo(() => { 19 | if (!response) return "The response will appear here."; 20 | if (response instanceof Error) return response.toString(); 21 | if (!response.length) return "Waiting for reponse..."; 22 | 23 | switch (outputType) { 24 | case "number": { 25 | return Number(JSON.parse(response)); 26 | } 27 | default: { 28 | return response; 29 | } 30 | } 31 | }, [outputType, response]); 32 | 33 | const { themeColor } = useTheme(); 34 | 35 | return ( 36 | 44 | 54 | {
{formattedResponse}
} 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/src/features/interact/hooks/useCallFunction.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import { useContract } from "./useContract"; 3 | import { modifyJsonStringify } from "../utils/modifyJsonStringify"; 4 | import { CallType } from "../../../utils/types"; 5 | import { CallableParamValue } from "../components/FunctionParameters"; 6 | import { Contract, DryRunResult, FunctionResult } from "fuels"; 7 | 8 | interface CallFunctionProps { 9 | parameters: CallableParamValue[]; 10 | contractId: string; 11 | functionName: string; 12 | callType: CallType; 13 | setResponse: (response: string | Error) => void; 14 | updateLog: (entry: string) => void; 15 | } 16 | 17 | export function useCallFunction({ 18 | parameters, 19 | contractId, 20 | functionName, 21 | callType, 22 | setResponse, 23 | updateLog, 24 | }: CallFunctionProps) { 25 | const { contract } = useContract(contractId); 26 | 27 | const mutation = useMutation({ 28 | onSuccess: (data) => { 29 | handleSuccess(data); 30 | }, 31 | onError: handleError, 32 | mutationFn: async () => { 33 | updateLog( 34 | `Calling ${functionName} with parameters ${JSON.stringify(parameters)}${ 35 | callType === "dryrun" ? " (DRY RUN)" : "" 36 | }`, 37 | ); 38 | 39 | if (!contract) throw new Error("Contract not connected"); 40 | 41 | const functionHandle = contract.functions[functionName]; 42 | const functionCaller = functionHandle(...parameters); 43 | const transactionResult = 44 | callType === "dryrun" || functionHandle.isReadOnly() 45 | ? await functionCaller.dryRun() 46 | : await functionCaller.call(); 47 | return transactionResult; 48 | }, 49 | }); 50 | 51 | function handleError(error: Error) { 52 | updateLog(`Function call failed. Error: ${error?.message}`); 53 | setResponse(new Error(error?.message)); 54 | } 55 | 56 | async function handleSuccess( 57 | data: 58 | | DryRunResult 59 | | { 60 | transactionId: string; 61 | waitForResult: () => Promise>; 62 | }, 63 | ) { 64 | if ("transactionId" in data) { 65 | updateLog(`Transaction submitted. Transaction ID: ${data.transactionId}`); 66 | const result = await data.waitForResult(); 67 | setResponse(JSON.stringify(result.value, modifyJsonStringify, 2)); 68 | } else { 69 | setResponse(JSON.stringify(data.value, modifyJsonStringify, 2)); 70 | } 71 | } 72 | 73 | return mutation; 74 | } 75 | -------------------------------------------------------------------------------- /app/src/features/interact/hooks/useContract.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { useWallet } from "@fuels/react"; 3 | import { Contract, Interface } from "fuels"; 4 | import { loadAbi } from "../../../utils/localStorage"; 5 | 6 | export function useContract(contractId: string) { 7 | const { wallet, isLoading, isError } = useWallet(); 8 | 9 | const { 10 | data: contract, 11 | isLoading: isContractLoading, 12 | isError: isContractError, 13 | } = useQuery({ 14 | enabled: !isLoading && !isError && !!wallet && !!contractId.length, 15 | queryKey: ["contract"], 16 | queryFn: async () => { 17 | const cachedAbi = loadAbi(); 18 | if (!!wallet && !!cachedAbi.length && !!contractId.length) { 19 | const abi: Interface = JSON.parse(cachedAbi); 20 | return new Contract(contractId, abi, wallet); 21 | } 22 | }, 23 | }); 24 | 25 | return { contract, isLoading: isContractLoading, isError: isContractError }; 26 | } 27 | -------------------------------------------------------------------------------- /app/src/features/interact/hooks/useContractFunctions.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { useContract } from "./useContract"; 3 | import { Contract } from "fuels"; 4 | 5 | export function useContractFunctions(contractId: string): { 6 | contract: Contract | undefined; 7 | functionNames: string[]; 8 | } { 9 | const { contract } = useContract(contractId); 10 | 11 | const { data: functions } = useQuery({ 12 | enabled: !!contract, 13 | queryKey: ["contractFunctions"], 14 | queryFn: async () => { 15 | if (!contract) throw new Error("Contract not connected"); 16 | return contract.interface.functions; 17 | }, 18 | }); 19 | 20 | return { 21 | contract, 22 | functionNames: functions ? [...Object.keys(functions)] : [], 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /app/src/features/interact/utils/abi.ts: -------------------------------------------------------------------------------- 1 | import { JsonAbi } from "fuels"; 2 | 3 | /// Types ported from the fuels SDK 4 | export interface SdkConcreteType { 5 | readonly type: string; 6 | readonly concreteTypeId: string; 7 | readonly metadataTypeId?: number; 8 | readonly typeArguments?: readonly string[]; 9 | } 10 | export interface SdkMetadataType { 11 | readonly type: string; 12 | readonly metadataTypeId: number; 13 | readonly components?: readonly SdkComponent[]; 14 | readonly typeParameters?: readonly number[]; 15 | } 16 | 17 | export interface SdkComponent extends SdkTypeArgument { 18 | readonly name: string; 19 | } 20 | 21 | export interface SdkTypeArgument { 22 | readonly typeId: number | string; 23 | readonly typeArguments?: readonly SdkTypeArgument[]; 24 | } 25 | 26 | /// This is a helper class for ABI related functions. 27 | export class AbiHelper { 28 | /// A map of all the concrete types in the ABI indexed by the concrete type ID (hash). 29 | concreteTypeMap: Map; 30 | 31 | /// A map of all the metadata types in the ABI indexed by the metadata type ID (index). 32 | metadataTypeMap: Map; 33 | 34 | constructor(jsonAbi: JsonAbi | undefined) { 35 | this.concreteTypeMap = new Map(); 36 | this.metadataTypeMap = new Map(); 37 | 38 | jsonAbi?.concreteTypes?.forEach((concreteType: SdkConcreteType) => { 39 | this.concreteTypeMap.set(concreteType.concreteTypeId, concreteType); 40 | }); 41 | 42 | jsonAbi?.metadataTypes?.forEach( 43 | (metadataType: SdkMetadataType, index: number) => { 44 | this.metadataTypeMap.set(index, metadataType); 45 | }, 46 | ); 47 | } 48 | 49 | getConcreteTypeById(id?: string): SdkConcreteType | undefined { 50 | return id ? this.concreteTypeMap.get(id) : undefined; 51 | } 52 | 53 | getMetadataTypeById(id?: number): SdkMetadataType | undefined { 54 | return id ? this.metadataTypeMap.get(id) : undefined; 55 | } 56 | 57 | getTypesById(id: string | number | undefined): { 58 | concreteType: SdkConcreteType | undefined; 59 | metadataType: SdkMetadataType | undefined; 60 | } { 61 | if (typeof id === "number") { 62 | const metadataType = this.getMetadataTypeById(id); 63 | return { concreteType: undefined, metadataType }; 64 | } else { 65 | const concreteType = this.getConcreteTypeById(id); 66 | const metadataType = concreteType 67 | ? this.getMetadataTypeById(concreteType.metadataTypeId) 68 | : undefined; 69 | return { concreteType, metadataType }; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/features/interact/utils/getTypeInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { getLiteral, parseTypeName } from "./getTypeInfo"; 2 | 3 | describe(`test getLiteral`, () => { 4 | test.each` 5 | input | expected 6 | ${"enum std::option::Option"} | ${"option"} 7 | ${"enum std::option::Option"} | ${"option"} 8 | ${"struct ComplexStruct2"} | ${"object"} 9 | ${"struct std::vec::Vec"} | ${"vector"} 10 | ${"bool"} | ${"bool"} 11 | ${"b256"} | ${"string"} 12 | ${"str[7]"} | ${"string"} 13 | ${"random string"} | ${"string"} 14 | `("$input", ({ input, expected }) => { 15 | expect(getLiteral(input)).toEqual(expected); 16 | }); 17 | }); 18 | 19 | describe(`test parseTypeName`, () => { 20 | test.each` 21 | input | expected 22 | ${"std::option::Option"} | ${"Option"} 23 | ${"std::option::Option"} | ${"Option"} 24 | ${"ComplexStruct2"} | ${"ComplexStruct2"} 25 | ${"std::vec::Vec"} | ${"Vec"} 26 | ${"bool"} | ${"bool"} 27 | ${"b256"} | ${"b256"} 28 | ${"str[7]"} | ${"str[7]"} 29 | ${""} | ${""} 30 | `("$input", ({ input, expected }) => { 31 | expect(parseTypeName(input)).toEqual(expected); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /app/src/features/interact/utils/getTypeInfo.ts: -------------------------------------------------------------------------------- 1 | import { ParamTypeLiteral } from "../components/FunctionParameters"; 2 | import { AbiHelper, SdkConcreteType } from "./abi"; 3 | 4 | /// An interface for displaying ABI types. 5 | export interface TypeInfo { 6 | literal: ParamTypeLiteral; 7 | swayType: string; 8 | } 9 | 10 | export function getLiteral(sdkType: string): ParamTypeLiteral { 11 | const [type, name] = sdkType.split(" "); 12 | const trimmedName = name ? parseTypeName(name) : name; 13 | switch (type) { 14 | case "struct": { 15 | return trimmedName === "Vec" ? "vector" : "object"; 16 | } 17 | case "enum": { 18 | return trimmedName === "Option" ? "option" : "enum"; 19 | } 20 | case "u8": 21 | case "u16": 22 | case "u32": 23 | case "u64": 24 | return "number"; 25 | case "bool": 26 | return "bool"; 27 | case "b512": 28 | case "b256": 29 | case "raw untyped ptr": 30 | default: 31 | return "string"; 32 | } 33 | } 34 | 35 | export function parseTypeName(typeName: string): string { 36 | const trimmed = typeName.split("<")[0].split("::"); 37 | return trimmed[trimmed.length - 1]; 38 | } 39 | 40 | function formatTypeArguments( 41 | concreteTypeId: string, 42 | abiHelper: AbiHelper, 43 | ): string { 44 | const sdkType = abiHelper.getConcreteTypeById(concreteTypeId); 45 | if (!sdkType) { 46 | return "Unknown"; 47 | } 48 | const [type, name] = sdkType.type.split(" "); 49 | if (!name) { 50 | return type; 51 | } 52 | if (!sdkType?.typeArguments?.length) { 53 | return parseTypeName(name); 54 | } 55 | return `${parseTypeName(name)}<${sdkType.typeArguments.map((ta) => formatTypeArguments(ta, abiHelper)).join(", ")}>`; 56 | } 57 | 58 | export function getTypeInfo( 59 | sdkType: SdkConcreteType | undefined, 60 | abiHelper: AbiHelper, 61 | ): TypeInfo { 62 | if (!abiHelper || !sdkType) { 63 | return { 64 | literal: "string", 65 | swayType: "Unknown", 66 | }; 67 | } 68 | return { 69 | literal: getLiteral(sdkType.type), 70 | swayType: formatTypeArguments(sdkType.concreteTypeId, abiHelper), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /app/src/features/interact/utils/modifyJsonStringify.ts: -------------------------------------------------------------------------------- 1 | export function modifyJsonStringify(key: unknown, value: unknown) { 2 | // JSON.stringify omits the key when value === undefined 3 | if (value === undefined) { 4 | return "undefined"; 5 | } 6 | // possibilities for values: 7 | // undefined => Option::None 8 | // [] => () 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/features/toolbar/components/AbiActionToolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import OpenInNew from "@mui/icons-material/OpenInNew"; 3 | import SecondaryButton from "../../../components/SecondaryButton"; 4 | import { useIsMobile } from "../../../hooks/useIsMobile"; 5 | import SwitchThemeButton from "./SwitchThemeButton"; 6 | import { useConnectIfNotAlready } from "../hooks/useConnectIfNotAlready"; 7 | import { useDisconnect } from "@fuels/react"; 8 | 9 | export interface AbiActionToolbarProps { 10 | drawerOpen: boolean; 11 | setDrawerOpen: (open: boolean) => void; 12 | } 13 | 14 | function AbiActionToolbar({ 15 | drawerOpen, 16 | setDrawerOpen, 17 | }: AbiActionToolbarProps) { 18 | const isMobile = useIsMobile(); 19 | const { isConnected, connect } = useConnectIfNotAlready(); 20 | const { disconnect } = useDisconnect(); 21 | 22 | const onDocsClick = useCallback(() => { 23 | window.open("https://docs.fuel.network/docs/sway", "_blank", "noreferrer"); 24 | }, []); 25 | 26 | return ( 27 |
33 | setDrawerOpen(!drawerOpen)} 36 | text="INTERACT" 37 | tooltip="Interact with the contract ABI" 38 | /> 39 | } 45 | /> 46 | 54 | {!isMobile && } 55 |
56 | ); 57 | } 58 | 59 | export default AbiActionToolbar; 60 | -------------------------------------------------------------------------------- /app/src/features/toolbar/components/ActionToolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import PlayArrow from "@mui/icons-material/PlayArrow"; 3 | import OpenInNew from "@mui/icons-material/OpenInNew"; 4 | import { DeployState } from "../../../utils/types"; 5 | import { DeploymentButton } from "./DeploymentButton"; 6 | import CompileButton from "./CompileButton"; 7 | import SecondaryButton from "../../../components/SecondaryButton"; 8 | import { 9 | loadAbi, 10 | loadBytecode, 11 | loadStorageSlots, 12 | } from "../../../utils/localStorage"; 13 | import { useIsMobile } from "../../../hooks/useIsMobile"; 14 | import SwitchThemeButton from "./SwitchThemeButton"; 15 | import { useConnectIfNotAlready } from "../hooks/useConnectIfNotAlready"; 16 | import { useDisconnect } from "@fuels/react"; 17 | import { useNavigate } from "react-router-dom"; 18 | 19 | export interface ActionToolbarProps { 20 | deployState: DeployState; 21 | setContractId: (contractId: string) => void; 22 | onShareClick: () => void; 23 | onCompile: () => void; 24 | isCompiled: boolean; 25 | setDeployState: (state: DeployState) => void; 26 | drawerOpen: boolean; 27 | setDrawerOpen: (open: boolean) => void; 28 | showSolidity: boolean; 29 | setShowSolidity: (open: boolean) => void; 30 | updateLog: (entry: string) => void; 31 | } 32 | 33 | function ActionToolbar({ 34 | deployState, 35 | setContractId, 36 | onShareClick, 37 | onCompile, 38 | isCompiled, 39 | setDeployState, 40 | drawerOpen, 41 | setDrawerOpen, 42 | showSolidity, 43 | setShowSolidity, 44 | updateLog, 45 | }: ActionToolbarProps) { 46 | const isMobile = useIsMobile(); 47 | const { isConnected } = useConnectIfNotAlready(); 48 | const { disconnect } = useDisconnect(); 49 | const navigate = useNavigate(); 50 | 51 | const onDocsClick = useCallback(() => { 52 | window.open("https://docs.fuel.network/docs/sway", "_blank", "noreferrer"); 53 | }, []); 54 | 55 | return ( 56 |
62 | } 66 | disabled={isCompiled === true || deployState === DeployState.DEPLOYING} 67 | tooltip="Compile sway code" 68 | /> 69 | {!isMobile && ( 70 | 81 | )} 82 | {!isMobile && deployState === DeployState.DEPLOYED && ( 83 | setDrawerOpen(!drawerOpen)} 86 | text="INTERACT" 87 | tooltip={ 88 | deployState !== DeployState.DEPLOYED 89 | ? "A contract must be deployed to interact with it on-chain" 90 | : "Interact with the contract ABI" 91 | } 92 | /> 93 | )} 94 | setShowSolidity(!showSolidity)} 97 | text="SOLIDITY" 98 | tooltip={ 99 | showSolidity 100 | ? "Hide the Solidity editor" 101 | : "Show the Solidity editor to transpile Solidity to Sway" 102 | } 103 | /> 104 | navigate("/abi")} 107 | text="ABI" 108 | tooltip="Query an already-deployed contract using the ABI" 109 | /> 110 | } 116 | /> 117 | 123 | {isConnected && !isMobile && ( 124 | 130 | )} 131 | {!isMobile && } 132 |
133 | ); 134 | } 135 | 136 | export default ActionToolbar; 137 | -------------------------------------------------------------------------------- /app/src/features/toolbar/components/CompileButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@mui/material/Button"; 3 | import Tooltip from "@mui/material/Tooltip"; 4 | import { darkColors, lightColors } from "@fuel-ui/css"; 5 | import useTheme from "../../../context/theme"; 6 | 7 | export interface CompileButtonProps { 8 | onClick: () => void; 9 | text: string; 10 | endIcon?: React.ReactNode; 11 | disabled?: boolean; 12 | tooltip?: string; 13 | style?: React.CSSProperties; 14 | } 15 | function CompileButton({ 16 | onClick, 17 | text, 18 | endIcon, 19 | disabled, 20 | tooltip, 21 | style, 22 | }: CompileButtonProps) { 23 | const { themeColor } = useTheme(); 24 | 25 | return ( 26 | 27 | 28 | 55 | 56 | 57 | ); 58 | } 59 | 60 | export default CompileButton; 61 | -------------------------------------------------------------------------------- /app/src/features/toolbar/components/DeploymentButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from "react"; 2 | import { DeployState } from "../../../utils/types"; 3 | import { 4 | DeployContractData, 5 | useDeployContract, 6 | } from "../hooks/useDeployContract"; 7 | import SecondaryButton from "../../../components/SecondaryButton"; 8 | import { ButtonSpinner } from "../../../components/shared"; 9 | import { useConnectIfNotAlready } from "../hooks/useConnectIfNotAlready"; 10 | import { track } from "@vercel/analytics/react"; 11 | 12 | interface DeploymentButtonProps { 13 | abi: string; 14 | bytecode: string; 15 | storageSlots: string; 16 | isCompiled: boolean; 17 | setContractId: (contractId: string) => void; 18 | deployState: DeployState; 19 | setDeployState: (state: DeployState) => void; 20 | setDrawerOpen: (open: boolean) => void; 21 | updateLog: (entry: string) => void; 22 | } 23 | 24 | export function DeploymentButton({ 25 | abi, 26 | bytecode, 27 | storageSlots, 28 | isCompiled, 29 | setContractId, 30 | deployState, 31 | setDeployState, 32 | setDrawerOpen, 33 | updateLog, 34 | }: DeploymentButtonProps) { 35 | const { connectIfNotAlready, isConnected } = useConnectIfNotAlready(); 36 | 37 | const handleError = useCallback( 38 | (error: Error) => { 39 | setDeployState(DeployState.NOT_DEPLOYED); 40 | updateLog(`Deployment failed: ${error.message}`); 41 | }, 42 | [setDeployState, updateLog], 43 | ); 44 | 45 | const handleSuccess = useCallback( 46 | ({ contractId, networkUrl }: DeployContractData) => { 47 | setDeployState(DeployState.DEPLOYED); 48 | setContractId(contractId); 49 | setDrawerOpen(true); 50 | updateLog(`Contract was successfully deployed to ${networkUrl}`); 51 | }, 52 | [setContractId, setDeployState, setDrawerOpen, updateLog], 53 | ); 54 | 55 | const deployContractMutation = useDeployContract( 56 | abi, 57 | bytecode, 58 | storageSlots, 59 | handleError, 60 | handleSuccess, 61 | updateLog, 62 | ); 63 | 64 | const handleDeploy = useCallback(async () => { 65 | updateLog(`Deploying contract...`); 66 | setDeployState(DeployState.DEPLOYING); 67 | deployContractMutation.mutate(); 68 | }, [updateLog, setDeployState, deployContractMutation]); 69 | 70 | const handleConnectionFailed = useCallback( 71 | async () => handleError(new Error("Failed to connect to wallet.")), 72 | [handleError], 73 | ); 74 | 75 | const onDeployClick = useCallback(async () => { 76 | track("Deploy Click"); 77 | if (!isConnected) { 78 | updateLog(`Connecting to wallet...`); 79 | } 80 | await connectIfNotAlready(handleDeploy, handleConnectionFailed); 81 | }, [ 82 | isConnected, 83 | updateLog, 84 | connectIfNotAlready, 85 | handleDeploy, 86 | handleConnectionFailed, 87 | ]); 88 | 89 | const { isDisabled, tooltip } = useMemo(() => { 90 | switch (deployState) { 91 | case DeployState.DEPLOYING: 92 | return { 93 | isDisabled: true, 94 | tooltip: `Deploying contract`, 95 | }; 96 | case DeployState.NOT_DEPLOYED: 97 | return { 98 | isDisabled: !abi || !bytecode || !isCompiled, 99 | tooltip: "Deploy a contract to interact with it on-chain", 100 | }; 101 | case DeployState.DEPLOYED: 102 | return { 103 | isDisabled: false, 104 | tooltip: 105 | "Contract is deployed. You can interact with the deployed contract or re-compile and deploy a new contract.", 106 | }; 107 | } 108 | }, [abi, bytecode, deployState, isCompiled]); 109 | 110 | return ( 111 | : undefined 118 | } 119 | tooltip={tooltip} 120 | /> 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /app/src/features/toolbar/components/SwitchThemeButton.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from "@mui/material/IconButton"; 2 | import LightModeIcon from "@mui/icons-material/LightMode"; 3 | import DarkModeIcon from "@mui/icons-material/DarkMode"; 4 | import { darkColors, lightColors } from "@fuel-ui/css"; 5 | import useTheme from "../../../context/theme"; 6 | 7 | function SwitchThemeButton() { 8 | const { theme, setTheme } = useTheme(); 9 | 10 | const handleChange = async () => { 11 | const next = theme === "dark" ? "light" : "dark"; 12 | setTheme(next); 13 | }; 14 | 15 | return ( 16 | 28 | {theme === "light" ? ( 29 | 30 | ) : ( 31 | 32 | )} 33 | 34 | ); 35 | } 36 | 37 | export default SwitchThemeButton; 38 | -------------------------------------------------------------------------------- /app/src/features/toolbar/hooks/useConnectIfNotAlready.ts: -------------------------------------------------------------------------------- 1 | import { useConnectUI, useWallet } from "@fuels/react"; 2 | import { useCallback, useMemo, useEffect, useRef } from "react"; 3 | 4 | export function useConnectIfNotAlready() { 5 | const connectedCallbackRef = useRef<(() => Promise) | null>(null); 6 | const failedCallbackRef = useRef<(() => Promise) | null>(null); 7 | const { connect, isError, isConnecting } = useConnectUI(); 8 | const { wallet } = useWallet(); 9 | const isConnected = useMemo(() => !!wallet, [wallet]); 10 | 11 | const connectIfNotAlready = useCallback( 12 | ( 13 | connectedCallback: () => Promise, 14 | failedCallback: () => Promise, 15 | ) => { 16 | connectedCallbackRef.current = connectedCallback; 17 | failedCallbackRef.current = failedCallback; 18 | 19 | if (!isConnected && !isConnecting) { 20 | connect(); 21 | } else { 22 | connectedCallback(); 23 | } 24 | }, 25 | [connect, isConnected, isConnecting], 26 | ); 27 | 28 | useEffect(() => { 29 | if (connectedCallbackRef.current && isConnected) { 30 | connectedCallbackRef.current(); 31 | connectedCallbackRef.current = null; 32 | } 33 | }, [isConnected, connectedCallbackRef]); 34 | 35 | useEffect(() => { 36 | if (failedCallbackRef.current && isError) { 37 | failedCallbackRef.current(); 38 | failedCallbackRef.current = null; 39 | } 40 | }, [isError, failedCallbackRef]); 41 | 42 | return { connectIfNotAlready, isConnected, connect }; 43 | } 44 | -------------------------------------------------------------------------------- /app/src/features/toolbar/hooks/useDeployContract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | ContractFactory, 4 | DeployContractResult, 5 | JsonAbi, 6 | StorageSlot, 7 | } from "fuels"; 8 | import { useMutation } from "@tanstack/react-query"; 9 | import { useFuel, useWallet } from "@fuels/react"; 10 | import { track } from "@vercel/analytics/react"; 11 | import { useEffect, useState } from "react"; 12 | import { toMetricProperties } from "../../../utils/metrics"; 13 | import Timeout from "await-timeout"; 14 | 15 | const DEPLOYMENT_TIMEOUT_MS = 120000; 16 | 17 | export interface DeployContractData { 18 | contractId: string; 19 | networkUrl: string; 20 | } 21 | 22 | export function useDeployContract( 23 | abi: string, 24 | bytecode: string, 25 | storageSlots: string, 26 | onError: (error: Error) => void, 27 | onSuccess: (data: DeployContractData) => void, 28 | updateLog: (entry: string) => void, 29 | ) { 30 | const { wallet, isLoading: walletIsLoading } = useWallet(); 31 | const { fuel } = useFuel(); 32 | const [metricMetadata, setMetricMetadata] = useState({}); 33 | 34 | useEffect(() => { 35 | const waitForMetadata = async () => { 36 | const name = fuel.currentConnector()?.name ?? "none"; 37 | const networkUrl = wallet?.provider.url ?? "none"; 38 | const version = (await wallet?.provider.getVersion()) ?? "none"; 39 | setMetricMetadata({ name, version, networkUrl }); 40 | }; 41 | waitForMetadata(); 42 | }, [wallet, fuel]); 43 | 44 | const mutation = useMutation({ 45 | // Retry once if the wallet is still loading. 46 | retry: walletIsLoading && !wallet ? 1 : 0, 47 | onSuccess, 48 | onError: (error) => { 49 | track("Deploy Error", toMetricProperties(error, metricMetadata)); 50 | onError(error); 51 | }, 52 | mutationFn: async (): Promise => { 53 | if (!wallet) { 54 | if (walletIsLoading) { 55 | updateLog("Connecting to wallet..."); 56 | } else { 57 | throw new Error("Failed to connect to wallet", { 58 | cause: { source: "wallet" }, 59 | }); 60 | } 61 | } 62 | 63 | const resultPromise = new Promise( 64 | (resolve: (data: DeployContractData) => void, reject) => { 65 | const contractFactory = new ContractFactory( 66 | bytecode, 67 | JSON.parse(abi) as JsonAbi, 68 | wallet, 69 | ); 70 | 71 | contractFactory 72 | .deploy({ 73 | storageSlots: JSON.parse(storageSlots) as StorageSlot[], 74 | }) 75 | .then(({ waitForResult }: DeployContractResult) => 76 | waitForResult(), 77 | ) 78 | .then(({ contract }) => { 79 | resolve({ 80 | contractId: contract.id.toB256(), 81 | networkUrl: contract.provider.url, 82 | }); 83 | }) 84 | .catch( 85 | (error: { 86 | code: number | undefined; 87 | cause: object | undefined; 88 | }) => { 89 | // This is a hack to handle the case where the deployment failed because the user rejected the transaction. 90 | const source = error?.code === 0 ? "user" : "sdk"; 91 | error.cause = { source }; 92 | reject(error); 93 | }, 94 | ); 95 | }, 96 | ); 97 | 98 | return Timeout.wrap( 99 | resultPromise, 100 | DEPLOYMENT_TIMEOUT_MS, 101 | `Request timed out after ${DEPLOYMENT_TIMEOUT_MS / 1000} seconds`, 102 | ); 103 | }, 104 | }); 105 | 106 | return mutation; 107 | } 108 | -------------------------------------------------------------------------------- /app/src/hooks/useIsMobile.tsx: -------------------------------------------------------------------------------- 1 | import useTheme from "@mui/material/styles/useTheme"; 2 | import useMediaQuery from "@mui/material/useMediaQuery"; 3 | 4 | export function useIsMobile() { 5 | const theme = useTheme(); 6 | return useMediaQuery(theme.breakpoints.down("md")); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import AbiApp from "./AbiApp"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import "./index.css"; 7 | import { Providers } from "./components/Providers"; 8 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 9 | 10 | const router = createBrowserRouter([ 11 | { 12 | path: "/", 13 | element: , 14 | }, 15 | { 16 | path: "/abi", 17 | element: , 18 | }, 19 | ]); 20 | 21 | const root = ReactDOM.createRoot( 22 | document.getElementById("root") as HTMLElement, 23 | ); 24 | 25 | root.render( 26 | 27 | 28 | 29 | 30 | , 31 | ); 32 | 33 | // If you want to start measuring performance in your app, pass a function 34 | // to log results (for example: reportWebVitals(console.log)) 35 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 36 | reportWebVitals(); 37 | -------------------------------------------------------------------------------- /app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /app/src/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { EXAMPLE_SOLIDITY_CONTRACTS } from "../features/editor/examples/solidity"; 2 | import { EXAMPLE_SWAY_CONTRACTS } from "../features/editor/examples/sway"; 3 | 4 | const STORAGE_ABI_KEY = "playground_abi"; 5 | const STORAGE_SLOTS_KEY = "playground_slots"; 6 | const STORAGE_BYTECODE_KEY = "playground_bytecode"; 7 | const STORAGE_CONTRACT_KEY = "playground_contract"; 8 | const STORAGE_SOLIDITY_CONTRACT_KEY = "playground_solidity_contract"; 9 | 10 | export function saveAbi(abi: string) { 11 | localStorage.setItem(STORAGE_ABI_KEY, abi); 12 | } 13 | 14 | export function loadAbi() { 15 | return localStorage.getItem(STORAGE_ABI_KEY) || ""; 16 | } 17 | 18 | export function saveStorageSlots(slots: string) { 19 | localStorage.setItem(STORAGE_SLOTS_KEY, slots); 20 | } 21 | 22 | export function loadStorageSlots() { 23 | return localStorage.getItem(STORAGE_SLOTS_KEY) || ""; 24 | } 25 | 26 | export function saveBytecode(bytecode: string) { 27 | localStorage.setItem(STORAGE_BYTECODE_KEY, bytecode); 28 | } 29 | 30 | export function loadBytecode() { 31 | return localStorage.getItem(STORAGE_BYTECODE_KEY) || ""; 32 | } 33 | 34 | export function saveSwayCode(code: string) { 35 | localStorage.setItem(STORAGE_CONTRACT_KEY, code); 36 | } 37 | 38 | export function saveSolidityCode(code: string) { 39 | localStorage.setItem(STORAGE_SOLIDITY_CONTRACT_KEY, code); 40 | } 41 | 42 | export function loadSwayCode() { 43 | return ( 44 | localStorage.getItem(STORAGE_CONTRACT_KEY) ?? EXAMPLE_SWAY_CONTRACTS[0].code 45 | ); 46 | } 47 | 48 | export function loadSolidityCode() { 49 | return ( 50 | localStorage.getItem(STORAGE_SOLIDITY_CONTRACT_KEY) ?? 51 | EXAMPLE_SOLIDITY_CONTRACTS[0].code 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/src/utils/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { toMetricProperties } from "./metrics"; 2 | 3 | describe(`test toMetricProperties`, () => { 4 | test.each` 5 | label | cause | metadata | expected 6 | ${"with metadata and cause"} | ${{ source: "test" }} | ${{ other: "other" }} | ${{ source: "test", other: "other" }} 7 | ${"with invalid cause"} | ${"str"} | ${undefined} | ${undefined} 8 | ${"without cause or metadata"} | ${undefined} | ${undefined} | ${undefined} 9 | ${"with cause only"} | ${{ source: "test" }} | ${undefined} | ${{ source: "test" }} 10 | ${"with metadata only"} | ${undefined} | ${{ other: "other" }} | ${{ other: "other" }} 11 | `("$label", ({ cause, metadata, expected }) => { 12 | const error = cause ? new Error("Test", { cause }) : new Error("Test"); 13 | expect(toMetricProperties(error, metadata)).toEqual(expected); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/utils/metrics.ts: -------------------------------------------------------------------------------- 1 | type AllowedProperty = string | number | boolean | null; 2 | 3 | function isRecord(value: unknown): value is Record { 4 | return typeof value === "object" && value !== null; 5 | } 6 | 7 | function isAllowedEntry( 8 | entry: [string, unknown], 9 | ): entry is [string, AllowedProperty] { 10 | const value = entry[1]; 11 | return ( 12 | typeof value === "string" || 13 | typeof value === "number" || 14 | typeof value === "boolean" || 15 | value === null 16 | ); 17 | } 18 | 19 | export function toMetricProperties( 20 | error: Error, 21 | metadata?: Record, 22 | ): Record | undefined { 23 | const combined = { ...metadata }; 24 | if (isRecord(error.cause)) { 25 | Object.assign(combined, error.cause); 26 | } 27 | if (Object.keys(combined).length) { 28 | return Object.entries(combined) 29 | .filter(isAllowedEntry) 30 | .reduce((acc: Record, [key, value]) => { 31 | acc[key] = value; 32 | return acc; 33 | }, {}); 34 | } 35 | return undefined; 36 | } 37 | -------------------------------------------------------------------------------- /app/src/utils/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | retry: false, 7 | refetchOnWindowFocus: false, 8 | structuralSharing: false, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /app/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export enum DeployState { 2 | NOT_DEPLOYED = "NOT_DEPLOYED", 3 | DEPLOYING = "DEPLOYING", 4 | DEPLOYED = "DEPLOYED", 5 | } 6 | 7 | export type CallType = "call" | "dryrun"; 8 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "jsxImportSource": "@emotion/react" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build 2 | FROM lukemathwalker/cargo-chef:latest-rust-1.81 as chef 3 | WORKDIR /build/ 4 | # hadolint ignore=DL3008 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y --no-install-recommends \ 8 | lld \ 9 | clang \ 10 | libclang-dev \ 11 | && apt-get clean \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | # Build sway-playground 15 | FROM chef as planner 16 | ENV CARGO_NET_GIT_FETCH_WITH_CLI=true 17 | COPY . . 18 | RUN cargo chef prepare --recipe-path recipe.json 19 | 20 | FROM chef as builder 21 | 22 | # Install charcoal 23 | RUN cargo install --git https://github.com/ourovoros-io/charcoal.git --rev e69a6ffaf3e7eaf9f3ceea543087ea59ec5fd5d1 24 | 25 | ENV CARGO_NET_GIT_FETCH_WITH_CLI=true 26 | COPY --from=planner /build/recipe.json recipe.json 27 | # Build our project dependecies, not our application! 28 | RUN cargo chef cook --recipe-path recipe.json 29 | # Up to this point, if our dependency tree stays the same, 30 | # all layers should be cached. 31 | COPY . . 32 | RUN cargo build 33 | 34 | # Stage 2: Run 35 | FROM ubuntu:22.04 as run 36 | 37 | RUN apt-get update -y \ 38 | && apt-get install -y --no-install-recommends ca-certificates curl git pkg-config libssl-dev \ 39 | # Clean up 40 | && apt-get autoremove -y \ 41 | && apt-get clean -y \ 42 | && rm -rf /var/lib/apt/lists/* 43 | 44 | WORKDIR /root/ 45 | 46 | COPY --from=builder /build/target/debug/sway-playground . 47 | COPY --from=builder /build/target/debug/sway-playground.d . 48 | COPY --from=builder /build/Rocket.toml . 49 | COPY --from=builder /build/projects projects 50 | COPY --from=builder /usr/local/cargo/bin/charcoal /bin 51 | 52 | # Install fuelup 53 | RUN curl -fsSL https://install.fuel.network/ | sh -s -- --no-modify-path 54 | ENV PATH="/root/.fuelup/bin:$PATH" 55 | 56 | # Install all fuel toolchains 57 | RUN fuelup toolchain install latest 58 | RUN fuelup toolchain install nightly 59 | RUN fuelup toolchain install testnet 60 | RUN fuelup toolchain install mainnet 61 | 62 | # Install the forc dependencies 63 | RUN fuelup default testnet 64 | RUN forc build --path projects/swaypad 65 | 66 | EXPOSE 8080 67 | 68 | CMD ["./sway-playground"] 69 | -------------------------------------------------------------------------------- /deployment/charts/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: sway-playground 3 | description: Sway Playground Helm Chart 4 | type: application 5 | appVersion: '0.1.0' 6 | version: 0.1.0 7 | -------------------------------------------------------------------------------- /deployment/charts/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "sway-playground.name" -}} 2 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 3 | {{- end -}} 4 | 5 | {{- define "sway-playground.fullname" -}} 6 | {{- if .Values.fullnameOverride -}} 7 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 8 | {{- else -}} 9 | {{- $name := default .Chart.Name .Values.nameOverride -}} 10 | {{- if contains $name .Release.Name -}} 11 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 12 | {{- else -}} 13 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 14 | {{- end -}} 15 | {{- end -}} 16 | {{- end -}} 17 | 18 | {{- define "sway-playground.chart" -}} 19 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 20 | {{- end -}} 21 | 22 | -------------------------------------------------------------------------------- /deployment/charts/templates/sway-playground-deploy.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | labels: 5 | app: {{ template "sway-playground.name" . }} 6 | chart: {{ template "sway-playground.chart" . }} 7 | release: {{ .Release.Name }} 8 | heritage: {{ .Release.Service }} 9 | name: {{ template "sway-playground.name" . }}-service 10 | spec: 11 | type: NodePort 12 | selector: 13 | app: {{ template "sway-playground.name" . }} 14 | ports: 15 | - name: http 16 | port: {{ .Values.app.http_port }} 17 | protocol: TCP 18 | targetPort: {{ .Values.app.target_port }} 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | name: {{ template "sway-playground.name" . }}-k8s 24 | labels: 25 | app: {{ template "sway-playground.name" . }} 26 | chart: {{ template "sway-playground.chart" . }} 27 | release: {{ .Release.Name }} 28 | heritage: {{ .Release.Service }} 29 | spec: 30 | selector: 31 | matchLabels: 32 | app: {{ template "sway-playground.name" . }} 33 | release: {{ .Release.Name }} 34 | replicas: {{ .Values.app.replicas }} 35 | template: 36 | metadata: 37 | labels: 38 | app: {{ template "sway-playground.name" . }} 39 | release: {{ .Release.Name }} 40 | spec: 41 | containers: 42 | - name: {{ .Values.app.name }} 43 | image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}" 44 | command: ["./sway-playground"] 45 | resources: {} 46 | imagePullPolicy: {{ .Values.app.image.pullPolicy }} 47 | ports: 48 | - name: http 49 | containerPort: {{ .Values.app.target_port }} 50 | protocol: TCP 51 | livenessProbe: 52 | httpGet: 53 | path: /health 54 | port: {{ .Values.app.target_port }} 55 | initialDelaySeconds: 10 56 | periodSeconds: 5 57 | timeoutSeconds: 60 58 | env: 59 | - name: PORT 60 | value: "{{ .Values.app.target_port }}" 61 | -------------------------------------------------------------------------------- /deployment/charts/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for fuel sway-playground 2 | 3 | app: 4 | name: sway-playground 5 | replicas: 1 6 | http_port: 80 7 | target_port: 8080 8 | image: 9 | repository: '${sway_playground_image_repository}' 10 | tag: '${sway_playground_image_tag}' 11 | pullPolicy: Always 12 | -------------------------------------------------------------------------------- /deployment/ingress/eks/sway-playground-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: ${k8s_namespace}-sway-playground-ingress 5 | namespace: ${k8s_namespace} 6 | annotations: 7 | nginx.ingress.kubernetes.io/ssl-redirect: 'false' 8 | nginx.ingress.kubernetes.io/force-ssl-redirect: 'false' 9 | nginx.ingress.kubernetes.io/rewrite-target: / 10 | cert-manager.io/cluster-issuer: 'letsencrypt-prod' 11 | kubernetes.io/ingress.class: 'nginx' 12 | spec: 13 | rules: 14 | - host: ${sway_playground_ingress_dns} 15 | http: 16 | paths: 17 | - path: / 18 | pathType: Prefix 19 | backend: 20 | service: 21 | name: sway-playground-service 22 | port: 23 | number: ${sway_playground_ingress_http_port} 24 | tls: 25 | - hosts: 26 | - ${sway_playground_ingress_dns} 27 | secretName: ${sway_playground_dns_secret} 28 | -------------------------------------------------------------------------------- /deployment/scripts/.env: -------------------------------------------------------------------------------- 1 | # k8s envs 2 | k8s_provider="eks" 3 | k8s_namespace="fuel-core" 4 | 5 | # sway-playground envs 6 | sway_playground_image_repository="ghcr.io/fuellabs/sway-playground" 7 | sway_playground_image_tag="latest" 8 | 9 | # Ingress envs 10 | letsencrypt_email="helloworld@gmail.com" 11 | sway_playground_ingress_dns="sway-playground.example.com" 12 | sway_playground_dns_secret="sway-playground-example-com" 13 | sway_playground_ingress_http_port="80" 14 | 15 | # EKS 16 | TF_VAR_eks_cluster_name="test-cluster" 17 | -------------------------------------------------------------------------------- /deployment/scripts/sway-playground-delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | 6 | set -o allexport && source .env && set +o allexport 7 | 8 | if [ "${k8s_provider}" == "eks" ]; then 9 | echo "Updating your kube context locally ...." 10 | aws eks update-kubeconfig --name ${TF_VAR_eks_cluster_name} 11 | echo "Deleting sway-playground helm chart on ${TF_VAR_eks_cluster_name} ...." 12 | helm delete sway-playground \ 13 | --namespace ${k8s_namespace} \ 14 | --wait \ 15 | --timeout 8000s \ 16 | --debug 17 | else 18 | echo "You have inputted a non-supported kubernetes provider in your .env" 19 | fi 20 | -------------------------------------------------------------------------------- /deployment/scripts/sway-playground-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | 6 | set -o allexport && source .env && set +o allexport 7 | 8 | if [ "${k8s_provider}" == "eks" ]; then 9 | echo "Updating your kube context locally ...." 10 | aws eks update-kubeconfig --name ${TF_VAR_eks_cluster_name} 11 | cd ../charts 12 | mv values.yaml values.template 13 | envsubst < values.template > values.yaml 14 | rm values.template 15 | echo "Deploying sway-playground helm chart to ${TF_VAR_eks_cluster_name} ...." 16 | helm upgrade sway-playground . \ 17 | --values values.yaml \ 18 | --install \ 19 | --create-namespace \ 20 | --namespace=${k8s_namespace} \ 21 | --wait \ 22 | --timeout 8000s \ 23 | --debug 24 | else 25 | echo "You have inputted a non-supported kubernetes provider in your .env" 26 | fi 27 | -------------------------------------------------------------------------------- /deployment/scripts/sway-playground-ingress-delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | 6 | set -o allexport && source .env && set +o allexport 7 | 8 | if [ "${k8s_provider}" == "eks" ]; then 9 | echo "Updating your kube context locally ...." 10 | aws eks update-kubeconfig --name ${TF_VAR_eks_cluster_name} 11 | cd ../ingress/${k8s_provider} 12 | echo "Deleting fuel-core ingress on ${TF_VAR_eks_cluster_name} ...." 13 | kubectl delete -f fuel-ingress.yaml 14 | else 15 | echo "You have inputted a non-supported kubernetes provider in your .env" 16 | fi 17 | -------------------------------------------------------------------------------- /deployment/scripts/sway-playground-ingress-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | 6 | set -o allexport && source .env && set +o allexport 7 | 8 | if [ "${k8s_provider}" == "eks" ]; then 9 | echo " ...." 10 | echo "Updating your kube context locally ...." 11 | aws eks update-kubeconfig --name ${TF_VAR_eks_cluster_name} 12 | cd ../ingress/${k8s_provider} 13 | echo "Deploying sway-playground ingress to ${TF_VAR_eks_cluster_name} ...." 14 | mv sway-playground-ingress.yaml sway-playground-ingress.template 15 | envsubst < sway-playground-ingress.template > sway-playground-ingress.yaml 16 | rm sway-playground-ingress.template 17 | kubectl apply -f sway-playground-ingress.yaml 18 | else 19 | echo "You have inputted a non-supported kubernetes provider in your .env" 20 | fi 21 | -------------------------------------------------------------------------------- /helm/sway-playground/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/sway-playground/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: sway-playground 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.4 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.1.0" 25 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sway-playground.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sway-playground.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sway-playground.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sway-playground.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "sway-playground.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "sway-playground.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "sway-playground.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "sway-playground.labels" -}} 37 | helm.sh/chart: {{ include "sway-playground.chart" . }} 38 | {{ include "sway-playground.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "sway-playground.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "sway-playground.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "sway-playground.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "sway-playground.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "sway-playground.fullname" . }} 5 | labels: 6 | {{- include "sway-playground.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "sway-playground.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "sway-playground.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "sway-playground.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | envFrom: 37 | - secretRef: 38 | name: sway-playground 39 | ports: 40 | - name: http 41 | containerPort: {{ .Values.service.port }} 42 | protocol: TCP 43 | livenessProbe: 44 | httpGet: 45 | path: /health 46 | port: http 47 | initialDelaySeconds: 10 48 | periodSeconds: 5 49 | timeoutSeconds: 60 50 | env: 51 | - name: PORT 52 | value: {{ .Values.service.port | quote }} 53 | resources: 54 | {{- toYaml .Values.resources | nindent 12 }} 55 | {{- with .Values.nodeSelector }} 56 | nodeSelector: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.affinity }} 60 | affinity: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.tolerations }} 64 | tolerations: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "sway-playground.fullname" . }} 6 | labels: 7 | {{- include "sway-playground.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "sway-playground.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "sway-playground.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "sway-playground.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "sway-playground.fullname" . }} 5 | labels: 6 | {{- include "sway-playground.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "sway-playground.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "sway-playground.serviceAccountName" . }} 6 | labels: 7 | {{- include "sway-playground.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /helm/sway-playground/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "sway-playground.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "sway-playground.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "sway-playground.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /helm/sway-playground/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for sway-playground. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: ghcr.io/fuellabs/sway-playground 9 | pullPolicy: Always 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "latest" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: NodePort 41 | port: 8080 42 | 43 | ingress: 44 | enabled: false 45 | className: "" 46 | annotations: {} 47 | # kubernetes.io/ingress.class: nginx 48 | # kubernetes.io/tls-acme: "true" 49 | hosts: 50 | - host: chart-example.local 51 | paths: 52 | - path: / 53 | pathType: ImplementationSpecific 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: {} 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | autoscaling: 72 | enabled: true 73 | minReplicas: 1 74 | maxReplicas: 100 75 | targetCPUUtilizationPercentage: 80 76 | targetMemoryUtilizationPercentage: 60 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | -------------------------------------------------------------------------------- /projects/swaypad/Forc.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "standards" 3 | source = "git+https://github.com/FuelLabs/sway-standards?tag=v0.7.0#7d35df95e0b96dc8ad188ab169fbbeeac896aae8" 4 | dependencies = ["std"] 5 | 6 | [[package]] 7 | name = "std" 8 | source = "git+https://github.com/fuellabs/sway?tag=v0.67.0#d821dcb0c7edb1d6e2a772f5a1ccefe38902eaec" 9 | 10 | [[package]] 11 | name = "sway_libs" 12 | source = "git+https://github.com/FuelLabs/sway-libs?tag=v0.25.1#00569f811eae256a522c0e592522ea638815b362" 13 | dependencies = [ 14 | "standards", 15 | "std", 16 | ] 17 | 18 | [[package]] 19 | name = "swaypad" 20 | source = "member" 21 | dependencies = [ 22 | "standards", 23 | "std", 24 | "sway_libs", 25 | ] 26 | -------------------------------------------------------------------------------- /projects/swaypad/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs Inc."] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "swaypad" 6 | 7 | [dependencies] 8 | standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.7.0" } 9 | sway_libs = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.25.2" } 10 | -------------------------------------------------------------------------------- /projects/swaypad/src/main.sw: -------------------------------------------------------------------------------- 1 | contract; 2 | 3 | abi Counter { 4 | #[storage(read, write)] 5 | fn increment(amount: u64) -> u64; 6 | 7 | #[storage(read)] 8 | fn get() -> u64; 9 | } 10 | 11 | storage { 12 | counter: u64 = 0, 13 | } 14 | 15 | impl Counter for Contract { 16 | #[storage(read, write)] 17 | fn increment(amount: u64) -> u64 { 18 | let incremented = storage.counter.read() + amount; 19 | storage.counter.write(incremented); 20 | incremented 21 | } 22 | 23 | #[storage(read)] 24 | fn get() -> u64 { 25 | storage.counter.read() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/compilation/mod.rs: -------------------------------------------------------------------------------- 1 | mod swaypad; 2 | mod tooling; 3 | 4 | use self::{ 5 | swaypad::{create_project, remove_project, write_main_file}, 6 | tooling::{build_project, check_forc_version, switch_fuel_toolchain}, 7 | }; 8 | use crate::{ 9 | error::ApiError, 10 | types::CompileResponse, 11 | util::{clean_error_content, read_file_contents}, 12 | }; 13 | use hex::encode; 14 | use std::fs::read_to_string; 15 | 16 | const FILE_NAME: &str = "main.sw"; 17 | 18 | /// Build and destroy a project. 19 | pub fn build_and_destroy_project( 20 | contract: String, 21 | toolchain: String, 22 | ) -> Result { 23 | // Check if any contract has been submitted. 24 | if contract.is_empty() { 25 | return Ok(CompileResponse { 26 | abi: "".to_string(), 27 | bytecode: "".to_string(), 28 | storage_slots: "".to_string(), 29 | forc_version: "".to_string(), 30 | error: Some("No contract.".to_string()), 31 | }); 32 | } 33 | 34 | // Switch to the given fuel toolchain and check forc version. 35 | switch_fuel_toolchain(toolchain); 36 | let forc_version = check_forc_version(); 37 | 38 | // Create a new project. 39 | let project_name = 40 | create_project().map_err(|_| ApiError::Filesystem("create project".into()))?; 41 | 42 | // Write the file to the temporary project and compile. 43 | write_main_file(project_name.to_owned(), contract.as_bytes()) 44 | .map_err(|_| ApiError::Filesystem("write main file".into()))?; 45 | let output = build_project(project_name.clone()); 46 | 47 | // If the project compiled successfully, read the ABI and BIN files. 48 | if output.status.success() { 49 | let abi = read_to_string(format!( 50 | "projects/{}/out/debug/swaypad-abi.json", 51 | project_name 52 | )) 53 | .expect("Should have been able to read the file"); 54 | let bin = read_file_contents(format!("projects/{}/out/debug/swaypad.bin", project_name)); 55 | let storage_slots = read_file_contents(format!( 56 | "projects/{}/out/debug/swaypad-storage_slots.json", 57 | project_name 58 | )); 59 | 60 | // Remove the project directory and contents. 61 | remove_project(project_name).map_err(|_| ApiError::Filesystem("remove project".into()))?; 62 | 63 | // Return the abi, bin, empty error message, and forc version. 64 | Ok(CompileResponse { 65 | abi: clean_error_content(abi, FILE_NAME), 66 | bytecode: clean_error_content(encode(bin), FILE_NAME), 67 | storage_slots: String::from_utf8_lossy(&storage_slots).into(), 68 | forc_version, 69 | error: None, 70 | }) 71 | } else { 72 | // Get the error message presented in the console output. 73 | let error = std::str::from_utf8(&output.stderr).unwrap(); 74 | 75 | // Get the index of the main file. 76 | let main_index = error.find("/main.sw:").unwrap_or_default(); 77 | 78 | // Truncate the error message to only include the relevant content. 79 | let trunc = String::from(error).split_off(main_index); 80 | 81 | // Remove the project. 82 | remove_project(project_name).unwrap(); 83 | 84 | // Return an empty abi, bin, error message, and forc version. 85 | Ok(CompileResponse { 86 | abi: String::from(""), 87 | bytecode: String::from(""), 88 | storage_slots: String::from(""), 89 | error: Some(clean_error_content(trunc, FILE_NAME)), 90 | forc_version, 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/compilation/swaypad.rs: -------------------------------------------------------------------------------- 1 | use fs_extra::dir::{copy, CopyOptions}; 2 | use nanoid::nanoid; 3 | use std::fs::{create_dir_all, remove_dir_all, File}; 4 | use std::io::prelude::*; 5 | 6 | const PROJECTS: &str = "projects"; 7 | 8 | /// Copy the template project to a new project. 9 | pub fn create_project() -> Result { 10 | // Create a new project id. 11 | let project_name = nanoid!(); 12 | 13 | // Create a new directory for the project. 14 | create_dir_all(format!("{PROJECTS}/{project_name}"))?; 15 | 16 | // Setup the copy options for copying the template project to the new dir. 17 | let options = CopyOptions { 18 | overwrite: false, 19 | skip_exist: false, 20 | buffer_size: 64000, 21 | copy_inside: false, 22 | content_only: true, 23 | depth: 0, 24 | }; 25 | 26 | // Copy the template project over to the new directory. 27 | copy( 28 | format!("{PROJECTS}/swaypad"), 29 | format!("{PROJECTS}/{project_name}"), 30 | &options, 31 | )?; 32 | 33 | // Return the project id. 34 | Ok(project_name) 35 | } 36 | 37 | /// Remove a project from the projects dir. 38 | pub fn remove_project(project_name: String) -> std::io::Result<()> { 39 | remove_dir_all(format!("{PROJECTS}/{project_name}")) 40 | } 41 | 42 | /// Write the main sway file to a project. 43 | pub fn write_main_file(project_name: String, contract: &[u8]) -> std::io::Result<()> { 44 | let mut file = File::create(format!("{PROJECTS}/{project_name}/src/main.sw"))?; 45 | file.write_all(contract) 46 | } 47 | -------------------------------------------------------------------------------- /src/compilation/tooling.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Output}; 2 | 3 | use crate::util::spawn_and_wait; 4 | 5 | const FORC: &str = "forc"; 6 | const FUELUP: &str = "fuelup"; 7 | 8 | /// Switch to the given fuel toolchain. 9 | pub fn switch_fuel_toolchain(toolchain: String) { 10 | // Set the default toolchain to the one provided. 11 | let _ = spawn_and_wait(Command::new(FUELUP).arg("default").arg(toolchain)); 12 | } 13 | 14 | /// Check the version of forc. 15 | pub fn check_forc_version() -> String { 16 | let output = spawn_and_wait(Command::new(FORC).arg("--version")); 17 | std::str::from_utf8(&output.stdout).unwrap().to_string() 18 | } 19 | 20 | /// Use forc to build the project. 21 | pub fn build_project(project_name: String) -> Output { 22 | spawn_and_wait( 23 | Command::new(FORC) 24 | .arg("build") 25 | .arg("--path") 26 | .arg(format!("projects/{}", project_name)), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/cors.rs: -------------------------------------------------------------------------------- 1 | use rocket::fairing::{Fairing, Info, Kind}; 2 | use rocket::http::Header; 3 | use rocket::{Request, Response}; 4 | 5 | // Build an open cors module so this server can be used accross many locations on the web. 6 | pub struct Cors; 7 | 8 | // Build Cors Fairing. 9 | #[rocket::async_trait] 10 | impl Fairing for Cors { 11 | fn info(&self) -> Info { 12 | Info { 13 | name: "Cross-Origin-Resource-Sharing Fairing", 14 | kind: Kind::Response, 15 | } 16 | } 17 | 18 | // Build an Access-Control-Allow-Origin * policy Response header. 19 | async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) { 20 | response.set_header(Header::new("Access-Control-Allow-Origin", "*")); 21 | response.set_header(Header::new( 22 | "Access-Control-Allow-Methods", 23 | "POST, PATCH, PUT, DELETE, HEAD, OPTIONS, GET", 24 | )); 25 | response.set_header(Header::new("Access-Control-Allow-Headers", "*")); 26 | response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use rocket::{ 2 | http::Status, 3 | response::Responder, 4 | serde::{json::Json, Serialize}, 5 | Request, 6 | }; 7 | use thiserror::Error; 8 | 9 | /// A wrapper for API responses that can return errors. 10 | pub type ApiResult = Result, ApiError>; 11 | 12 | /// An empty response. 13 | #[derive(Serialize)] 14 | pub struct EmptyResponse; 15 | 16 | #[derive(Error, Debug)] 17 | pub enum ApiError { 18 | #[error("Filesystem error: {0}")] 19 | Filesystem(String), 20 | #[error("Charcoal error: {0}")] 21 | Charcoal(String), 22 | #[error("GitHub error: {0}")] 23 | Github(String), 24 | } 25 | 26 | impl<'r, 'o: 'r> Responder<'r, 'o> for ApiError { 27 | fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'o> { 28 | match self { 29 | ApiError::Filesystem(_) => Err(Status::InternalServerError), 30 | ApiError::Charcoal(_) => Err(Status::InternalServerError), 31 | ApiError::Github(_) => Err(Status::InternalServerError), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/gist.rs: -------------------------------------------------------------------------------- 1 | use octocrab::{models::gists::Gist, Octocrab}; 2 | 3 | use crate::{ 4 | error::ApiError, 5 | types::{ContractCode, GistMeta, GistResponse, Language, NewGistRequest}, 6 | }; 7 | 8 | const GIST_SWAY_FILENAME: &str = "playground.sw"; 9 | const GIST_SOLIDITY_FILENAME: &str = "playground.sol"; 10 | 11 | pub struct GistClient { 12 | octocrab: Octocrab, 13 | } 14 | 15 | impl GistClient { 16 | pub fn default() -> Self { 17 | // Do not throw an error if the token is not set. It is only needed in production and for testing 18 | // the "Share" feature. 19 | let gh_token = std::env::var("GITHUB_API_TOKEN").unwrap_or_default(); 20 | let octocrab = Octocrab::builder() 21 | .personal_token(gh_token) 22 | .build() 23 | .expect("octocrab builder"); 24 | Self { octocrab } 25 | } 26 | 27 | /// Creates a new gist. 28 | pub async fn create(&self, request: NewGistRequest) -> Result { 29 | let gist = self 30 | .octocrab 31 | .gists() 32 | .create() 33 | .file(GIST_SWAY_FILENAME, request.sway_contract.clone()) 34 | .file( 35 | GIST_SOLIDITY_FILENAME, 36 | request.transpile_contract.contract.clone(), 37 | ) 38 | .send() 39 | .await 40 | .map_err(|_| ApiError::Github("create gist".into()))?; 41 | 42 | Ok(GistMeta { 43 | id: gist.id, 44 | url: gist.html_url.to_string(), 45 | }) 46 | } 47 | 48 | /// Fetches a gist by ID. 49 | pub async fn get(&self, id: String) -> Result { 50 | let gist = self 51 | .octocrab 52 | .gists() 53 | .get(id) 54 | .await 55 | .map_err(|_| ApiError::Github("get gist".into()))?; 56 | 57 | let sway_contract = Self::extract_file_contents(&gist, GIST_SWAY_FILENAME); 58 | let solidity_contract = Self::extract_file_contents(&gist, GIST_SOLIDITY_FILENAME); 59 | 60 | Ok(GistResponse { 61 | gist: GistMeta { 62 | id: gist.id, 63 | url: gist.html_url.to_string(), 64 | }, 65 | sway_contract, 66 | transpile_contract: ContractCode { 67 | contract: solidity_contract, 68 | language: Language::Solidity, 69 | }, 70 | error: None, 71 | }) 72 | } 73 | 74 | fn extract_file_contents(gist: &Gist, filename: &str) -> String { 75 | gist.files 76 | .get(filename) 77 | .map(|file| file.content.clone()) 78 | .unwrap_or_default() 79 | .unwrap_or_default() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // We ignore this lint because clippy doesn't like the rocket macro for OPTIONS. 2 | #![allow(clippy::let_unit_value)] 3 | #[macro_use] 4 | extern crate rocket; 5 | 6 | mod compilation; 7 | mod cors; 8 | mod error; 9 | mod gist; 10 | mod transpilation; 11 | mod types; 12 | mod util; 13 | 14 | use crate::compilation::build_and_destroy_project; 15 | use crate::cors::Cors; 16 | use crate::error::ApiResult; 17 | use crate::gist::GistClient; 18 | use crate::types::{ 19 | CompileRequest, CompileResponse, GistResponse, Language, NewGistRequest, NewGistResponse, 20 | TranspileRequest, 21 | }; 22 | use crate::{transpilation::solidity_to_sway, types::TranspileResponse}; 23 | use rocket::serde::json::Json; 24 | use rocket::State; 25 | 26 | /// The endpoint to compile a Sway contract. 27 | #[post("/compile", data = "")] 28 | fn compile(request: Json) -> ApiResult { 29 | let response = 30 | build_and_destroy_project(request.contract.to_string(), request.toolchain.to_string())?; 31 | Ok(Json(response)) 32 | } 33 | 34 | /// The endpoint to transpile a contract written in another language into Sway. 35 | #[post("/transpile", data = "")] 36 | fn transpile(request: Json) -> ApiResult { 37 | let response = match request.contract_code.language { 38 | Language::Solidity => solidity_to_sway(request.contract_code.contract.to_string()), 39 | }?; 40 | Ok(Json(response)) 41 | } 42 | 43 | /// The endpoint to create a new gist to store the playground editors' code. 44 | #[post("/gist", data = "")] 45 | async fn new_gist( 46 | request: Json, 47 | gist: &State, 48 | ) -> ApiResult { 49 | let gist = gist.create(request.into_inner()).await?; 50 | Ok(Json(NewGistResponse { gist, error: None })) 51 | } 52 | 53 | /// The endpoint to fetch a gist. 54 | #[get("/gist/")] 55 | async fn get_gist(id: String, gist: &State) -> ApiResult { 56 | let gist_response = gist.get(id).await?; 57 | Ok(Json(gist_response)) 58 | } 59 | 60 | /// Catches all OPTION requests in order to get the CORS related Fairing triggered. 61 | #[options("/<_..>")] 62 | fn all_options() { 63 | // Intentionally left empty 64 | } 65 | 66 | // Indicates the service is running 67 | #[get("/health")] 68 | fn health() -> String { 69 | "true".to_string() 70 | } 71 | 72 | // Launch the rocket server. 73 | #[launch] 74 | fn rocket() -> _ { 75 | rocket::build() 76 | .manage(GistClient::default()) 77 | .attach(Cors) 78 | .mount( 79 | "/", 80 | routes![compile, transpile, new_gist, get_gist, all_options, health], 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/transpilation/mod.rs: -------------------------------------------------------------------------------- 1 | mod solidity; 2 | 3 | use self::solidity::run_charcoal; 4 | use crate::{error::ApiError, types::TranspileResponse, util::clean_error_content}; 5 | use nanoid::nanoid; 6 | use regex::Regex; 7 | use std::{ 8 | fs::{create_dir_all, remove_dir_all, File}, 9 | io::Write, 10 | path::PathBuf, 11 | }; 12 | 13 | const TMP: &str = "tmp"; 14 | const FILE_NAME: &str = "main.sol"; 15 | 16 | /// Transpile the given Solitiy contract to Sway. 17 | pub fn solidity_to_sway(contract: String) -> Result { 18 | if contract.is_empty() { 19 | return Ok(TranspileResponse { 20 | sway_contract: "".to_string(), 21 | error: Some("No contract.".to_string()), 22 | }); 23 | } 24 | 25 | let project_name = 26 | create_project(contract).map_err(|_| ApiError::Filesystem("create project".into()))?; 27 | 28 | // Run charcoal on the file and capture the output. 29 | let output = run_charcoal(contract_path(project_name.clone())); 30 | let response = if !output.stderr.is_empty() { 31 | let error: &str = 32 | std::str::from_utf8(&output.stderr).map_err(|e| ApiError::Charcoal(e.to_string()))?; 33 | TranspileResponse { 34 | sway_contract: "".to_string(), 35 | error: Some(clean_error_content(error.to_string(), FILE_NAME)), 36 | } 37 | } else if !output.stdout.is_empty() { 38 | let result = 39 | std::str::from_utf8(&output.stdout).map_err(|e| ApiError::Charcoal(e.to_string()))?; 40 | 41 | // Replace the generated comments from charcoal with a custom comment. 42 | let re = 43 | Regex::new(r"// Translated from.*").map_err(|e| ApiError::Charcoal(e.to_string()))?; 44 | let replacement = "// Transpiled from Solidity using charcoal. Generated code may be incorrect or unoptimal."; 45 | let sway_contract = re.replace_all(result, replacement).into_owned(); 46 | 47 | TranspileResponse { 48 | sway_contract, 49 | error: None, 50 | } 51 | } else { 52 | TranspileResponse { 53 | sway_contract: "".to_string(), 54 | error: Some( 55 | "An unknown error occurred while transpiling the Solidity contract.".to_string(), 56 | ), 57 | } 58 | }; 59 | 60 | // Delete the temporary file. 61 | if let Err(err) = remove_project(project_name.clone()) { 62 | return Ok(TranspileResponse { 63 | sway_contract: String::from(""), 64 | error: Some(format!("Failed to remove temporary file: {err}")), 65 | }); 66 | } 67 | 68 | Ok(response) 69 | } 70 | 71 | fn create_project(contract: String) -> std::io::Result { 72 | // Create a new project file. 73 | let project_name = nanoid!(); 74 | create_dir_all(project_path(project_name.clone()))?; 75 | let mut file = File::create(contract_path(project_name.clone()))?; 76 | 77 | // Write the contract to the file. 78 | file.write_all(contract.as_bytes())?; 79 | Ok(project_name) 80 | } 81 | 82 | fn remove_project(project_name: String) -> std::io::Result<()> { 83 | remove_dir_all(project_path(project_name)) 84 | } 85 | 86 | fn project_path(project_name: String) -> String { 87 | format!("{TMP}/{project_name}") 88 | } 89 | 90 | fn contract_path(project_name: String) -> PathBuf { 91 | PathBuf::from(format!("{}/{FILE_NAME}", project_path(project_name))) 92 | } 93 | -------------------------------------------------------------------------------- /src/transpilation/solidity.rs: -------------------------------------------------------------------------------- 1 | use crate::util::spawn_and_wait; 2 | use std::{ 3 | path::PathBuf, 4 | process::{Command, Output}, 5 | }; 6 | 7 | const CHARCOAL: &str = "charcoal"; 8 | 9 | /// Use forc to build the project. 10 | pub fn run_charcoal(path: PathBuf) -> Output { 11 | spawn_and_wait( 12 | Command::new(CHARCOAL) 13 | .arg("--target") 14 | .arg(path.to_string_lossy().to_string()), 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::{Deserialize, Serialize}; 2 | use std::fmt::{self}; 3 | 4 | #[derive(Serialize, Deserialize, Clone)] 5 | #[serde(rename_all = "camelCase")] 6 | pub enum Language { 7 | Solidity, 8 | } 9 | 10 | #[derive(Deserialize, Serialize)] 11 | #[serde(rename_all = "lowercase")] 12 | pub enum Toolchain { 13 | Latest, 14 | Nightly, 15 | Testnet, 16 | Mainnet, 17 | } 18 | 19 | impl fmt::Display for Toolchain { 20 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 21 | let s = match self { 22 | Toolchain::Latest => "latest", 23 | Toolchain::Nightly => "nightly", 24 | Toolchain::Testnet => "testnet", 25 | Toolchain::Mainnet => "mainnet", 26 | }; 27 | 28 | write!(formatter, "{}", s) 29 | } 30 | } 31 | 32 | /// The compile request. 33 | #[derive(Deserialize)] 34 | pub struct CompileRequest { 35 | pub contract: String, 36 | pub toolchain: Toolchain, 37 | } 38 | 39 | /// The response to a compile request. 40 | #[derive(Serialize)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct CompileResponse { 43 | pub abi: String, 44 | pub bytecode: String, 45 | pub storage_slots: String, 46 | pub forc_version: String, 47 | #[serde(skip_serializing_if = "Option::is_none")] 48 | pub error: Option, 49 | } 50 | 51 | /// A contract's code and its language. Used for contracts in languages other than Sway that can be transpiled. 52 | #[derive(Serialize, Deserialize, Clone)] 53 | pub struct ContractCode { 54 | pub contract: String, 55 | pub language: Language, 56 | } 57 | 58 | /// The transpile request. 59 | #[derive(Deserialize)] 60 | pub struct TranspileRequest { 61 | #[serde(flatten)] 62 | pub contract_code: ContractCode, 63 | } 64 | 65 | /// The response to a transpile request. 66 | #[derive(Serialize)] 67 | #[serde(rename_all = "camelCase")] 68 | pub struct TranspileResponse { 69 | pub sway_contract: String, 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub error: Option, 72 | } 73 | 74 | /// The new gist request. 75 | #[derive(Deserialize)] 76 | pub struct NewGistRequest { 77 | pub sway_contract: String, 78 | pub transpile_contract: ContractCode, 79 | } 80 | 81 | /// Information about a gist. 82 | #[derive(Serialize, Deserialize)] 83 | pub struct GistMeta { 84 | pub id: String, 85 | pub url: String, 86 | } 87 | 88 | /// The response to a new gist request. 89 | #[derive(Serialize)] 90 | #[serde(rename_all = "camelCase")] 91 | pub struct NewGistResponse { 92 | pub gist: GistMeta, 93 | #[serde(skip_serializing_if = "Option::is_none")] 94 | pub error: Option, 95 | } 96 | 97 | /// The response to a gist request. 98 | #[derive(Serialize)] 99 | #[serde(rename_all = "camelCase")] 100 | pub struct GistResponse { 101 | pub gist: GistMeta, 102 | pub sway_contract: String, 103 | pub transpile_contract: ContractCode, 104 | #[serde(skip_serializing_if = "Option::is_none")] 105 | pub error: Option, 106 | } 107 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::fs::File; 3 | use std::io::Read; 4 | use std::path::Path; 5 | use std::process::{Command, Output, Stdio}; 6 | 7 | /// Check the version of forc. 8 | pub fn spawn_and_wait(cmd: &mut Command) -> Output { 9 | // Pipe stdin, stdout, and stderr to the child. 10 | let child = cmd 11 | .stdin(Stdio::piped()) 12 | .stdout(Stdio::piped()) 13 | .stderr(Stdio::piped()) 14 | .spawn() 15 | .expect("failed to spawn command"); 16 | 17 | // Wait for the output. 18 | child 19 | .wait_with_output() 20 | .expect("failed to fetch command output") 21 | } 22 | 23 | /// Read a file from the IO. 24 | pub fn read_file_contents(file_name: String) -> Vec { 25 | // Declare the path to the file. 26 | let path = Path::new(&file_name); 27 | 28 | // If the path does not exist, return not found. 29 | if !path.exists() { 30 | return String::from("Not Found!").into(); 31 | } 32 | 33 | // Setup an empty vecotr of file content. 34 | let mut file_content = Vec::new(); 35 | 36 | // Open the file. 37 | let mut file = File::open(&file_name).expect("Unable to open file"); 38 | 39 | // Read the file's contents. 40 | file.read_to_end(&mut file_content).expect("Unable to read"); 41 | 42 | // Return the file's contents. 43 | file_content 44 | } 45 | 46 | /// This replaces the full file paths in error messages with just the file name. 47 | pub fn clean_error_content(content: String, filename: &str) -> std::string::String { 48 | let path_pattern = Regex::new(format!(r"(/).*(/{filename})").as_str()).unwrap(); 49 | 50 | path_pattern.replace_all(&content, filename).to_string() 51 | } 52 | --------------------------------------------------------------------------------