"} | ${"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 |
--------------------------------------------------------------------------------