diff --git a/docs/building-on-etherlink/development-toolkits.md b/docs/building-on-etherlink/development-toolkits.md index 66881f06..d7e8cb09 100644 --- a/docs/building-on-etherlink/development-toolkits.md +++ b/docs/building-on-etherlink/development-toolkits.md @@ -6,7 +6,7 @@ dependencies: ## đź‘· Hardhat -[Hardhat](https://hardhat.org/) is an Ethereum development environment for professionals. It facilitates performing frequent tasks, such as running tests, automatically checking code for mistakes or interacting with a smart contract. To get started, check out the [Hardhat documentation](https://hardhat.org/docs) or the [Etherlink tutorial](/tutorials/marketpulse). +[Hardhat](https://hardhat.org/) is an Ethereum development environment for professionals. It facilitates performing frequent tasks, such as running tests, automatically checking code for mistakes or interacting with a smart contract. To get started, check out the [Hardhat documentation](https://hardhat.org/docs) or the [Etherlink tutorial](/tutorials/predictionMarket). ### Using Hardhat with Etherlink diff --git a/docs/tutorials/marketpulse/backend.md b/docs/tutorials/marketpulse/backend.md deleted file mode 100644 index fbc44bfe..00000000 --- a/docs/tutorials/marketpulse/backend.md +++ /dev/null @@ -1,306 +0,0 @@ -# Create a smart contract - -Etherlink runs contracts like the Solidity language. -For more information about Solidity, see https://docs.soliditylang.org. - -Follow these steps to set up a Solidity smart contract: - -1. Remove the default Solidity smart contract files `Counter.sol` and `Counter.t.sol` in the `./contracts` folder. - -1. Create a new file named `./contracts/Marketpulse.sol` with the following content: - - ```Solidity - // SPDX-License-Identifier: MIT - pragma solidity ^0.8.24; - - // Use console.log for Hardhat debugging - import "hardhat/console.sol"; - - import "@openzeppelin/contracts/utils/math/Math.sol"; - - /** - * @title Marketpulse - * @author Benjamin Fuentes - */ - contract Marketpulse { - using Math for uint256; - - struct Bet { - uint256 id; - address payable owner; - string option; - uint256 amount; //wei - } - - enum BET_RESULT { - WIN, - DRAW, - PENDING - } - - uint256 public constant ODD_DECIMALS = 10; - uint256 public constant FEES = 10; // as PERCENTAGE unit (%) - - /** SLOTS */ - address payable public admin; - mapping(uint256 => Bet) public bets; - uint256[] public betKeys; - BET_RESULT public status = BET_RESULT.PENDING; - string public winner; - - event Pong(); - - constructor() payable { - admin = payable(msg.sender); - } - - /** - * Getter /setter - */ - function getBetKeys() public view returns (uint256[] memory) { - return betKeys; - } - - function getBets(uint256 betId) public view returns (Bet memory bet) { - return bets[betId]; - } - - /** Utility - * - * */ - - function addressToString( - address _addr - ) public pure returns (string memory) { - bytes memory alphabet = "0123456789abcdef"; - bytes20 value = bytes20(_addr); - bytes memory str = new bytes(42); - - str[0] = "0"; - str[1] = "x"; - - for (uint i = 0; i < 20; i++) { - str[2 + i * 2] = alphabet[uint(uint8(value[i] >> 4))]; - str[3 + i * 2] = alphabet[uint(uint8(value[i] & 0x0f))]; - } - - return string(str); - } - - /** - * Simple Ping - */ - function ping() public{ - console.log("Ping"); - emit Pong(); - } - - function generateBetId() private view returns (uint256) { - console.log("Calling generateBetId"); - return - uint256( - keccak256( - abi.encodePacked( - block.timestamp, - block.prevrandao, - msg.sender - ) - ) - ); - } - - /** - * place bets and returns the betId - */ - function bet( - string calldata selection, - uint256 odds - ) public payable returns (uint256) { - require(msg.value > 0, "Bet amount must be positive."); - require( - msg.value <= msg.sender.balance, - "Insufficient balance to place this bet." - ); - - uint256 betId = generateBetId(); - - bets[betId] = Bet({ - id: betId, - option: selection, - amount: msg.value, - owner: payable(msg.sender) - }); - betKeys.push(betId); - - console.log("Bet %d placed", betId); - - console.log( - "Bet placed: %d on %s at odds of %d", - msg.value, - selection, - odds - ); - return betId; - } - - /** - * - * @param option selected option - * @param betAmount (Optional: default is 0) if user want to know the output gain after putting some money on it. Otherwise it gives actual gain without betting and influencing odds calculation - * @return odds (in ODDS_DECIMAL unit) - */ - function calculateOdds( - string memory option, - uint256 betAmount //wei - ) public view returns (uint256) { - console.log( - "calculateOdds for option %s and bet amount is %d", - option, - betAmount - ); - - uint256 totalLoserAmount = 0; //wei - for (uint i = 0; i < betKeys.length; i++) { - Bet memory bet = bets[betKeys[i]]; - - if (keccak256(bytes(bet.option)) != keccak256(bytes(option))) { - (bool success, uint256 result) = totalLoserAmount.tryAdd( - bet.amount - ); - require(success, "Cannot add totalLoserAmount and bet.amount"); - totalLoserAmount = result; - } - } - console.log("totalLoserAmount: %d", totalLoserAmount); - - uint256 totalWinnerAmount = betAmount; //wei - for (uint i = 0; i < betKeys.length; i++) { - Bet memory bet = bets[betKeys[i]]; - - if (keccak256(bytes(bet.option)) == keccak256(bytes(option))) { - (bool success, uint256 result) = totalWinnerAmount.tryAdd( - bet.amount - ); - require(success, "Cannot add totalWinnerAmount and bet.amount"); - totalWinnerAmount = result; - } - } - console.log("totalWinnerAmount: %d", totalWinnerAmount); - uint256 part = Math.mulDiv( - totalLoserAmount, - 10 ** ODD_DECIMALS, - totalWinnerAmount - ); - - console.log("part per ODD_DECIMAL: %d", part); - - (bool success1, uint256 oddwithoutFees) = part.tryAdd( - 10 ** ODD_DECIMALS - ); - require(success1, "Cannot add part and 1"); - - console.log("oddwithoutFees: %d", oddwithoutFees); - - (bool success2, uint256 odd) = oddwithoutFees.trySub( - (FEES * 10 ** ODD_DECIMALS) / 100 - ); - require(success2, "Cannot remove fees from odd"); - - console.log("odd: %d", odd); - - return odd; - } - - function resolveResult( - string memory optionResult, - BET_RESULT result - ) public { - require( - msg.sender == admin, - string.concat( - "Only the admin ", - addressToString(admin), - " can give the result." - ) - ); - - require( - status == BET_RESULT.PENDING, - string( - abi.encodePacked( - "Result is already given and bets are resolved: ", - status - ) - ) - ); - - require( - result == BET_RESULT.WIN || result == BET_RESULT.DRAW, - "Only give winners or draw, no other choices" - ); - - for (uint i = 0; i < betKeys.length; i++) { - Bet memory bet = bets[betKeys[i]]; - if ( - result == BET_RESULT.WIN && - keccak256(bytes(bet.option)) == keccak256(bytes(optionResult)) - ) { - //WINNER! - uint256 earnings = Math.mulDiv( - bet.amount, - calculateOdds(bet.option, 0), - 10 ** ODD_DECIMALS - ); - console.log("earnings: %d for %s", earnings, bet.owner); - bet.owner.transfer(earnings); - winner = optionResult; - } else if (result == BET_RESULT.DRAW) { - //GIVE BACK MONEY - FEES - - uint256 feesAmount = Math.mulDiv(bet.amount, FEES, 100); - - (bool success, uint256 moneyBack) = bet.amount.trySub( - feesAmount - ); - - require(success, "Cannot sub fees amount from amount"); - - console.log( - "give back money: %d for %s", - moneyBack, - bet.owner - ); - - bet.owner.transfer(moneyBack); - } else { - //NEXT - console.log("bet lost for %s", bet.owner); - } - } - - status = result; - } - } - ``` - - This contract is a bet application where any user can place bets on a predefined poll by calling `bet`. - Each bet includes an ID, the address of the submitter, an option that represents their choice, and the bet amount in wei. - - The ID is randomly generated to showcase on the next advanced tutorial how to use an indexer to list all the bets for local odd calculation and use an oracle for randomization. - > Note: An optimized implementation would remove the bets themselves and keep only some aggregated variables, saving storage space and removing the need for an indexer. - - Users can place as many bets as they want. - When you are ready to resolve the bets, you can call `resolveResult` and make the contract pay the correct bets. - On the next advanced tutorial, an oracle is used to do this job instead of having to call the entrypoint manually. - - The contract calculates odds for the bet using safe math to avoid unexpected and dangerous behaviors. - -1. Compile the smart contract: - - ```bash - npx hardhat compile - ``` - - You can ignore any warnings in the console because they do not affect the application. - - If you see errors, make sure that your contract matches the contract in this repository: https://github.com/trilitech/tutorial-applications/tree/main/etherlink-marketpulse. diff --git a/docs/tutorials/marketpulse/cicd.md b/docs/tutorials/marketpulse/cicd.md deleted file mode 100644 index 7873ed75..00000000 --- a/docs/tutorials/marketpulse/cicd.md +++ /dev/null @@ -1,171 +0,0 @@ -# Set up a pipeline for the application - -There plenty of CICD tools on the market to build pipelines. -Here is an example of one using the Github configuration files and [Vercel](https://vercel.com/) for free web application hosting: - -1. From the root folder that contains the HardHat config file and the frontend `app` folder, create a Github pipeline: - - ```bash - mkdir .github - mkdir .github/workflows - touch .github/workflows/marketpulse.yml - ``` - -1. Edit the `.github/workflows/marketpulse.yml` file to create a CI/CD pipeline, as in this example: - - ```yml - name: CI - on: push - permissions: - contents: read - pages: write - id-token: write - concurrency: - group: "pages" - cancel-in-progress: false - jobs: - build-contract: - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v3 - - name: Use node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - run: npm ci - - run: DEPLOYER_PRIVATE_KEY=${{ secrets.DEPLOYER_PRIVATE_KEY }} npx hardhat compile - - run: DEPLOYER_PRIVATE_KEY=${{ secrets.DEPLOYER_PRIVATE_KEY }} npx hardhat test - - name: Cache build-hardhat-artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-build-hardhat-artifacts - path: artifacts - retention-days: 1 - deploy-contract: - needs: build-contract - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v3 - - name: Restore build-hardhat-artifacts - uses: actions/download-artifact@v4 - with: - name: ${{ runner.os }}-build-hardhat-artifacts - path: artifacts - - name: Use node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - run: npm ci - - run: yes | DEPLOYER_PRIVATE_KEY=${{ secrets.DEPLOYER_PRIVATE_KEY }} npx hardhat ignition deploy ignition/modules/Marketpulse.ts --verify --reset --network etherlinkShadownet - - name: Cache hardhat-ignition - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-deploy-hardhat-ignition - path: ignition - retention-days: 1 - build-app: - needs: deploy-contract - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v3 - - name: Restore hardhat-artifacts - uses: actions/download-artifact@v4 - with: - name: ${{ runner.os }}-build-hardhat-artifacts - path: artifacts - - name: Restore hardhat-ignition - uses: actions/download-artifact@v4 - with: - name: ${{ runner.os }}-deploy-hardhat-ignition - path: ignition - - name: Use node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - run: npm ci - working-directory: ./app - - run: more ./ignition/deployments/chain-127823/deployed_addresses.json - - run: npm run build - working-directory: ./app - - name: Cache app build - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-build-app-artifacts - path: ./app/dist - retention-days: 1 - deploy-app: - needs: build-app - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v3 - - name: Use node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - name: Install Vercel CLI - run: npm install -g vercel - - name: Link to Vercel - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - run: vercel link --yes --token=$VERCEL_TOKEN --cwd ./app --project marketpulse - - name: Restore hardhat-artifacts - uses: actions/download-artifact@v4 - with: - name: ${{ runner.os }}-build-hardhat-artifacts - path: artifacts - - name: Restore hardhat-ignition - uses: actions/download-artifact@v4 - with: - name: ${{ runner.os }}-deploy-hardhat-ignition - path: ignition - - name: Prepare build for Vercel - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - run: vercel build --prod --yes --token=$VERCEL_TOKEN --cwd=./app - - name: Deploy to Vercel - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - run: vercel deploy --prebuilt --prod --yes --token=$VERCEL_TOKEN --cwd=./app - ``` - - This pipeline includes several jobs that reproduce what you did manually in the tutorial: - - - `build-contract`: Build Solidity code with Hardhat - - `deploy-contract`: Deploy the contract with Hardhat ignition - - `build-app`: Build the app for production with Vite - - `deploy-app`: Use Vercel to link the project, prepare the deployment, and deploy it - -1. Push the project to GitHub. - -1. Set these variables in the GitHub pipeline configuration: - - - `DEPLOYER_PRIVATE_KEY`: The Etherlink account secret `private key` you need to use to deploy with Hardhat. This variable overrides the default environment variable mechanism of HardHat. - - `VERCEL_TOKEN`: Your personal Vercel token that you need to create on your Vercel account. For more information about configuring Vercel, see https://vercel.com/kb/guide/how-do-i-use-a-vercel-api-access-token. - - You can set these variables in two ways: - - - Use the [Github action extension for VSCode](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions) to set the variables from VSCode. - - - Set the variables manually in the GitHub project settings: - - 1. From the GitHub repository page, click **Settings > Secrets and variables > Actions**. - - 1. Under **Repository secrets**, click **New repository secret**. - - 1. Enter the name and value of the variable and click **Add secret**. - - You can see the variables on the **Actions secrets and variables** page at `https://github.com///settings/secrets/actions`, as in this example: - - ![The two secrets in the settings for GitHub actions for the repository](/img/tutorials/github-secrets.png) - -Now each time that you push your code, the GitHub action runs all the jobs, including compiling the contract, deploying it, and deploying the frontend app. When the run is finished you can follow the deployment on the Vercel deployment page (`https://vercel.com///deployments`) and the get the URL of your deployed application. - -The complete application is in this repository: https://github.com/trilitech/tutorial-applications/tree/main/etherlink-marketpulse. diff --git a/docs/tutorials/marketpulse/deploy.md b/docs/tutorials/marketpulse/deploy.md deleted file mode 100644 index b5e2a77d..00000000 --- a/docs/tutorials/marketpulse/deploy.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Deploy the contract -dependencies: - viem: 2.41.2 - hardhat: 3.0.17 ---- - -Deploy the contract locally is fine for doing simple tests, but we recommend to target the Etherlink Testnet to run complete scenarios as you may depend on other services like block explorers, oracles, etc. - -1. Deploy the contract locally with Hardhat: - - 1. Remove the default module for the ignition plugin of Hardhat. - This module is used as the default script for deployment: - - ```bash - rm ./ignition/modules/Counter.ts - ``` - - 1. Create a module to deploy your contract named `./ignition/modules/Marketpulse.ts` with the following content: - - ```TypeScript - // This setup uses Hardhat Ignition to manage smart contract deployments. - // Learn more about it at https://hardhat.org/ignition - - import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - - const MarketpulseModule = buildModule("MarketpulseModule", (m) => { - const MarketpulseContract = m.contract("Marketpulse", []); - - m.call(MarketpulseContract, "ping", []); - - return { MarketpulseContract }; - }); - - export default MarketpulseModule; - ``` - - This module deploys the contract and calls the Ping endpoint to verify that it deployed well. - - 1. Start a local Ethereum node: - - ```bash - npx hardhat node - ``` - - 1. In a different terminal window, within the same directory, deploy the contract using Hardhat ignition: - - ```bash - npx hardhat ignition deploy ignition/modules/Marketpulse.ts --reset --network localhost - ``` - - You can deploy the contract to any local Ethereum node but Etherlink is a good choice because it is persistent and free and most tools and indexers are already deployed on it. - - The response looks like this: - - ``` - Hardhat Ignition 🚀 - - Deploying [ MarketpulseModule ] - - Batch #1 - Executed MarketpulseModule#Marketpulse - - Batch #2 - Executed MarketpulseModule#Marketpulse.ping - - [ MarketpulseModule ] successfully deployed 🚀 - - Deployed Addresses - - MarketpulseModule#Marketpulse - 0x5FbDB2315678afecb367f032d93F642f64180aa3 - ``` - -1. Check that your deployment logs do not contain any error and stop the Hardhat node. - -1. Deploy the contract on Etherlink Shadownet Testnet: - - 1. In the Hardhat configuration file `hardhat.config.ts`, add Etherlink Mainnet and Shadownet Testnet as custom networks by replacing the default file with this code: - - ```TypeScript - import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem"; - import { configVariable, defineConfig } from "hardhat/config"; - - if (!configVariable("DEPLOYER_PRIVATE_KEY")) { - console.error("Missing env var DEPLOYER_PRIVATE_KEY"); - } - - const deployerPrivateKey = configVariable("DEPLOYER_PRIVATE_KEY"); - - export default defineConfig({ - plugins: [hardhatToolboxViemPlugin], - solidity: { - profiles: { - default: { - version: "0.8.28", - }, - production: { - version: "0.8.28", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - }, - }, - networks: { - hardhatMainnet: { - type: "edr-simulated", - chainType: "l1", - }, - hardhatOp: { - type: "edr-simulated", - chainType: "op", - }, - etherlinkMainnet: { - type: "http", - url: "https://node.mainnet.etherlink.com", - accounts: [deployerPrivateKey], - }, - etherlinkShadownet: { - type: "http", - url: "https://node.shadownet.etherlink.com", - accounts: [deployerPrivateKey], - }, - }, - chainDescriptors: { - 127823: { - chainType: "generic", - name: "etherlinkShadownet", - blockExplorers: { - etherscan: { - name: "EtherlinkExplorer", - apiUrl: "https://shadownet.explorer.etherlink.com/api", - url: "https://shadownet.explorer.etherlink.com", - }, - }, - }, - 42793: { - name: "EtherlinkMainnet", - } - }, - verify: { - blockscout: { - enabled: false, - }, - etherscan: { - apiKey: "DUMMY", - enabled: true, - }, - sourcify: { - enabled: false, - } - } - }); - ``` - - 1. Set up an Etherlink Shadownet Testnet account with some native tokens to deploy the contract. See [Using your wallet](/get-started/using-your-wallet) connect your wallet to Etherlink. Then use the faucet to get XTZ tokens on Etherlink Shadownet Testnet, as described in [Getting testnet tokens](/get-started/getting-testnet-tokens). - - 1. Retrieve your account private key, e.g. using your wallet. - - 1. Set the private key (represented in this example as ``) as the value of the `DEPLOYER_PRIVATE_KEY` environment variable by running this command: - - ```bash - export DEPLOYER_PRIVATE_KEY= - ``` - - 1. Deploy the contract to Etherlink Shadownet Testnet network specifying the `--network` option: - - ```bash - npx hardhat ignition deploy ignition/modules/Marketpulse.ts --network etherlinkShadownet - ``` - - A successful output should look like this: - - ```logs - Hardhat Ignition 🚀 - - Deploying [ MarketpulseModule ] - - Batch #1 - Executed MarketpulseModule#Marketpulse - - Batch #2 - Executed MarketpulseModule#Marketpulse.ping - - [ MarketpulseModule ] successfully deployed 🚀 - - Deployed Addresses - - MarketpulseModule#Marketpulse - 0xc64Bc334cf7a6b528357F8E88bbB3712E98629FF - ``` - -1. Run this command to verify your deployed contract, using the contract address as the value of ``: - - ```bash - npx hardhat verify --network etherlinkShadownet - ``` - - The response should include the message "Successfully verified contract Marketpulse on the block explorer" and a link to the block explorer. - - You can also pass the `--verify` option to the deployment command to verify the contract as part of the deployment process. - -The next step is to create the frontend application. diff --git a/docs/tutorials/marketpulse/frontend.md b/docs/tutorials/marketpulse/frontend.md deleted file mode 100644 index 8b565730..00000000 --- a/docs/tutorials/marketpulse/frontend.md +++ /dev/null @@ -1,918 +0,0 @@ ---- -title: Create the frontend application -dependencies: - vite: 6.0.1 - viem: 2.41.2 ---- - -1. Create a frontend app on the same project root directory. Here we use `Vite` and `React` to start a default project; - - ```bash - npm create vite@latest - ``` - -1. Choose a name for the frontend project (such as `app`, which is what the examples later use), select the `React` framework, and select the `Typescript` language. -If prompted, don't use the experimental version of Vite, and choose to not install and run immediately, because we have a few more settings to do below. - -1. Run these commands to install the dependencies: - - ```bash - cd app - npm install - ``` - -1. From the `./app` folder, download some sample images for the frontend application: - - ```bash - wget -O public/chiefs.png https://github.com/trilitech/tutorial-applications/raw/main/etherlink-marketpulse/app/public/chiefs.png - wget -O public/lions.png https://github.com/trilitech/tutorial-applications/raw/main/etherlink-marketpulse/app/public/lions.png - wget -O public/graph.png https://github.com/trilitech/tutorial-applications/raw/main/etherlink-marketpulse/app/public/graph.png - ``` - -1. Within your frontend `./app` project, import the `Viem` library for blockchain interactions, `thirdweb` for the wallet connection and `bignumber` for calculations on large numbers: - - ```bash - npm i viem thirdweb bignumber.js - ``` - -1. Add the `typechain` library to generate your contract structures and Typescript ABI classes from your ABI json file, that is your smart contract descriptor: - - ```bash - npm i -D typechain @typechain/ethers-v6 - ``` - -1. Add this line to the `scripts` section of the `./app/package.json` file in the frontend application: - - ```json - "postinstall": "cp ../ignition/deployments/chain-127823/deployed_addresses.json ./src && typechain --target=ethers-v6 --out-dir=./src/typechain-types --show-stack-traces ../artifacts/contracts/Marketpulse.sol/Marketpulse.json", - ``` - - This script copies the output address of the last deployed contract into your source files and calls `typechain` to generate types from the ABI file from the Hardhat folders. - -1. Run `npm i` to call the postinstall script automatically. You should see new files and folders in the `./src` folder of the frontend application. - -1. Create a utility file called `app/src/DecodeEvmTransactionLogsArgs.ts` to manage Viem errors (better than the technical defaults and not helpful ones), with this content: - - ```Typescript - import { - Abi, - BaseError, - ContractFunctionRevertedError, - decodeErrorResult, - } from "viem"; - - // Type-Safe Error Handling Interface - interface DetailedError { - type: "DecodedError" | "RawError" | "UnknownError"; - message: string; - details?: string; - errorData?: any; - } - - // Advanced Error Extraction Function - export function extractErrorDetails(error: unknown, abi: Abi): DetailedError { - // Type guard for BaseError - if (error instanceof BaseError) { - // Type guard for ContractFunctionRevertedError - if (error.walk() instanceof ContractFunctionRevertedError) { - try { - // Safe data extraction - const revertError = error.walk() as ContractFunctionRevertedError; - - // Extract error data safely - const errorData = (revertError as any).data; - - // Attempt to decode error - if (errorData) { - try { - // Generic error ABI for decoding - const errorAbi = abi; - - const decodedError = decodeErrorResult({ - abi: errorAbi, - data: errorData, - }); - - return { - type: "DecodedError", - message: decodedError.errorName || "Contract function reverted", - details: decodedError.args?.toString(), - errorData, - }; - } catch { - // Fallback if decoding fails - return { - type: "RawError", - message: "Could not decode error", - errorData, - }; - } - } - } catch (extractionError) { - // Fallback error extraction - return { - type: "UnknownError", - message: error.shortMessage || "Unknown contract error", - details: error.message, - }; - } - } - - // Generic BaseError handling - return { - type: "RawError", - message: error.shortMessage || "Base error occurred", - details: error.message, - }; - } - - // Fallback for non-BaseError - return { - type: "UnknownError", - message: "message" in (error as any) ? (error as any).message : String(error), - details: error instanceof Error ? error.message : undefined, - }; - } - - ``` - -1. Edit `./app/src/main.tsx` to add a `Thirdweb` provider around your application, by replacing its content with the one below. Then, replace on **line 8** the placeholder `` (including the delimiters `<` and `>`!) with your own `clientId` configured on the [Thirdweb dashboard here](https://portal.thirdweb.com/typescript/v4/getting-started#initialize-the-sdk): - - ```Typescript - import { createRoot } from "react-dom/client"; - import { createThirdwebClient } from "thirdweb"; - import { ThirdwebProvider } from "thirdweb/react"; - import App from "./App.tsx"; - import "./index.css"; - - const client = createThirdwebClient({ - clientId: "", - }); - - createRoot(document.getElementById("root")!).render( - - - - ); - ``` - - ThirdwebProvider encapsulates your application to inject account context and wrapped Viem functions - -1. Edit `./app/src/App.tsx` to have this code: - - ```TypeScript - import { Marketpulse, Marketpulse__factory } from "./typechain-types"; - - import BigNumber from "bignumber.js"; - import { useEffect, useState } from "react"; - import "./App.css"; - - import { - defineChain, - getContract, - prepareContractCall, - readContract, - sendTransaction, - ThirdwebClient, - waitForReceipt, - } from "thirdweb"; - import { ConnectButton, useActiveAccount } from "thirdweb/react"; - import { createWallet, inAppWallet } from "thirdweb/wallets"; - import { parseEther } from "viem"; - import { etherlinkShadownetTestnet } from "viem/chains"; - import { extractErrorDetails } from "./DecodeEvmTransactionLogsArgs"; - import CONTRACT_ADDRESS_JSON from "./deployed_addresses.json"; - - const wallets = [ - inAppWallet({ - auth: { - options: ["google", "email", "passkey", "phone"], - }, - }), - createWallet("io.metamask"), - createWallet("com.coinbase.wallet"), - createWallet("io.rabby"), - createWallet("com.trustwallet.app"), - createWallet("global.safe"), - ]; - - //copy pasta from Solidity code as Abi and Typechain does not export enum types - enum BET_RESULT { - WIN = 0, - DRAW = 1, - PENDING = 2, - } - - interface AppProps { - thirdwebClient: ThirdwebClient; - } - - export default function App({ thirdwebClient }: AppProps) { - console.log("*************App"); - - const marketPulseContract = { - abi: Marketpulse__factory.abi, - client: thirdwebClient, - chain: defineChain(etherlinkShadownetTestnet.id), - address: CONTRACT_ADDRESS_JSON["MarketpulseModule#Marketpulse"], - } - - const account = useActiveAccount(); - - const [options, setOptions] = useState>(new Map()); - - const [error, setError] = useState(""); - - const [status, setStatus] = useState(BET_RESULT.PENDING); - const [winner, setWinner] = useState(undefined); - const [fees, setFees] = useState(0); - const [betKeys, setBetKeys] = useState([]); - const [_bets, setBets] = useState([]); - - const reload = async () => { - if (!account?.address) { - console.log("No address..."); - } else { - const dataStatus = await readContract({ - contract: getContract(marketPulseContract), - method: "status", - params: [], - }); - - const dataWinner = await readContract({ - contract: getContract(marketPulseContract), - method: "winner", - params: [], - }); - - const dataFEES = await readContract({ - contract: getContract(marketPulseContract), - method: "FEES", - params: [], - }); - - const dataBetKeys = await readContract({ - contract: getContract(marketPulseContract), - method: "getBetKeys", - params: [], - }); - - setStatus(dataStatus as unknown as BET_RESULT); - setWinner(dataWinner as unknown as string); - setFees(Number(dataFEES as unknown as bigint) / 100); - setBetKeys(dataBetKeys as unknown as bigint[]); - - console.log( - "**********status, winner, fees, betKeys", - status, - winner, - fees, - betKeys - ); - } - }; - - //first call to load data - useEffect(() => { - (() => reload())(); - }, [account?.address]); - - //fetch bets - - useEffect(() => { - (async () => { - if (!betKeys || betKeys.length === 0) { - console.log("no dataBetKeys"); - setBets([]); - } else { - const bets = await Promise.all( - betKeys.map( - async (betKey) => - (await readContract({ - contract: getContract(marketPulseContract), - method: "getBets", - params: [betKey], - })) as unknown as Marketpulse.BetStruct - ) - ); - setBets(bets); - - //fetch options - let newOptions = new Map(); - setOptions(newOptions); - bets.forEach((bet) => { - if (newOptions.has(bet!.option)) { - newOptions.set( - bet!.option, - newOptions.get(bet!.option)! + bet!.amount - ); //acc - } else { - newOptions.set(bet!.option, bet!.amount); - } - }); - setOptions(newOptions); - console.log("options", newOptions); - } - })(); - }, [betKeys]); - - const Ping = () => { - // Comprehensive error handling - const handlePing = async () => { - try { - const preparedContractCall = await prepareContractCall({ - contract: getContract(marketPulseContract), - method: "ping", - params: [], - }); - - console.log("preparedContractCall", preparedContractCall); - - const transaction = await sendTransaction({ - transaction: preparedContractCall, - account: account!, - }); - - //wait for tx to be included on a block - const receipt = await waitForReceipt({ - client: thirdwebClient, - chain: defineChain(etherlinkShadownetTestnet.id), - transactionHash: transaction.transactionHash, - }); - - console.log("receipt:", receipt); - - setError(""); - } catch (error) { - const errorParsed = extractErrorDetails( - error, - Marketpulse__factory.abi - ); - setError(errorParsed.message); - } - }; - - return ( - - - {!error || error === "" ? <>🟢 : <>🔴} - - ); - }; - - const BetFunction = () => { - const [amount, setAmount] = useState(BigNumber(0)); //in Ether decimals - const [option, setOption] = useState("chiefs"); - - const runFunction = async () => { - try { - const contract = getContract(marketPulseContract); - - const preparedContractCall = await prepareContractCall({ - contract, - method: "bet", - params: [option, parseEther(amount.toString(10))], - value: parseEther(amount.toString(10)), - }); - - const transaction = await sendTransaction({ - transaction: preparedContractCall, - account: account!, - }); - - //wait for tx to be included on a block - const receipt = await waitForReceipt({ - client: thirdwebClient, - chain: defineChain(etherlinkShadownetTestnet.id), - transactionHash: transaction.transactionHash, - }); - - console.log("receipt:", receipt); - - await reload(); - - setError(""); - } catch (error) { - const errorParsed = extractErrorDetails( - error, - Marketpulse__factory.abi - ); - console.log("ERROR", error); - setError(errorParsed.message); - } - }; - - const calculateOdds = (option: string, amount?: bigint): BigNumber => { - //check option exists - if (!options.has(option)) return new BigNumber(0); - - console.log( - "actuel", - options && options.size > 0 - ? new BigNumber(options.get(option)!.toString()).toString() - : 0, - "total", - new BigNumber( - [...options.values()] - .reduce((acc, newValue) => acc + newValue, amount ? amount : 0n) - .toString() - ).toString() - ); - - return options && options.size > 0 - ? new BigNumber(options.get(option)!.toString(10)) - .plus( - amount ? new BigNumber(amount.toString(10)) : new BigNumber(0) - ) - .div( - new BigNumber( - [...options.values()] - .reduce( - (acc, newValue) => acc + newValue, - amount ? amount : 0n - ) - .toString(10) - ) - ) - .plus(1) - .minus(fees) - : new BigNumber(0); - }; - - return ( - - {status && status === BET_RESULT.PENDING ? ( - <> -

Choose team

- - -

Amount

- { - if (e.target.value && !isNaN(Number(e.target.value))) { - //console.log("e.target.value",e.target.value) - setAmount(new BigNumber(e.target.value)); - } - }} - /> - -
- {account?.address ? : ""} - - - - - - - - - - - - - -
Avg price (decimal) - {options && options.size > 0 - ? calculateOdds(option, parseEther(amount.toString(10))) - .toFixed(3) - .toString() - : 0} -
Potential return - XTZ{" "} - {amount - ? calculateOdds(option, parseEther(amount.toString(10))) - .multipliedBy(amount) - .toFixed(6) - .toString() - : 0}{" "} - ( - {options && options.size > 0 - ? calculateOdds(option, parseEther(amount.toString(10))) - .minus(new BigNumber(1)) - .multipliedBy(100) - .toFixed(2) - .toString() - : 0} - %) -
- - ) : ( - <> - - Outcome: {BET_RESULT[status]} - - {winner ?
{winner}
: ""} - - )} -
- ); - }; - - const resolve = async (option: string) => { - try { - const preparedContractCall = await prepareContractCall({ - contract: getContract(marketPulseContract), - method: "resolveResult", - params: [option, BET_RESULT.WIN], - }); - - console.log("preparedContractCall", preparedContractCall); - - const transaction = await sendTransaction({ - transaction: preparedContractCall, - account: account!, - }); - - //wait for tx to be included on a block - const receipt = await waitForReceipt({ - client: thirdwebClient, - chain: defineChain(etherlinkShadownetTestnet.id), - transactionHash: transaction.transactionHash, - }); - - console.log("receipt:", receipt); - - await reload(); - - setError(""); - } catch (error) { - const errorParsed = extractErrorDetails(error, Marketpulse__factory.abi); - setError(errorParsed.message); - } - }; - - return ( - <> -
- -

Market Pulse

- -
- -
-
-
- -
-
- -
- - - - - - - - - - - {options && options.size > 0 ? ( - [...options.entries()].map(([option, amount]) => ( - - - - - - - )) - ) : ( - <> - )} - -
Outcome% chanceaction
-
- -
- {option} -
- {new BigNumber(amount.toString()) - .div( - new BigNumber( - [...options.values()] - .reduce((acc, newValue) => acc + newValue, 0n) - .toString() - ) - ) - .multipliedBy(100) - .toFixed(2)} - % - - {status && status === BET_RESULT.PENDING ? ( - - ) : ( - "" - )} -
-
- -
- {} -
-
- -
-

Errors

- - - - {account?.address ? : ""} -
- - ); - } - ``` - - Explanations: - - - `import { Marketpulse, Marketpulse__factory } from "./typechain-types";`: Imports the contract ABI and contract structures - - `import CONTRACT_ADDRESS_JSON from "./deployed_addresses.json";`: Imports the address of the last deployed contract automatically - - `const wallets = [inAppWallet(...),createWallet(...)}`: Configures the Thirdweb wallet connection. Look at the [Thirdweb playground](https://playground.thirdweb.com/connect/sign-in/button?tab=code) and play with the generator. - - `useActiveAccount`: Uses Thirdweb React hooks and functions as a wrapper over the Viem library to get the active account. - - `const reload = async () => {`: Refreshes the smart contract storage (status, winner, fees and mapping keys). - - `useEffect...[betKeys]);`: React effect that reloads all bets from the storage when `betKeys` is updated. - - `const Ping = () => {`: Checks that the smart contract interaction works. It can be removed in production deployments. - - `const BetFunction = () => {`: Sends your bet to the smart contract, passing along the correct amount of XTZ. - - `const calculateOdds = (option: string, amount?: bigint): BigNumber => {`: Calculates the odds, similar to the onchain function in the smart contract. - -1. To fix the CSS for the page styling, replace the `./app/src/App.css` file with this code: - - ```css - #root { - margin: 0 auto; - padding: 2rem; - text-align: center; - - width: 100vw; - height: calc(100vh - 4rem); - } - - .logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; - } - - .logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); - } - - .logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); - } - - @keyframes logo-spin { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } - } - - @media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } - } - - header { - border-bottom: 1px solid #2c3f4f; - height: 100px; - } - - footer { - border-top: 1px solid #2c3f4f; - } - - hr { - color: #2c3f4f; - height: 1px; - } - - .tdTable { - align-items: center; - gap: 1rem; - width: 100%; - flex: 3 1 0%; - display: flex; - font-weight: bold; - } - - .picDiv { - height: 40px; - width: 40px; - min-width: 40px; - border-radius: 999px; - position: relative; - overflow: hidden; - } - - .card { - padding: 2em; - } - - .read-the-docs { - color: #888; - } - - h1 { - margin: unset; - } - ``` - -1. Replace the `./app/src/index.css` file with this code: - - ```css - :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #1D2B39; - - - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; - } - - a:hover { - color: #535bf2; - } - - body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; - - } - - h1 { - font-size: 3.2em; - line-height: 1.1; - } - - button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #2D9CDB; - cursor: pointer; - transition: border-color 0.25s; - } - - button:hover { - border-color: #646cff; - } - - button:focus, - button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; - } - - select { - width: inherit; - font-size: 0.875rem; - color: #858D92; - border-color: #344452; - transition: color 0.2s; - text-align: center; - border-width: 1px; - border-style: solid; - align-self: center; - padding: 1rem 1rem; - background: #1D2B39; - outline: none; - outline-color: currentcolor; - outline-style: none; - outline-width: medium; - border-radius: 8px; - } - - input { - width: calc(100% - 35px); - font-size: 0.875rem; - color: #858D92; - border-color: #344452; - transition: color 0.2s; - text-align: center; - border-width: 1px; - border-style: solid; - align-self: center; - padding: 1rem 1rem; - background: #1D2B39; - outline: none; - outline-color: currentcolor; - outline-style: none; - outline-width: medium; - border-radius: 8px; - } - - @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } - } - ``` - -1. Edit the file `app/tsconfig.app.json` to set both "verbatimModuleSyntax" and "erasableSyntaxOnly" to "false". - -1. Build and run the application: - - ```bash - npm run build - npm run dev - ``` - -1. In a web browser, click the **Connect** button to login with your wallet. - -1. Click the **Ping** button at the bottom. It should stay green if you can interact with your smart contract with no error messages. - -1. Run a betting scenario: - - 1. Select **Chiefs** on the select box on the right corner, choose a small amount like **0.00001 XTZ**, and click the **Bet** button. - - 1. Confirm the transaction in your wallet. - If you don't have enough XTZ in your account, the application shows an `OutOfFund` error. - - 1. Optional: Disconnect and connect with another account in your wallet. - You can also use the same account to make a second bet. - - 1. Select **Lions** on the select box on the right corner, choose a small amount like **0.00001 XTZ**, and click the **Bet** button. - - 1. Confirm the transaction in your wallet. - - Both teams have 50% of chance to win. Note: Default platform fees have been set to 10%, and the odds calculation takes those fees into account. - - 1. Connect as the account that you deployed the contract with (the admin) and click one of the **Winner** buttons to resolve the poll. - - The page's right-hand corner refreshes and displays the winner of the poll and the application automatically pays the winning bets. - - 1. Find your transaction `resolveResult` on the Etherlink Shadownet Testnet explorer at https://shadownet.explorer.etherlink.com. - On the **Transaction details > Internal txns tab**, you should see, if you won something, the expected amount transferred to you from the smart contract address. diff --git a/docs/tutorials/marketpulse/index.md b/docs/tutorials/marketpulse/index.md deleted file mode 100644 index 45d35101..00000000 --- a/docs/tutorials/marketpulse/index.md +++ /dev/null @@ -1,33 +0,0 @@ -# Introduction - -This guide is an Etherlink tutorial to create a real application from scratch for a betting platform. -It allows users to bet cryptocurrency on the outcome of Super Bowl Championship as a way of demonstrating how to deploy and run dApps on the Etherlink platform. - -![](/img/tutorials/screen.png) - -## Learning objectives - -In this tutorial, you will learn: - -- How to set up a development environment with Hardhat -- How to write a simple smart contract in Solidity -- How to create an Etherlink account and get Testnet tokens using the faucet -- How to test a smart contract locally -- How to use Hardhat to deploy the contract to Etherlink -- How to verify your contract on the Etherlink explorer -- How to create a frontend application to interact with the smart contract using Viem and Thirdweb -- How to design a CI/CD pipeline with Github Actions and deploy the frontend with Vercel - -## Not included - -This tutorial is not intended to be a complete course for developing smart contracts with Solidity or Hardhat or for developing frontend applications; the smart contract and frontend application in this tutorial are just examples. -Also, the tools in this tutorial are only examples of tools that you can use with Etherlink. -You can use many different smart contract and frontend development tools and technologies with Etherlink. - -## Disclaimer - -The code is for education purposes and hasn’t been audited or optimized. You shouldn’t use this code in production. - -## Application source code - -You can see the source code for the completed application here: https://github.com/trilitech/tutorial-applications/tree/main/etherlink-marketpulse. diff --git a/docs/tutorials/marketpulse/setup.md b/docs/tutorials/marketpulse/setup.md deleted file mode 100644 index ad8d5167..00000000 --- a/docs/tutorials/marketpulse/setup.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Set up a development environment for Etherlink -dependencies: - hardhat: 3.0.17 ---- - -> Etherlink is 100% compatible with Ethereum technology, which means that you can use any Ethereum-compatible tool for development, including Hardhat, Foundry, Truffle Suite, and Remix IDE. -> For more information on tools that work with Etherlink, see [Developer toolkits](https://docs.etherlink.com/building-on-etherlink/development-toolkits) in the Etherlink documentation. - -In this tutorial, you use [Hardhat](https://hardhat.org/tutorial/creating-a-new-hardhat-project) to manage development tasks such as compiling and testing smart contracts. -You also use Viem, which is a lightweight, type-safe Ethereum library for JavaScript/TypeScript. -It provides low-level, efficient blockchain interactions with minimal abstraction. - -1. Install Node.JS version 22 or later, which is required for Hardhat. - -1. [Install npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). - -1. Initialize an Node project with NPM in a fresh directory: - - ```bash - npm init -y - npm install -D typescript @types/node ts-node chai @types/chai - ``` - -1. Install Hardhat and initialize it: - - ```bash - npm install -D hardhat - npx hardhat --init - ``` - -1. Follow these steps in the Hardhat prompts: - - 1. In the Hardhat prompts, select version 3 of Hardhat and `.` as the relative path to the project. - - 1. In the prompt for the type of project to create, select `A TypeScript Hardhat project using Node Test Runner and Viem`. - - 1. Select `true` or `Y` to convert the project's `package.json` file to ESM. - - 1. At the prompt to install dependencies, select `true` or `Y`. - - 1. If Hardhat prompts you to update TypeScript dependencies, select `true` or `Y`. - -1. Install `@openzeppelin/contracts` to use the Math library for safe calculations: - - ```bash - npm i @openzeppelin/contracts - ``` - -1. Install dev libraries for verifying your smart contract: - - ```bash - npm i -D @nomicfoundation/hardhat-verify - ``` - - Verify is a feature that verifies contracts on an Ethereum block explorer by checking the compiled code against the source code. - Verifying your contracts provides source code transparency and a source reference for some tools to generate utility code. - -1. (Optional) If you are using VsCode for development, install the Hardhat/Solidity plugin from Nomic: [Solidity plugin for VsCode](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) diff --git a/docs/tutorials/marketpulse/test.md b/docs/tutorials/marketpulse/test.md deleted file mode 100644 index 1be5402d..00000000 --- a/docs/tutorials/marketpulse/test.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -title: Test the contract -dependencies: - viem: 2.41.2 - hardhat: 3.0.17 ---- - -With blockchain development, testing is very important because you don't have the luxury to redeploy application updates as it. Hardhat provides you smart contract helpers on `chai` Testing framework to do so. - -1. Remove the default `./test/Counter.ts` test file: - - ```bash - rm ./test/Counter.ts - ``` - -1. Create a test file named `./test/Marketpulse.ts` with the following content: - - ```TypeScript - import { expect } from "chai"; - import hre from "hardhat"; - import { ContractFunctionExecutionError, parseEther } from "viem"; - import { describe, it } from "node:test"; - const { viem, networkHelpers } = await hre.network.connect(); - - //constants and local variables - const ODD_DECIMALS = 10; - let initAliceAmount = 0n; - let initBobAmount = 0n; - - //Enum definition copy/pasta from Solidity code - enum BET_RESULT { - WIN = 0, - DRAW = 1, - PENDING = 2, - } - - describe("Marketpulse", function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployContractFixture() { - // Contracts are deployed using the first signer/account by default - const [owner, bob] = await viem.getWalletClients(); - - // Set block base fee to zero because we want exact calculation checks without network fees - await networkHelpers.setNextBlockBaseFeePerGas("0x0"); - - const marketpulseContract = await viem.deployContract( - "Marketpulse", - [] - ); - - const publicClient = await viem.getPublicClient(); - - initAliceAmount = await publicClient.getBalance({ - address: owner.account.address, - }); - - initBobAmount = await publicClient.getBalance({ - address: bob.account.address, - }); - - return { - marketpulseContract, - owner, - bob, - publicClient, - }; - } - - describe("init function", function () { - it("should be initialized", async function () { - const { marketpulseContract, owner } = await networkHelpers.loadFixture( - deployContractFixture - ); - - const ownerFromStorage = await marketpulseContract.read.admin(); - console.log("ownerFromStorage", ownerFromStorage); - expect(ownerFromStorage.toLowerCase()).to.equal( - owner.account.address.toLowerCase() - ); //trick to remove capital letters - }); - - it("should return Pong", async function () { - const { marketpulseContract, publicClient } = await networkHelpers.loadFixture( - deployContractFixture - ); - - await marketpulseContract.write.ping({ gasPrice: 0n }); - - const logs = await publicClient.getContractEvents({ - abi: marketpulseContract.abi, - eventName: "Pong", - }); - console.log(logs); - expect(logs.length).to.equal(1); - }); - }); - - // BET SCENARIO - - //full scenario should be contained inside the same 'it' , otherwise the full context is reset - describe("scenario", () => { - let betChiefs1Id: bigint = BigInt(0); - let betLions2Id: string = ""; - let betKeys: bigint[] = []; - - it("should run the full scenario correctly", async () => { - console.log("Initialization should return a list of empty bets"); - const { - marketpulseContract, - owner: alice, - publicClient, - bob, - } = await networkHelpers.loadFixture(deployContractFixture); - - expect(await marketpulseContract.read.betKeys.length).to.equal(0); - - console.log("Chiefs bet for 1 ether should return a hash"); - - const betChiefs1IdHash = await marketpulseContract.write.bet( - ["chiefs", parseEther("1")], - { value: parseEther("1"), gasPrice: 0n } - ); - expect(betChiefs1IdHash).not.null; - - // Wait for the transaction receipt - let receipt = await publicClient.waitForTransactionReceipt({ - hash: betChiefs1IdHash, - }); - - expect(receipt.status).equals("success"); - - betKeys = [...(await marketpulseContract.read.getBetKeys())]; - console.log("betKeys", betKeys); - - betChiefs1Id = betKeys[0]; - - console.log("Should find the Chiefs bet from hash"); - - const betChiefs1 = await marketpulseContract.read.getBets([betChiefs1Id]); - - expect(betChiefs1).not.null; - - expect(betChiefs1.owner.toLowerCase()).equals( - alice.account.address.toLowerCase() - ); - expect(betChiefs1.option).equals("chiefs"); - expect(betChiefs1.amount).equals(parseEther("1")); - - console.log("Should get a correct odd of 0.9 (including fees) for Chiefs if we bet 1"); - - let odd = await marketpulseContract.read.calculateOdds([ - "chiefs", - parseEther("1"), - ]); - - expect(odd).equals(BigInt(Math.floor(0.9 * 10 ** ODD_DECIMALS))); //rounding - - console.log("Lions bet for 2 ethers should return a hash"); - - // Set block base fee to zero - await networkHelpers.setNextBlockBaseFeePerGas("0x0"); - - const betLions2IdHash = await marketpulseContract.write.bet( - ["lions", parseEther("2")], - { value: parseEther("2"), account: bob.account.address, gasPrice: 0n } - ); - expect(betLions2IdHash).not.null; - - // Wait for the transaction receipt - receipt = await publicClient.waitForTransactionReceipt({ - hash: betLions2IdHash, - }); - - expect(receipt.status).equals("success"); - - betKeys = [...(await marketpulseContract.read.getBetKeys())]; - console.log("betKeys", betKeys); - - const betLions2Id = betKeys[1]; - - console.log("Should find the Lions bet from hash"); - - const betLions2 = await marketpulseContract.read.getBets([ - betLions2Id, - ]); - - expect(betLions2).not.null; - - expect(betLions2.owner.toLowerCase()).equals( - bob.account.address.toLowerCase() - ); - expect(betLions2.option).equals("lions"); - expect(betLions2.amount).equals(parseEther("2")); - - console.log("Should get a correct odd of 1.9 for Chiefs (including fees) if we bet 1"); - - odd = await marketpulseContract.read.calculateOdds([ - "chiefs", - parseEther("1"), - ]); - - expect(odd).equals(BigInt(Math.floor(1.9 * 10 ** ODD_DECIMALS))); - - console.log( - "Should get a correct odd of 1.23333 for lions (including fees) if we bet 1" - ); - - odd = await marketpulseContract.read.calculateOdds([ - "lions", - parseEther("1"), - ]); - - expect(odd).equals( - BigInt(Math.floor((1 + 1 / 3 - 0.1) * 10 ** ODD_DECIMALS)) - ); - - console.log("Should return all correct balances after resolving Win on Chiefs"); - - await marketpulseContract.write.resolveResult( - ["chiefs", BET_RESULT.WIN], - { gasPrice: 0n } - ); - - expect( - await publicClient.getBalance({ - address: marketpulseContract.address, - }) - ).equals(parseEther("0.1")); - expect( - await publicClient.getBalance({ address: alice.account.address }) - ).equals(initAliceAmount + parseEther("1.9")); // -1+2.9 - - expect( - await publicClient.getBalance({ address: bob.account.address }) - ).equals(initBobAmount - parseEther("2")); //-2 - - console.log("Should have state finalized after resolution"); - - const result = await marketpulseContract.read.status(); - expect(result).not.null; - expect(result).equals(BET_RESULT.WIN); - - console.log("Should return an error if we try to resolve results again"); - - try { - await marketpulseContract.write.resolveResult( - ["chiefs", BET_RESULT.WIN], - { gasPrice: 0n } - ); - } catch (e) { - expect((e as ContractFunctionExecutionError).details).contains( - "VM Exception while processing transaction: reverted with reason string 'Result is already given and bets are resolved" - ); - } - }); - }); - }); - ``` - -1. Run the tests with Hardhat: - - ```bash - npx hardhat test - ``` - - The technical Ping test and the full end2end scenario should pass. - If you see errors, make sure that your application matches the application in this repository: https://github.com/trilitech/tutorial-applications/tree/main/etherlink-marketpulse. diff --git a/docs/tutorials/predictionMarket/deploy-contract.md b/docs/tutorials/predictionMarket/deploy-contract.md new file mode 100644 index 00000000..35ca2537 --- /dev/null +++ b/docs/tutorials/predictionMarket/deploy-contract.md @@ -0,0 +1,102 @@ +--- +title: "Part 2: Deploying the contract" +--- + +In this section you deploy the contract to the Etherlink Shadownet Testnet, where you can test it without working with real funds. + +1. In hte `backend` project, create a file named `scripts/deploy.js` with this code: + + ```javascript + // scripts/deploy.js + const hre = require("hardhat"); + + async function main() { + // Get the deployer account + const [deployer] = await hre.ethers.getSigners(); + console.log("Deploying contract with account:", deployer.address); + + // Compile & get the contract factory + const MyContract = await ethers.getContractFactory("PredictxtzContract"); + + // Deploy the contract + const DeployedContract = await MyContract.deploy(); + await DeployedContract.deployed(); + + console.log("Contract deployed to:", DeployedContract.address); + } + + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error("Deployment failed:", error); + process.exit(1); + }); + ``` + + This script deploys the compiled contract. + The account that deploys it automatically becomes the administrator account. + +1. Using the [Etherlink Shadownet faucet](https://shadownet.faucet.etherlink.com/), get some XTZ in your account to pay for transaction fees. + +1. Run this command to deploy the contract to Shadownet: + + ```bash + npx hardhat run scripts/deploy.js --network etherlink + ``` + + If deployment is successful, Hardhat prints the address of your account and the address of the deployed contract to the console, as in this example: + + ``` + Deploying contract with account: 0x45Ff91b4bF16aC9907CF4A11436f9Ce61BE0650d + Contract deployed to: 0xCC276f21e018aD59ee1b91C430AFfeF0147f9C91 + ``` + + If the command failed, check your contract and deployment files and run the compilation and deployment commands again. + +1. Copy the address of the deployed contract and look it up on the [Etherlink Shadownet block explorer](https://shadownet.explorer.etherlink.com/). + +In the block explorer you can see the creator of the contract and information about its transactions. + +The deployed contract in the block explorer, showing only the origination transaction + +## Interacting with the contract + +To test the contract, you can interact with it directly on the block explorer. +However, you must add the ABI of the contract so the block explorer can format transactions correctly. +The ABI is the complete interface for the contract, including all of its public functions and events. +It is generated during the compilation process. + +1. Log in to the Etherlink [block explorer](https://shadownet.explorer.etherlink.com/) with your wallet by clicking `Log in`, clicking `Continue with Web3 wallet`, and connecting your wallet. + +1. Upload the contract ABI: + + 1. Copy the ABI of the compiled contract by opening the `artifacts/contracts/Contract.sol/PredictxtzContract.json` file and copying the value of the `abi` field, which is an array. + + 1. On the block explorer, go to the contract, go to the **Contract** tab, click **Custom ABI**, and click **Add custom ABI**. + + 1. In the pop-up window, give the contract the name `PredictxtzContract` and paste the ABI array, as in this picture: + + Adding a name and custom ABI for the contract + + 1. Click **Create custom ABI**. + + Now the **Contract > Custom ABI** tab shows the functions in the contract: + + The list of functions in the contract based on the ABI you uploaded + +1. Create the first prediction market in the contract: + + 1. Expand the `CreateMarket` function. + + 1. In the **Question** field, put the question for the prediction market, such as "Will it rain one week from today?" + + 1. In the **Duration** field, enter `604 800` to represent one week in seconds. + + Setting the parameters for the function + + 1. Click **Write**. + + 1. Approve the transaction in your wallet and wait for it to be confirmed. + +Now the contract has an active prediction market and users can make bets. +Continue to [Part 3: Setting up the frontend](/tutorials/predictionMarket/frontend). diff --git a/docs/tutorials/predictionMarket/frontend.md b/docs/tutorials/predictionMarket/frontend.md new file mode 100644 index 00000000..09c0de60 --- /dev/null +++ b/docs/tutorials/predictionMarket/frontend.md @@ -0,0 +1,902 @@ +--- +title: "Part 3: Setting up the frontend" +--- + +The frontend application uses the Thirdweb client SDK to interact with the smart contract. +dApp users can connect their wallets, place bets, and claim winnings. + +To use the client SDK, you’ll need a ThirdWeb client ID, which allows you to send transactions to Etherlink with the ThirdWeb SDK. + +The starter frontend project is in the folder `tutorial-applications/etherlink-prediction/starter/frontend`. + +## Creating the frontend application + +1. Get a ThirdWeb client ID by going to https://thirdweb.com/create-api-key. + +1. In the `tutorial-applications/etherlink-prediction/starter/frontend` folder, create an `.env` file by copying the `.env.example` file: + + ```bash + cp .env.example .env + ``` + +1. Update the `.env` file by setting your ThirdWeb client ID as the value of the `NEXT_PUBLIC_THIRDWEB_CLIENT_ID` variable and the address of the deployed contract as the value of the `NEXT_PUBLIC_CONTRACT_ADDRESS` variable. + +1. Run this command to install the dependencies for the frontend application: + + ```bash + npm install + ``` + +1. In the `lib` folder, create a file named `contract-utils.ts` and add this code: + + ```javascript + //lib/contract-utils.ts + + import { getContract } from "thirdweb"; + import { etherlinkTestnet } from "thirdweb/chains"; + import { client } from "./providers"; + + const abi = [YOUR_CONTRACT_ABI] + + export const contract = getContract({ + client, // Your ThirdWeb client + address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS!, // Your contract address + chain: etherlinkTestnet, + abi, + }); + ``` + +1. Replace the `YOUR_CONTRACT_ABI` variable with the same ABI that you pasted into the block explorer. + +1. Replace the `app/page.tsx` file with this code: + + ```javascript + "use client"; + + import { useState, useEffect } from "react"; + import { Sidebar } from "@/components/sidebar"; + import { MarketGrid } from "@/components/market-grid"; + import { + ConnectButton, + useSendAndConfirmTransaction, + useReadContract, + } from "thirdweb/react"; + import { marketIds } from "../lib/utils"; + import {client} from '../lib/providers' + import { contract } from "@/lib/contract-utils"; + import { prepareContractCall } from "thirdweb"; + import { toWei } from "thirdweb/utils"; + import toast, { Toaster } from "react-hot-toast"; + + export default function HomePage() { + const [selectedCategory, setSelectedCategory] = useState("All"); + const [existingMarketIds, setExistingMarketIds] = useState([]); + + // Fetch the number of markets from the contract + const { data: marketCounter } = useReadContract({ + contract: contract, + method: "marketCounter", + params: [], + }); + + const { mutateAsync: mutateTransaction } = useSendAndConfirmTransaction(); + + useEffect(() => { + // Update existingMarketIds when marketCounter changes + if (marketCounter) { + setExistingMarketIds(marketIds(marketCounter)); + } + }, [marketCounter]); + + // place a bet + const handlePlaceBet = async ( + marketId: number, + side: "yes" | "no", + betAmount: number + ) => { + const isYes = side === "yes" ? true : false; + const marketIdBigInt = BigInt(marketId); + const betAmountWei = toWei(betAmount.toString()); + + // Prepare and send the transaction to place a bet + const transaction = prepareContractCall({ + contract, + method: "function placeBet(uint256 marketId, bool isYes)", + params: [marketIdBigInt, isYes], + value: betAmountWei, // Attach the bet amount as value + }); + + try { + const result = await mutateTransaction(transaction); + console.log({ result }); + } catch (error) { + console.log({ error }); + toast.error("Market not active."); + } + }; + + const claimWinnings = async (marketId: number) => { + console.log("Claiming winnings for market ID:", marketId); + // Prepare and send the transaction to claim winnings + const marketIdBigInt = BigInt(marketId); + const transaction = prepareContractCall({ + contract, + method: "function claimWinnings(uint256 marketId)", + params: [marketIdBigInt], + }); + + try { + const result = await mutateTransaction(transaction); + console.log("Winnings claimed", result); + toast.success("Congrats on your winnings!"); + } catch (error) { + toast.error("Winnings have been claimed."); + } + }; + + const resolveMarket = async (marketId: number, winner: string) => { + console.log("resolve market for market ID:", marketId); + // Prepare and send the transaction to claim winnings + const marketIdBigInt = BigInt(marketId); + const winnerInt = Number(winner); + const transaction = prepareContractCall({ + contract, + method: "function resolveMarket(uint256 marketId, uint8 winner)", + params: [marketIdBigInt, winnerInt], + }); + try { + await mutateTransaction(transaction); + } catch (error) { + toast.error("Market has been resolved."); + } + }; + + return ( +
+ + +
+
+
+

Markets

+

+ Show you're an expert. Trade on the outcomes of future events. +

+
+ +
+ +
+
+ + {/* pass the array of existing market IDs to MarketGrid to generate cards for each market */} + + +
+
+ ); + } + ``` + +1. Replace the `components/market-card.tsx` file with this code, which shows information about a specific prediction market: + + ```javascript + import { Card } from "@/components/ui/card"; + import { Badge } from "@/components/ui/badge"; + import { Button } from "@/components/ui/button"; + import { + TrendingUp, + TrendingDown, + Clock, + DollarSign, + CheckCircle2, + } from "lucide-react"; + import { cn } from "@/lib/utils"; + import { useState, useMemo } from "react"; + import { useReadContract } from "thirdweb/react"; + import { contract } from "@/lib/contract-utils"; + import { toEther } from "thirdweb/utils"; + + interface MarketCardProps { + handlePlaceBet: ( + marketId: number, + side: "yes" | "no", + betAmount: number + ) => void; + marketId: number; + } + + export function MarketCard({ marketId, handlePlaceBet }: MarketCardProps) { + const [hoveredSide, setHoveredSide] = useState<"yes" | "no" | null>(null); + const [selectedSide, setSelectedSide] = useState<"yes" | "no" | null>(null); + const [betAmount, setBetAmount] = useState(10); + const [showBettingInterface, setShowBettingInterface] = useState(false); + + // get Data about the market from the contract + const { + data: marketInfo, + isLoading: isLoadingMarketInfo, + error: marketInfoError, + } = useReadContract({ + contract: contract, + method: "getMarket", + params: [BigInt(marketId)], + }); + + const { + data: marketProbData, + isLoading: isLoadingMarketProb, + error: marketProbError, + } = useReadContract({ + contract: contract, + method: "getProbability", + params: [BigInt(marketId), true], + }); + + // Parse the market data using useMemo for performance and consistency + const marketData = useMemo(() => { + if (!marketInfo) { + return undefined; + } + + const typedMarketInfo = marketInfo as any; + const typedMarketProbData = marketProbData as any; + + // Ensure all BigInts are handled correctly to prevent precision loss + const totalYesAmount = typedMarketInfo.totalYesAmount as bigint; + const totalNoAmount = typedMarketInfo.totalNoAmount as bigint; + const totalYesShares = typedMarketInfo.totalYesShares as bigint; + const totalNoShares = typedMarketInfo.totalNoShares as bigint; + const probYes = typedMarketProbData as bigint; + + return { + title: typedMarketInfo.question, + endTime: typedMarketInfo.endTime.toString(), + probYes: probYes.toString(), + probNo: 100 - Number(probYes), + change: Number(probYes) - (100 - Number(typedMarketProbData)), // difference between yes and no + volume: Number(toEther(totalYesAmount + totalNoAmount)), + resolved: typedMarketInfo.resolved, + totalYesShares: totalYesShares, + winner: typedMarketInfo.winner, + totalNoShares: totalNoShares, + image: "/penguin-mascot.png", + marketBalance: toEther(typedMarketInfo.marketBalance), + }; + }, [marketInfo]); + + const calculatePotentialPayout = (betAmount: number) => { + if (betAmount <= 0) return 0; + + // NOTE: The code here does not take into account other holders of a position in the pool + // in this calculation + const totalPool = marketData ? marketData.volume + betAmount : 0; + return totalPool; + }; + + const calculateProfit = (amount: number) => { + const payout = calculatePotentialPayout(amount); + + return payout - amount; + }; + + if (!isLoadingMarketInfo) { + return ( + +
+ {/* Header */} +
+
+ {marketData?.title} + + XTZ + +
+
+
+ {marketData?.probYes}% +
+ +
= 0 ? "text-green-400" : "text-red-400" + )} + > + {marketData && marketData.change >= 0 ? ( + + ) : ( + + )} + {marketData && Math.abs(marketData.change)}% +
+
+
+ + {/* Title */} +

+ {marketData?.title} +

+ + {/* Probability Bar */} +
+
+ {marketData?.probYes}% + {marketData?.probNo}% +
+
+
+
+
+ + {/* Betting Interface */} + {!showBettingInterface ? ( +
+ + +
+ ) : ( +
+ {/* Selected Side Display */} +
+ Betting on: + + {selectedSide?.toUpperCase()} + +
+ + {/* Bet Amount Slider */} +
+
+ Bet Amount: + + {betAmount} XTZ + +
+ + setBetAmount(parseInt(e.target.value))} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + style={{ + background: `linear-gradient(to right, ${ + selectedSide === "yes" ? "#10b981" : "#ef4444" + } 0%, ${selectedSide === "yes" ? "#10b981" : "#ef4444"} ${ + (betAmount / 1000) * 100 + }%, #374151 ${(betAmount / 1000) * 100}%, #374151 100%)`, + }} + /> + +
+ 10 XTZ + 1000 XTZ +
+
+ + {/* Real-time Returns */} +
+
+ Potential Win: + + {calculatePotentialPayout(betAmount)} XTZ + +
+
+ Profit: + 0 + ? "text-green-400" + : "text-red-400" + )} + > + {calculateProfit(betAmount).toFixed(2)} XTZ + +
+
+ Return: + + + + {((calculateProfit(betAmount) / betAmount) * 100).toFixed( + 1 + )} + % + +
+
+ + {/* Action Buttons */} +
+ + +
+
+ )} + + {/* Footer Stats */} +
+
+
+ + {marketData?.marketBalance} XTZ +
+
+ + {new Date( + parseInt(marketData?.endTime) * 1000 + ).toLocaleDateString()} +
+
+ {!marketData?.resolved ? ( + + + Active + + ) : ( + + + Resolved + + )} +
+
+ + ); + } + } + ``` + +1. Replace the `components/market-grid.tsx` file with this code, which shows the prediction markets in a grid: + + ```javascript + import { MarketCard } from "@/components/market-card"; + import ResolveMarkets from "@/components/resolve-markets"; + + interface MarketGridProps { + existingMarketIds: number[]; + handlePlaceBet: ( + marketId: number, + side: "yes" | "no", + betAmount: number + ) => void; + claimWinnings: (marketId: number) => void; + resolveMarket: (marketId: number, winner: string) => void; + } + export function MarketGrid({ + existingMarketIds, + handlePlaceBet, + claimWinnings, + resolveMarket, + }: MarketGridProps) { + return ( +
+ {existingMarketIds.map((marketId) => ( +
+ + +
+ +
+
+ ))} +
+ ); + } + ``` + +1. Create a file named `components/resolve-markets.tsx` with this code, which handles the UI for resolving a prediction market: + + ```javascript + import { Card } from "@/components/ui/card"; + import { Badge } from "@/components/ui/badge"; + import { Button } from "@/components/ui/button"; + import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + } from "@/components/ui/select"; + import { + Clock, + DollarSign, + CheckCircle2, + AlertTriangle, + } from "lucide-react"; + import { useState, useMemo } from "react"; + import { useReadContract } from "thirdweb/react"; + import { contract } from "@/lib/contract-utils"; + import { toEther } from "thirdweb/utils"; + + interface ResolveMarketsProps { + marketId: number; + claimWinnings: (marketId: number) => void; + resolveMarket: (marketId: number, winner:string) => void; + } + + export default function ResolveMarkets({ + marketId, + claimWinnings, + resolveMarket, + }: ResolveMarketsProps) { + const [selectedWinner, setSelectedWinner] = useState<"1" | "2" | "">(""); + const [isResolving, setIsResolving] = useState(false); + + // get Data about the market from the contract + const { data: marketInfo, isLoading: isLoadingMarketInfo } = useReadContract({ + contract: contract, + method: "getMarket", + params: [BigInt(marketId)], + }); + + // Parse the market data using useMemo for performance and consistency + const marketData = useMemo(() => { + if (!marketInfo) { + return undefined; + } + + const typedMarketInfo = marketInfo as any; + + // Ensure all BigInts are handled correctly to prevent precision loss + const totalYesAmount = typedMarketInfo.totalYesAmount as bigint; + const totalNoAmount = typedMarketInfo.totalNoAmount as bigint; + const totalYesShares = typedMarketInfo.totalYesShares as bigint; + const totalNoShares = typedMarketInfo.totalNoShares as bigint; + + return { + title: typedMarketInfo.question, + endTime: typedMarketInfo.endTime.toString(), + volume: Number(toEther(totalYesAmount + totalNoAmount)), + resolved: typedMarketInfo.resolved, + totalYesShares: totalYesShares, + winner: typedMarketInfo.winner, + totalNoShares: totalNoShares, + image: "/penguin-mascot.png", + marketBalance: toEther(typedMarketInfo.marketBalance) + }; + }, [marketInfo]); + + + if (isLoadingMarketInfo || !marketData) { + return ( + +
+
+
+
+
+
+
+
+ ); + } + + return ( + +
+ {/* Header */} +
+
+ {marketData.title} + + Market #{marketId} + + {marketData.resolved && ( + + + Resolved + + )} +
+
+ + {/* Title */} +

+ {marketData.title} +

+ + {/* Resolution Interface */} + {!marketData.resolved ? ( +
+
+ + + Market Resolution Required + +
+ + {/* Winner Selection */} +
+ + +
+ + {/* Preview */} + {selectedWinner && ( +
+
+ Resolution Preview: +
+
+ Market ID: + #{marketId} +
+
+ Winner: + + Side {selectedWinner} ( + {selectedWinner === "1" ? "YES" : "NO"}) + +
+
+ )} + + {/* Resolve Button */} + + + {/* Warning */} +
+
+ +
+ Warning: Market resolution is permanent and + cannot be undone. Verify the outcome before proceeding. +
+
+
+
+ ) : ( +
+
+
+ + Market Resolved +
+
+ Winning side: {marketData.winner === 1 ? "Yes" : "No"} +
+
+
+ )} + + {/* Show resolved status */} + {marketData?.resolved && ( + + )} + + {/* Footer Stats */} +
+
+
+ + {marketData.marketBalance} XTZ +
+
+ + {new Date( + parseInt(marketData.endTime) * 1000 + ).toLocaleDateString()} +
+
+
+
+
+ ); + } + ``` + +1. Run this command to start the application: + + ```bash + npm run dev + ``` + +1. Open the application in a web browser to see the active prediction market. + +Now people can bet on the prediction market. + +## Testing the application + +The prediction market appears as a card on the home page, with Yes and No buttons that allow users to place bets: + +The active prediction market on the front page + +When you click Yes or No, the card expands to show a slider for you to set the amount of your bet in XTZ and a Place Bet button that allows you to submit the bet to the contract: + +Making a bet on the prediction market + +You can place multiple bets from the same account or different accounts to simulate many users. + +Then, when you are ready to close the market and distribute the winnings, connect to the application with the same account that you used to deploy the contract. +Underneath the market card is another card that shows its resolution state: + +The resolution card, showing the button to resolve the market + +When you are ready to resolve the market (which cannot be undone), select the winning outcome and then click the Resolve Market button: + +The resolution card, showing the button to resolve the market + +The market shows that it is resolved: + +The resolution card, showing the resolved market and the winning outcome + +When the market is resolved, the contract does not automatically distribute winnings. +Users must connect and click the Claim Winnings button to call the `claimWinnings` function to receive their winnings. + +## Conclusion + +Now you now everything you need to deploy simple smart contracts to Etherlink and use them as the backend for web-based applications. +From here you can \ No newline at end of file diff --git a/docs/tutorials/predictionMarket/index.md b/docs/tutorials/predictionMarket/index.md new file mode 100644 index 00000000..23f0020b --- /dev/null +++ b/docs/tutorials/predictionMarket/index.md @@ -0,0 +1,47 @@ +--- +title: "Tutorial: Create a prediction market on Etherlink" +sidebar_label: "Tutorial: Create a prediction market" +slug: /tutorials/predictionMarket +--- + +Prediction markets are platforms that allow users take a position on any event with a binary (true or false) outcome. +For example, the question “Will Manchester United win the English Premier League in 2026?” has only two possible outcomes — Yes or No — because only one team can win the English premier league in any given season. +Users can bet on Yes or No and the winners receive a portion of the total amount based on how much they bet on the winning outcome. + +As an example of deploying and interacting with smart contracts on Etherlink, this tutorial walks through how to build a simple prediction market dApp, powered by a prediction market smart contract deployed on Etherlink and a simple frontend application. + +:::note + +This is a version of a tutorial originally posted as [Build a Prediction Market on Etherlink](https://adebola-niran.medium.com/build-a-prediction-market-on-etherlink-76bcac1647a3). +For more information about prediction markets, such as how the winners' shares are calculated based on their bets, see that tutorial. + +::: + +## Learning objectives + +In this tutorial, you will learn: + +- How to set up a development environment with Hardhat +- How to write a simple smart contract in Solidity +- How to create an Etherlink account and get Testnet tokens using the faucet +- How to test a smart contract locally +- How to use Hardhat to deploy the contract to Etherlink +- How to verify your contract on the Etherlink explorer +- How to create a frontend application to interact with the smart contract + +## Not included + +This tutorial is not intended to be a complete course for developing smart contracts with Solidity or Hardhat or for developing frontend applications; the smart contract and frontend application in this tutorial are just examples. +Also, the tools in this tutorial are only examples of tools that you can use with Etherlink. +You can use many different smart contract and frontend development tools and technologies with Etherlink. + +## Disclaimer + +The code is for education purposes and hasn’t been audited or optimized. +You shouldn’t use this code in production. + +## Application source code + +You can see the source code for the completed application here: TODO link + +When you are ready, go to [Part 1: Write a contract](/tutorials/predictionMarket/write-contract). diff --git a/docs/tutorials/predictionMarket/write-contract.md b/docs/tutorials/predictionMarket/write-contract.md new file mode 100644 index 00000000..b27adc81 --- /dev/null +++ b/docs/tutorials/predictionMarket/write-contract.md @@ -0,0 +1,425 @@ +--- +title: "Part 1: Write a contract" +--- + +The starter project for this tutorial includes an example prediction market contract. +This contract uses the [OpenZeppelin](https://docs.openzeppelin.com/) library as a starting point. +The OpenZeppelin library includes tested and secure contracts, and using pre-tested contracts like these can be easier and safer than writing your own contracts, especially when you deal with betting systems that can be manipulated. + +Etherlink is compatible with Ethereum technology, which means that you can use any Ethereum-compatible tool for development, including Hardhat, Foundry, Truffle Suite, and Remix IDE. +For more information on tools that work with Etherlink, see [Developer toolkits](/building-on-etherlink/development-toolkits). +The starter project also uses the [Hardhat](/building-on-etherlink/development-toolkits) development environment to simplify the process of compiling and deploying the contract. + +The starter project is in the repository https://github.com/trilitech/tutorial-applications and the following steps walk you through downloading it. + +Follow these steps to set up the contract for the prediction market: + +1. Clone the starter project by running these commands: + + ```bash + git clone --no-checkout https://github.com/trilitech/tutorial-applications.git + cd tutorial-applications/ + git sparse-checkout init + git sparse-checkout set etherlink-prediction + git checkout main + ``` + + The starter project is in the folder `etherlink-prediction/starter`, with folders `backend` for the project that contains the smart contract and `frontend` for the frontend application that you will use later. + The `backend` folder contains a starter Hardhat project, consisting primarily of a `hardhat.config.js` configuration file with information about the Shadownet testnet and a `package.json` file with dependencies for the project. + +1. Open the `contract/Contract.sol` file in any text editor or IDE. +The starter contract looks like this: + + ```solidity + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.9; + + // for creating a contract that can be owned by an address + // this is useful for managing access permissions to methods in the contract + import "@openzeppelin/contracts/access/Ownable.sol"; + + // for preventing reentrancy attacks on functions that modify state + // this is important for functions that involve transferring tokens or ether + // to ensure that the function cannot be called again while it is still executing + import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + + /** @title A simple prediction market that uses the Pari-mutuel model allowing winners to share the prize pool. + */ + + contract PredictxtzContract is Ownable, ReentrancyGuard { + + } + ``` + + The starter contract imports two OpenZeppelin contract libraries: + + - `Ownable`: Manages access permissions in the contract by setting a single administrator account and allowing only that account to run certain functions + - `ReentrancyGuard`: Helps prevent re-entrancy attacks on functions that modify state, such as preventing the winner from claiming the same reward twice, which is particularly important for functions that involve transferring tokens + +1. Within the contract, add these variables and structs: + + ```solidity + // This contract allows users to create markets, place bets, resolve markets, and claim winnings. + + // constants + uint256 public constant PRECISION = 1e18; + uint256 public constant VIRTUAL_LIQUIDITY = 1000 * PRECISION; // used to calculate price per share + + // holds information about each market + struct Market { + uint256 id; + string question; + // string description; + uint256 endTime; // When betting stops + // uint256 resolveTime; // When market can be resolved. // For now, we resolve immediately after endTime or manually + bool resolved; + uint8 winner; // 0 = NO, 1 = YES, 2 = INVALID + uint256 totalYesAmount; // Total $ bet on YES + uint256 totalNoAmount; // Total $ bet on NO + uint256 totalYesShares; // Total YES shares (for price calculation) + uint256 totalNoShares; // Total No shares (for price calculation) + uint256 marketBalance; // how much is in the market + address creator; + // uint256 createdAt; + bool active; + } + + + // calculates the total position held by a market participant + struct Position { + uint256 yesAmount; // $ amount bet on YES + uint256 noAmount; // $ amount bet on NO + uint256 yesShares; // YES shares owned (for pool splitting) + uint256 noShares; // NO shares owned (for pool splitting) + bool claimed; // Whether winnings have been claimed + } + + uint256 public marketCounter; // keeps track of the no of markets created + + mapping(uint256 => Market) public markets; + mapping(uint256 => mapping(address => Position)) public positions; + mapping(address => uint256[]) public userMarkets; + ``` + + The `Market` struct defines a type for each prediction market that the contract manages, including a description of the question, the time for the end of the process, and information about the current bets. + + The `Position` struct defines a type for each bet that a user makes, including the amount that they bet on the yes or no options and whether they have claimed their rewards. + + +1. Add these event definitions: + + ```solidity + // events + event MarketCreated( + uint256 indexed marketId, + address indexed creator, + string question, + uint256 endTime + ); + + event BetPlaced( + uint256 indexed marketId, + address indexed user, + bool indexed isYes, + uint256 amount, + uint256 shares + ); + + event MarketResolved( + uint256 indexed marketId, + uint8 indexed winner, + address indexed resolver + ); + + event WinningsClaimed( + uint256 indexed marketId, + address indexed user, + uint256 amount + ); + ``` + + Off-chain applications can listen to these events and learn when a betting market opens, when bets are added, when accounts place bets, and when the winner claims their winnings. + +1. Add this empty constructor function to the contract: + + ```solidity + constructor() {} + ``` + +1. Add this function to create a betting market: + + ```solidity + function createMarket( + string calldata question, + uint256 duration + ) external returns (uint256) { + require(duration > 0, "Duration must be positive"); + require(bytes(question).length > 0, "Question cannot be empty"); + + uint256 marketId = ++marketCounter; + + markets[marketId] = Market({ + id: marketId, + question: question, + endTime: block.timestamp + duration, + resolved: false, + winner: 2, // Unresolved + totalYesAmount: 0, // No money in pool yet + totalNoAmount: 0, // No money in pool yet + totalYesShares: VIRTUAL_LIQUIDITY, // Virtual shares for pricing + totalNoShares: VIRTUAL_LIQUIDITY, // Virtual shares for pricing + marketBalance: 0, + creator: msg.sender, + // createdAt: block.timestamp, + active: true + }); + + // emiting events makes it cheaper to track changes in the contract without needing to read the entire state and paying gas + emit MarketCreated( + marketId, + msg.sender, + question, + block.timestamp + duration + ); + return marketId; + } + ``` + + This function accepts a question and a duration for the market in seconds. + It initializes a `Market` variable to store information about the market and emits a `MarketCreated` event to notify users that the new market is available. + +1. Add these utility functions: + + ```solidity + /** + * Calculate pricePerShare without fees + */ + function pricePerShareWithoutFees( + uint256 marketId, + bool isYes + ) public view returns (uint256) { + Market memory market = markets[marketId]; + uint256 totalShares = market.totalYesShares + market.totalNoShares; + + if (isYes) { + return (market.totalYesShares * PRECISION) / totalShares; + } else { + return (market.totalNoShares * PRECISION) / totalShares; + } + } + + /** + * Calculate how many shares you get for a bet amount + * More money = more shares = bigger portion of winnings + */ + function calculateShares( + uint256 marketId, + bool isYes, + uint256 betAmount + ) public view returns (uint256) { + Market memory market = markets[marketId]; + require(market.active, "Market not active"); + + // The share price is now calculated without fees + uint256 pricePerShare = pricePerShareWithoutFees(marketId, isYes); + uint256 shares = (betAmount * PRECISION) / pricePerShare; + return shares; + } + ``` + + These functions calculate the price per share and how many shares a user gets for a given bet amount. + +1. Add this function to allow users to place bets: + + ```solidity + // BETTING FUNCTIONS + + /** + * @dev Place a bet on YES or NO + * @param marketId The market to bet on + * @param isYes true for a bet on YES, false for a bet on NO + */ + function placeBet( + uint256 marketId, + bool isYes + ) external payable nonReentrant { + Market storage market = markets[marketId]; + uint256 betAmount = msg.value; // Use the value sent with the transaction as the bet amount + market.marketBalance += msg.value; + + // Validation + require(market.active, "Market not active"); + require(!market.resolved, "Market has been resolved"); + require(block.timestamp < market.endTime, "Betting period ended"); + require(betAmount > 0, "Must bet positive amount"); + + uint256 shares = calculateShares(marketId, isYes, betAmount); // 100shares when amount = $51 + + // Update market totals + if (isYes) { + market.totalYesAmount += betAmount; // Tracks the total amount bet on YES + market.totalYesShares += shares; // Synthetic YES shares (virtual liquidity) + Real YES shares + positions[marketId][msg.sender].yesAmount += betAmount; // Tracks user's YES bet amount + positions[marketId][msg.sender].yesShares += shares; // Tracks user's YES shares + } else { + market.totalNoAmount += betAmount; + market.totalNoShares += shares; + positions[marketId][msg.sender].noAmount += betAmount; + positions[marketId][msg.sender].noShares += shares; + } + + // Track user participation + if ( + positions[marketId][msg.sender].yesAmount + + positions[marketId][msg.sender].noAmount == + betAmount + ) { + userMarkets[msg.sender].push(marketId); + } + + emit BetPlaced(marketId, msg.sender, isYes, betAmount, shares); + } + ``` + + Any user can call this function, which places a bet on an open market. + The user passes the ID of the market and a Boolean value to bet on Yes or No. + They must also include XTZ for their bet in the transaction. + + The function updates the market with the new bet, updates its records about the user's position, and emits an event to notify applications of the new bet. + +1. Add this function to allow the administrator to mark a market as resolved: + + ```solidity + // Only the owner can resolve markets + function resolveMarket(uint256 marketId, uint8 winner) external onlyOwner { + Market storage market = markets[marketId]; + + require(!market.resolved, "Already resolved"); + require(winner <= 2, "Invalid winner"); + + market.resolved = true; + market.active = false; + market.winner = winner; + + emit MarketResolved(marketId, winner, msg.sender); + } + ``` + + This function has the `onlyOwner` modifier to ensure that only the contract administrator can call it. + It marks the specified market as resolved, sets the winning value, and emits a `MarketResolved` event that tells winning betters that they can claim their rewards. + +1. Add this function to allow winners to claim their share of the pot: + + ```solidity + // CLAIMING WINNINGS + + /** + * @dev Claim winnings from a resolved market + * Winners split the total pool proportionally to their shares + */ + function claimWinnings(uint256 marketId) external nonReentrant { + Market storage market = markets[marketId]; //access storage so we can update + Position storage position = positions[marketId][msg.sender]; + + require(market.resolved, "Market not resolved"); + require(!position.claimed, "Already claimed"); + + position.claimed = true; + + uint256 payout = 0; + + if (market.winner == 2) { + // INVALID - refund original bets + payout = position.yesAmount + position.noAmount; + } else if (market.winner == 1 && position.yesShares > 0) { + // YES wins - split the total pool among YES shareholders + uint256 totalPool = market.totalYesAmount + market.totalNoAmount; + uint256 winningSideShares = market.totalYesShares - + VIRTUAL_LIQUIDITY; // Remove virtual liquidity + + if (winningSideShares > 0) { + payout = (position.yesShares * totalPool) / winningSideShares; + } + } else if (market.winner == 0 && position.noShares > 0) { + // NO wins - split the total pool among NO shareholders + uint256 totalPool = market.totalYesAmount + market.totalNoAmount; + uint256 winningSideShares = market.totalNoShares - + VIRTUAL_LIQUIDITY; // Remove virtual liquidity + + if (winningSideShares > 0) { + payout = (position.noShares * totalPool) / winningSideShares; + } + } + + if (payout > 0) { + (bool success, ) = payable(msg.sender).call{value: payout}(""); + market.marketBalance -= payout; + require(success, "XTZ transfer failed"); + emit WinningsClaimed(marketId, msg.sender, payout); + } + } + ``` + + This function retrieves a better's position for the given market, calculates that better's share of the winnings, and sends it to them. + Each winning better needs to call this function to get their share. + The function also emits a `WinningsClaimed` event to notify applications that the better has claimed their winnings. + +1. Add these view functions to make it easier for off-chain applications to get information about betting markets: + + ```solidity + // VIEW FUNCTIONS + + function getUserPosition( + uint256 marketId, + address user + ) external view returns (Position memory) { + return positions[marketId][user]; + } + + function getUserMarkets( + address user + ) external view returns (uint256[] memory) { + return userMarkets[user]; + } + + function getMarket(uint256 marketId) external view returns (Market memory) { + return markets[marketId]; + } + + // Useful for the frontend to know the most recent probabilities for each outcome + function getProbability( + uint256 marketId, + bool isYes + ) external view returns (uint256) { + uint256 price = pricePerShareWithoutFees(marketId, isYes); + return (price * 100) / PRECISION; // get the percentage probability + } + ``` + + Using functions like these to get information about an account's position and the current probabilities for a betting market is easier than reading the contract storage directly. + + You can see the complete contract in the `etherlink-prediction/completed/backend/contract/Contract/sol` file. + +1. Set up an Etherlink account in a compatible wallet if you don't already have one. +For more information, see [Using your wallet](/get-started/using-your-wallet). + +1. Create a file named `.env` in the same folder as the `hardhat.config.js` file and set your Etherlink account private key as the value of the `PRIVATE_KEY` environment variable: + + ```env + PRIVATE_KEY= + ``` + +1. Compile the contract: + + ```bash + npx hardhat compile + ``` + + If you see any errors, check that your contract matches the version from the `main` branch of the repository. + +Hardhat compiles the contract into files in the `artifacts/contracts` folder. +Files in this folder include the compiled bytecode of the contract and the application binary interface (ABI) that describes the functions. +Applications use this ABI to know how to format calls to the contract. + +Now the contract is compiled and ready to be deployed to a test network. +Continue to [Part 2: Deploying the contract](/tutorials/predictionMarket/deploy-contract). diff --git a/package-lock.json b/package-lock.json index 58072860..2e707f96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -210,7 +210,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.40.1.tgz", "integrity": "sha512-Mw6pAUF121MfngQtcUb5quZVqMC68pSYYjCRZkSITC085S3zdk+h/g7i6FxnVdbSU6OztxikSDMh1r7Z+4iPlA==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", @@ -367,7 +366,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -2100,7 +2098,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2123,7 +2120,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2201,7 +2197,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2561,7 +2556,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3252,7 +3246,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -3494,7 +3487,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/babel": "3.9.2", "@docusaurus/bundler": "3.9.2", @@ -3862,7 +3854,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.9.2.tgz", "integrity": "sha512-DEVIwhbrZZ4ir31X+qQNEQqDWkgCJUV6kiPPAd2MGTY8n5/n0c4B8qA5k1ipF2izwH00JEf0h6Daaut71zzkyw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/types": "3.9.2", "@rspack/core": "^1.5.0", @@ -4244,7 +4235,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4783,7 +4773,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -4936,6 +4925,7 @@ "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.6.tgz", "integrity": "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w==", "license": "MIT", + "peer": true, "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", @@ -4954,6 +4944,7 @@ "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.74.0.tgz", "integrity": "sha512-nRh51kF8xIzG93VVXOMFlPFz8XGcvfglf+4XFFjv0DtKUhmkafx6IR4jD99Jpaau95UfqvKbGSbwL7RyfvdXFw==", "license": "MIT", + "peer": true, "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", @@ -4981,6 +4972,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -4990,6 +4982,7 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5002,6 +4995,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "license": "MIT", + "peer": true, "dependencies": { "is-inside-container": "^1.0.0" }, @@ -5017,6 +5011,7 @@ "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "license": "MIT", + "peer": true, "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", @@ -5194,7 +5189,8 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", @@ -5377,7 +5373,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -7432,7 +7427,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -7583,7 +7577,6 @@ "integrity": "sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" @@ -8008,7 +8001,6 @@ "version": "5.62.7", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.7.tgz", "integrity": "sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw==", - "peer": true, "dependencies": { "@tanstack/query-core": "5.62.7" }, @@ -9443,7 +9435,6 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9501,7 +9492,6 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -9545,7 +9535,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.40.1.tgz", "integrity": "sha512-iUNxcXUNg9085TJx0HJLjqtDE0r1RZ0GOGrt8KNQqQT5ugu8lZsHuMUYW/e0lHhq6xBvmktU9Bw4CXP9VQeKrg==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.6.1", "@algolia/client-abtesting": "5.40.1", @@ -10270,7 +10259,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -10361,6 +10349,7 @@ "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", @@ -10389,6 +10378,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -10403,13 +10393,15 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/c12/node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10419,6 +10411,7 @@ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", + "peer": true, "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", @@ -10429,13 +10422,15 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/c12/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -10695,7 +10690,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -10745,6 +10739,7 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=10" } @@ -10786,6 +10781,7 @@ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "license": "MIT", + "peer": true, "dependencies": { "consola": "^3.2.3" } @@ -10995,6 +10991,7 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "license": "ISC", + "peer": true, "bin": { "color-support": "bin.js" } @@ -11557,7 +11554,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11863,7 +11859,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", "integrity": "sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -12273,7 +12268,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -13669,7 +13663,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13915,7 +13908,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14078,6 +14070,7 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "license": "ISC", + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -14090,6 +14083,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -14101,7 +14095,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/fs-monkey": { "version": "1.0.5", @@ -14235,6 +14230,7 @@ "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz", "integrity": "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==", "license": "MIT", + "peer": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", @@ -14252,7 +14248,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/github-slugger": { "version": "1.5.0", @@ -14600,6 +14597,7 @@ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -14621,6 +14619,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15394,8 +15393,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/ieee754": { "version": "1.2.1", @@ -19192,6 +19190,7 @@ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "license": "MIT", + "peer": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -19205,6 +19204,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -19216,7 +19216,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/mipd": { "version": "0.0.7", @@ -19242,6 +19243,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -19870,7 +19872,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -19917,6 +19918,7 @@ "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz", "integrity": "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==", "license": "MIT", + "peer": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", @@ -19936,19 +19938,22 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/nypm/node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/nypm/node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", + "peer": true, "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", @@ -19959,7 +19964,8 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -20038,7 +20044,8 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.6.tgz", "integrity": "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/on-exit-leak-free": { "version": "0.2.0", @@ -20701,7 +20708,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pathval": { "version": "2.0.0", @@ -20733,7 +20741,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/periscopic": { "version": "3.1.0", @@ -21005,7 +21014,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -21899,7 +21907,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -22834,6 +22841,7 @@ "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", "license": "MIT", + "peer": true, "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" @@ -22843,7 +22851,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -22973,7 +22980,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -23025,7 +23031,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -24946,6 +24951,7 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", + "peer": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -24963,6 +24969,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=8" } @@ -24971,7 +24978,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/terser": { "version": "5.26.0", @@ -25027,7 +25035,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -25461,8 +25468,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "peer": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -25549,6 +25555,7 @@ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "license": "BSD-2-Clause", "optional": true, + "peer": true, "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -26061,7 +26068,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26241,7 +26247,6 @@ "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", "license": "MIT", - "peer": true, "dependencies": { "derive-valtio": "0.1.0", "proxy-compare": "2.6.0", @@ -26523,7 +26528,6 @@ "version": "5.97.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -26750,7 +26754,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26976,7 +26979,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/workerpool": { "version": "6.5.1", @@ -27098,7 +27102,6 @@ "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -27376,7 +27379,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.24.tgz", "integrity": "sha512-E77RpEqxeBGBVbcK/5QKQsLM+3u6aN7pVgiGJENbwYfdsExPS/xyyUMfmeM3eY32LBCIjuzv6XU505sHn2t+Kw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/sidebars.js b/sidebars.js index cf4c928e..f71efbfd 100644 --- a/sidebars.js +++ b/sidebars.js @@ -51,13 +51,10 @@ const sidebars = { label: 'Tutorial', collapsed: false, items: [ - 'tutorials/marketpulse/index', - 'tutorials/marketpulse/setup', - 'tutorials/marketpulse/backend', - 'tutorials/marketpulse/test', - 'tutorials/marketpulse/deploy', - 'tutorials/marketpulse/frontend', - 'tutorials/marketpulse/cicd', + 'tutorials/predictionMarket/index', + 'tutorials/predictionMarket/write-contract', + 'tutorials/predictionMarket/deploy-contract', + 'tutorials/predictionMarket/frontend', ], }, { diff --git a/static/img/tutorials/github-secrets.png b/static/img/tutorials/github-secrets.png deleted file mode 100644 index 6a674b48..00000000 Binary files a/static/img/tutorials/github-secrets.png and /dev/null differ diff --git a/static/img/tutorials/prediction-active-market.png b/static/img/tutorials/prediction-active-market.png new file mode 100644 index 00000000..85d9709a Binary files /dev/null and b/static/img/tutorials/prediction-active-market.png differ diff --git a/static/img/tutorials/prediction-create-market.png b/static/img/tutorials/prediction-create-market.png new file mode 100644 index 00000000..e5d1f667 Binary files /dev/null and b/static/img/tutorials/prediction-create-market.png differ diff --git a/static/img/tutorials/prediction-custom-abi-functions.png b/static/img/tutorials/prediction-custom-abi-functions.png new file mode 100644 index 00000000..8b80c651 Binary files /dev/null and b/static/img/tutorials/prediction-custom-abi-functions.png differ diff --git a/static/img/tutorials/prediction-deployed-contract.png b/static/img/tutorials/prediction-deployed-contract.png new file mode 100644 index 00000000..315bde85 Binary files /dev/null and b/static/img/tutorials/prediction-deployed-contract.png differ diff --git a/static/img/tutorials/prediction-making-bet.png b/static/img/tutorials/prediction-making-bet.png new file mode 100644 index 00000000..e92624ec Binary files /dev/null and b/static/img/tutorials/prediction-making-bet.png differ diff --git a/static/img/tutorials/prediction-new-custom-abi.png b/static/img/tutorials/prediction-new-custom-abi.png new file mode 100644 index 00000000..7c7789f3 Binary files /dev/null and b/static/img/tutorials/prediction-new-custom-abi.png differ diff --git a/static/img/tutorials/prediction-resolved-market.png b/static/img/tutorials/prediction-resolved-market.png new file mode 100644 index 00000000..17134437 Binary files /dev/null and b/static/img/tutorials/prediction-resolved-market.png differ diff --git a/static/img/tutorials/prediction-resolving-market.png b/static/img/tutorials/prediction-resolving-market.png new file mode 100644 index 00000000..c03225bb Binary files /dev/null and b/static/img/tutorials/prediction-resolving-market.png differ diff --git a/static/img/tutorials/prediction-unresolved-market.png b/static/img/tutorials/prediction-unresolved-market.png new file mode 100644 index 00000000..d0c01a6d Binary files /dev/null and b/static/img/tutorials/prediction-unresolved-market.png differ diff --git a/static/img/tutorials/screen.png b/static/img/tutorials/screen.png deleted file mode 100644 index d4366011..00000000 Binary files a/static/img/tutorials/screen.png and /dev/null differ