diff --git a/docs/learn/interactive/dapps/README.mdx b/docs/learn/interactive/dapps/README.mdx new file mode 100644 index 000000000..7c749c345 --- /dev/null +++ b/docs/learn/interactive/dapps/README.mdx @@ -0,0 +1,10 @@ +--- +title: Dapps Challenge +sidebar_position: 60 +--- + +import DocCardList from "@theme/DocCardList"; + +The Soroban Dapps Challenge is a dynamic course designed for developers eager to explore the potential of building decentralized applications (Dapps) on the Soroban smart contracts platform. + + diff --git a/docs/learn/interactive/dapps/introduction.mdx b/docs/learn/interactive/dapps/introduction.mdx new file mode 100644 index 000000000..14449186b --- /dev/null +++ b/docs/learn/interactive/dapps/introduction.mdx @@ -0,0 +1,31 @@ +--- +sidebar_position: 10 +title: Dapps on Soroban +sidebar_label: Introduction +--- + +Decentralized applications, or "dapps," mark a significant shift in our digital interactions, running on a blockchain or peer-to-peer network instead of centralized servers. This shift enhances transparency, security, and user control, as data and smart contracts are stored on a public ledger, open for audit by anyone. Soroban facilitates building and deploying dapps on the Stellar blockchain, offering tools and frameworks that simplify the development process for even those with minimal coding experience. The Soroban Dapps Challenge highlights this, enabling you to create a variety of dapp use cases on a single page in just 20 minutes, with minimal coding, guiding you from smart contract deployment to user interaction through a web frontend. + +The Soroban Dapps Challenge is a dynamic course designed for developers eager to explore the potential of building decentralized applications (Dapps) on the Soroban smart contracts platform. This course is part challenge, part educational journey that sets the stage for practical and creative blockchain development. + +While the course specifically focuses on the Soroban platform, the knowledge you gain can be applied to other transaction processors such as different blockchains, L2s, and permissioned ledgers. The skills you acquire here are meant to be transferable and versatile. + +Through The Soroban Dapps Challenge, you'll have hands-on experience using Soroban's initial versions of the smart contracts environment, a Rust SDK, a CLI, and an RPC server. You'll learn how to write, test, and deploy smart contracts, and you'll get to see your code in action on Futurenet. + +## What This Course Entails + +We've designed this course as a learning adventure. It's a way for developers from the Stellar ecosystem and other blockchain communities to experiment, provide feedback, and contribute to the Soroban development process. + +As you progress through The Soroban Dapps Challenge, anticipate your code to break and updates to shift things. We invite you to experiment and build but also remind you that changes are afoot as we prepare for the production release. + +## Getting Started + +To get started, simply head over to the [Dashboard](/docs/learn/interactive/dapps/dashboard), [connect your wallet](../../../smart-contracts/guides/freighter/connect-testnet.mdx), and see what challenges await you! + +## Giving Your Feedback + +We value your input. Feel free to file issues in the Soroban repos or raise them in the soroban channel in the Stellar Developer [Discord](https://discord.gg/3qrBhbwE). + +Join us in this exciting course and start building for the future of blockchain technology. + +> Disclaimer: The Soroban Dapps Challenge is for educational purposes only and is designed to teach developers how to write code using Soroban by experimenting in a Sandbox environment. None of the materials provided as part of the Soroban Dapps Challenge shall be construed as financial, legal, or investment advice. Any developers that wish to subsequently launch any Soroban Dapps live on mainnet acknowledge that they do so independently, outside of the Soroban Dapps Challenge program. All such developers should ensure they have considered any applicable legal and compliance obligations in force in their jurisdiction. diff --git a/docs/learn/interactive/dapps/scaffold-soroban.mdx b/docs/learn/interactive/dapps/scaffold-soroban.mdx new file mode 100644 index 000000000..1d210b6ff --- /dev/null +++ b/docs/learn/interactive/dapps/scaffold-soroban.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 30 +title: Scaffold Soroban +description: Dive into the simple implementations of Soroban dapps to understand and learn the Soroban ecosystem. +--- + +## Demonstrative Soroban Dapps + +import ReactPlayer from "react-player"; + +The **Soroban** team has invested considerable effort into contract implementation. They’ve built CLI’s and libraries, enabling contract builders to create and invoke using Rust, which forms the “backend” of Soroban. However, the “frontend”, which involves JS client libraries, required attention. + +## Background + +During the Soroban Hackathon, we recognized that while the frontend libraries were functional, their user experience was far from ideal. Furthermore, the lack of clear examples made it harder for dapp creators to design and understand Soroban's UX. + +To tackle this, the **Wallet Engineering** team, in collaboration with the Soroban team, has decided to launch “Scaffold Soroban”, a collection of demo dapps. These dapps demonstrate basic functionalities in a structured, easy-to-follow manner, primarily focusing on how to construct/deploy Soroban contract invocations. + +## Dapp Demos + +For easy accessibility, we have compiled the dapps into a [single repository](https://github.com/stellar/scaffold-soroban), which contains the following dapps: + +### [1. Payment Dapp](https://github.com/stellar/soroban-react-payment) + +This dapp mirrors the Soroban payment flow in Freighter by using the wallet’s Soroban token balances to invoke the `xfer` method on the token’s contract. + +See the [demo](https://github.com/stellar/soroban-react-payment/releases/tag/v1.0.0) + + + +### [2. Mint Token Dapp](https://github.com/stellar/soroban-react-mint-token) + +This dapp allows a token admin to mint tokens by using the admin account to invoke the `mint` method on the token’s contract. + +See the [demo](https://github.com/stellar/soroban-react-mint-token/releases/tag/v1.0.0) + + + +### [3. Atomic Swap Dapp](https://github.com/stellar/soroban-react-atomic-swap) + +This dapp demonstrates a simplified swap between two tokens by using the wallet’s Soroban token balances to invoke the `swap` method on the atomic swap contract. + +See the [demo](https://github.com/stellar/soroban-react-atomic-swap/releases/tag/v1.0.0) + + + +## How To Explore the Dapps on Scaffold-Soroban + +To begin using these examples, navigate to [Scaffold-Soroban](https://scaffold-soroban.stellar.org/) and choose the name of the dapp you're interested in from the "select demo" dropdown: + +- **Payment**: Choose "payment". +- **Token Minter**: Select "mint-token". +- **Atomic Swap**: Opt for "atomic-swap". + +Dive in and discover the power of Soroban! + +## Functionality Behind the Dapps + +With the introduction of these dapps, let's delve deeper into some of their [standout features](https://github.com/stellar/soroban-react-atomic-swap/blob/main/src/helpers/soroban.ts) that showcase the power and innovation behind the dapps: + +Functionality behind the dapps is extensive and diverse, leveraging the `@stellar/stellar-sdk` library to integrate with the Soroban RPC and facilitating direct communication with Soroban using a JSON RPC interface on the Stellar network. They are equipped to communicate across different network setups, as they incorporate the `RPC_URLS` and `getServer` functionalities, providing adaptability that is crucial for various development and deployment scenarios. Users can retrieve user-friendly token information without engaging with complex blockchain operations, by utilizing functions like `getTokenSymbol`, `getTokenName`, and `getTokenDecimals`. + +A significant feature is the `simulateTx` function, which allows users to preview the outcome of a transaction before actually executing it. `simulateTx` allows users to submit a trial contract invocation by first running a simulation of the contract invocation as defined on the incoming transaction. The results are then applied to a new copy of the transaction, which is returned with the ledger footprint and authorization set, making it ready for signing and sending. The returned transaction will also have an updated fee, the sum of the fee set on the incoming transaction with the contract resource fees estimated from the simulation. It is advisable to check the fee on the returned transaction and validate or take appropriate measures for interaction with the user to confirm it is acceptable. + +Other utilities such as `accountToScVal` and `numberToI128` are also provided to simplify transaction creation by converting user-friendly inputs into the formats expected by the Soroban RPC on the Stellar network. Furthermore, the dapps are built with premade helper functions such as the `makePayment` function which facilitates streamlined "transfer" operations and also includes memos for supplementary transaction-related information. + +The code referenced showcases various functionalities including sending transactions, retrieving token information, simulating transactions, building swaps, and authorizing contract calls, all of which are ready to be cloned, customized, and expanded upon to suit your unique needs and ideas. + +## Conclusion + +We hope these dapps will help you understand the Soroban ecosystem better and inspire you to build your own dapps using tools like [stellar-sdk](https://www.npmjs.com/package/@stellar/stellar-sdk) and [freighter-api](https://www.npmjs.com/package/@stellar/freighter-api). We look forward to seeing what you create! + +For any queries or discussions, don't hesitate to join us on [Discord!](https://discord.com/channels/897514728459468821/1037073682599780494). diff --git a/docs/smart-contracts/guides/dapps/README.mdx b/docs/smart-contracts/guides/dapps/README.mdx new file mode 100644 index 000000000..d19dc3fbf --- /dev/null +++ b/docs/smart-contracts/guides/dapps/README.mdx @@ -0,0 +1,5 @@ +--- +title: Dapp Development +--- + +We've written some helpful guides on some of the most useful tools available to you, the dapp developer. diff --git a/docs/smart-contracts/guides/dapps/docker.mdx b/docs/smart-contracts/guides/dapps/docker.mdx new file mode 100644 index 000000000..e2bd465ed --- /dev/null +++ b/docs/smart-contracts/guides/dapps/docker.mdx @@ -0,0 +1,136 @@ +--- +title: Use Docker to build and run dapps +--- + +## What is Docker? + +Welcome to the world of [Docker](https://www.docker.com/), an essential tool for software development. Docker packages software into units known as containers, ensuring consistency, isolation, portability, and scalability. + +Docker is particularly useful in dapp development. It helps manage microservices, maintain consistent environments throughout development stages, and simulate a decentralized network during testing. + +Understanding Docker begins with understanding Docker images and containers. A Docker image, created from a Dockerfile, is a package that contains everything needed to run the software. A Docker container is a running instance of this image. + +## Building and Running a Docker Image + +You can create a Docker image using the docker build command with a Dockerfile. Once the image is created, you can run a Docker container using the docker run command. + +In the context of the example Soroban dapps, understanding how to build Docker images is crucial. The Docker images serve as the basis for our container, which provides the environment for our dapp to run. + +Here's an example from our example + +To illustrate the process, let's take an example from our [example crowdfund dapp]. In order to build the Docker image, you utilize a command that is encapsulated within our Makefile: + +```bash +make build-docker +``` + +This command simplifies the Docker build process and ensures it's consistently executed each time. When you run `make build-docker`, Docker executes the following instructions: + +```bash +docker build . \ + --tag soroban-preview:11 \ + --force-rm \ + --rm +``` + +### Makefile Overview + +```bash +docker build . +``` + +Instructs Docker to build an image using the Dockerfile in the current directory (denoted by the "."). + +```bash +--tag soroban-preview:11 +``` + +Gives a name and tag to our image, in this case, soroban-preview with the tag 9. + +```bash +--force-rm +``` + +Ensures Docker removes any intermediate containers after the build process completes. This keeps our environment clean. + +```bash +--rm +``` + +Guarantees the removal of the intermediate container, even if the build fails. By using `make build-docker`, you're harnessing the power of Docker to create a consistent, reliable environment for our dapp. + +## Container Deployment + +You can streamline the deployment process by using a script to run the Docker container. The following script is a wrapper for the [`stellar/quickstart` Docker image], which provides a quick way to run a Stellar network. You can find an example of the `quickstart.sh` script located in the root directory of the [example crowdfund dapp]. + +```bash title="quickstart.sh" +#!/bin/bash + +set -e + +case "$1" in +standalone) + echo "Using standalone network" + ARGS="--standalone" + ;; +futurenet) + echo "Using Futurenet network" + ARGS="--futurenet" + ;; +*) + echo "Usage: $0 standalone|futurenet" + exit 1 + ;; +esac + +shift + +# Run the soroban-preview container +# Remember to do: +# make build-docker + +echo "Creating docker soroban network" +(docker network inspect soroban-network -f '{{.Id}}' 2>/dev/null) \ + || docker network create soroban-network + +echo "Searching for a previous soroban-preview docker container" +containerID=$(docker ps --filter="name=soroban-preview" --all --quiet) +if [[ ${containerID} ]]; then + echo "Start removing soroban-preview container." + docker rm --force soroban-preview + echo "Finished removing soroban-preview container." +else + echo "No previous soroban-preview container was found" +fi + +currentDir=$(pwd) +docker run -dti \ + --volume ${currentDir}:/workspace \ + --name soroban-preview \ + -p 8001:8000 \ + --ipc=host \ + --network soroban-network \ + soroban-preview:11 + +# Run the stellar quickstart image + +docker run --rm -ti \ + --name stellar \ + --network soroban-network \ + -p 8000:8000 \ + stellar/quickstart:testing \ + $ARGS \ + --enable-soroban-rpc \ + "$@" # Pass through args from the CLI +``` + +The `quickstart.sh` script sets up the Docker environment for running the dapp. It allows you to choose between a standalone network or the Futurenet network. The script performs the following steps: + +- Determines the network based on the provided argument (`standalone` or `futurenet`). +- Creates the Docker network named `soroban-network` if it doesn't exist. +- Removes any existing `soroban-preview` Docker container. +- Runs the `soroban-preview` container, which provides the Soroban Preview environment for development. +- Runs the `stellar/quickstart` Docker image, which sets up the Stellar network using the chosen network type and enables Soroban RPC. + +[`stellar/quickstart` Docker image]: https://hub.docker.com/r/stellar/quickstart +[example crowdfund dapp]: https://github.com/stellar/soroban-example-dapp diff --git a/docs/smart-contracts/guides/dapps/initialization.mdx b/docs/smart-contracts/guides/dapps/initialization.mdx new file mode 100644 index 000000000..c3a90cb89 --- /dev/null +++ b/docs/smart-contracts/guides/dapps/initialization.mdx @@ -0,0 +1,214 @@ +--- +title: Initialize a dapp using scripts +--- + +When setting up an example Soroban Dapp, correct initialization is crucial. This process entails several steps, including deploying Docker, cloning and deploying smart contracts, and invoking functions to configure them. In this comprehensive guide, you will walk you through the necessary steps to successfully build and deploy these smart contracts, ensuring a seamless setup for your Soroban Dapp. + +## Building and Deploying the Soroban Token Smart Contract + +In dapps like the [Example Payment Dapp](https://github.com/stellar/soroban-react-payment/), the [Soroban Token smart contracts](https://github.com/stellar/soroban-examples/tree/main/token) are used to represent the tokenized asset that users can send and receive. Here is an example of how to build and deploy the Soroban Token smart contracts: + +Start by cloning the Soroban examples repository: + +```shell +git clone https://github.com/stellar/soroban-examples.git +``` + +Then, navigate to the `token` directory: + +```shell +cd soroban-examples/token +``` + +At this point you can build the smart contract: + +```shell +make +``` + +This action will compile the smart contracts and place them in the `token/target/wasm32-unknown-unknown/release` directory. + +After building, you're ready to deploy the smart contracts to Futurenet. To do this, open a terminal in the `soroban-examples/token` directory and execute the following: + +```shell +soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_token_contract.wasm \ + --source \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' +``` + +This command deploys the smart contracts to Futurenet using the `soroban contract deploy` function. + +## Initializing a Token Contract + +With the contracts deployed, it's time to initialize the token contract: + +```shell +soroban contract invoke \ + --id \ + --source-account \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' \ + -- initialize \ + --admin \ + --decimal 7 \ + --name '44656d6f20546f6b656e' \ + --symbol '"4454"' +``` + +This command requires certain inputs: + +- Administrator Account: This is the public key of the administrator account. The administrator has control and authority over the token contract, enabling management of various contract functionalities. Learn more about the administrator's role from the Soroban Token Interface. + +- Decimal Precision: The decimal precision value of 7 specifies that the token can support transactions up to 7 decimal places. This precision level enables flexibility when transferring token amounts. + +- Token Name: The token's name, represented as a hex-encoded string. In this case, '44656d6f20546f6b656e' corresponds to "Demo Token". + +- Token Symbol: This is the token's symbol, also represented as a hex string. '4454' translates to the symbol "DT". + +## Minting Tokens + +Lastly, you need to mint some tokens to the sender's account: + +```shell +soroban contract invoke \ + --id \ + --source-account \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' \ + -- mint \ + --to \ + --amount 1000000000 +``` + +This command will mint 100 tokens to the designated user's account. + +By following these steps, you ensure that the Soroban token smart contracts are correctly deployed and initialized, setting the stage for the Dapp to effectively interact with the token. + +For a deeper dive into Soroban CLI commands, check out the [Soroban CLI repo](https://github.com/stellar/soroban-cli/tree/main/cmd/soroban-cli/src/commands). + +## Automating Initialization with Scripts + +To streamline the initialization process, you can use a script. This script should automate various tasks such as setting up the network, wrapping Stellar assets, generating token-admin identities, funding the token-admin account, building and deploying the contracts, and initializing them with necessary parameters. + +Here's an example initializer script: + +```bash title="initialize.sh" +#!/bin/bash + +set -e + +NETWORK="$1" + +# If soroban-cli is called inside the soroban-preview docker container, +# it can call the stellar standalone container just using its name "stellar" +if [[ "$IS_USING_DOCKER" == "true" ]]; then + SOROBAN_RPC_HOST="http://stellar:8000" +else + SOROBAN_RPC_HOST="http://localhost:8000" +fi + +case "$1" in +standalone) + echo "Using standalone network" + SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017" + FRIENDBOT_URL="$SOROBAN_RPC_HOST/friendbot" + SOROBAN_RPC_URL="$SOROBAN_RPC_HOST/soroban/rpc" + ;; +futurenet) + echo "Using Futurenet network" + SOROBAN_NETWORK_PASSPHRASE="Test SDF Future Network ; October 2022" + FRIENDBOT_URL="https://friendbot-futurenet.stellar.org/" + SOROBAN_RPC_URL="https://rpc-futurenet.stellar.org" + ;; +testnet) + echo "Using Testnet network" + SOROBAN_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" + FRIENDBOT_URL="https://friendbot.stellar.org/" + SOROBAN_RPC_URL="https://soroban-testnet.stellar.org" + ;; +*) + echo "Usage: $0 standalone|futurenet|testnet" + exit 1 + ;; +esac + + +echo Add the $NETWORK network to cli client +soroban network add \ + --rpc-url "$SOROBAN_RPC_URL" \ + --network-passphrase "$SOROBAN_NETWORK_PASSPHRASE" "$NETWORK" + +if !(soroban keys ls | grep token-admin 2>&1 >/dev/null); then + echo Create the token-admin identity + soroban keys generate token-admin +fi +TOKEN_ADMIN_SECRET="$(soroban keys show token-admin)" +TOKEN_ADMIN_ADDRESS="$(soroban keys address token-admin)" + +# TODO: Remove this once we can use `soroban keys` from webpack. +mkdir -p .soroban-example-dapp +echo "$TOKEN_ADMIN_SECRET" > .soroban-example-dapp/token_admin_secret +echo "$TOKEN_ADMIN_ADDRESS" > .soroban-example-dapp/token_admin_address + +# This will fail if the account already exists, but it'll still be fine. +echo Fund token-admin account from friendbot +curl --silent -X POST "$FRIENDBOT_URL?addr=$TOKEN_ADMIN_ADDRESS" >/dev/null + +ARGS="--network $NETWORK --source token-admin" + +echo Deploy the Stellar asset contract +TOKEN_ID=$(soroban contract asset deploy $ARGS --asset "EXT:$TOKEN_ADMIN_ADDRESS") +echo "Token deployed successfully with TOKEN_ID: $TOKEN_ID" + +# TODO - remove this workaround when +# https://github.com/stellar/soroban-tools/issues/661 is resolved. +TOKEN_ADDRESS="$(node ./address_workaround.js $TOKEN_ID)" +echo "Token Address converted to StrKey contract address format:" $TOKEN_ADDRESS + +echo -n "$TOKEN_ID" > .soroban-example-dapp/token_id + +echo Build the crowdfund contract +make build + +echo Deploy the crowdfund contract +CROWDFUND_ID="$( + soroban contract deploy $ARGS \ + --wasm target/wasm32-unknown-unknown/release/soroban_crowdfund_contract.wasm +)" +echo "Contract deployed successfully with ID: $CROWDFUND_ID" +echo "$CROWDFUND_ID" > .soroban-example-dapp/crowdfund_id + +echo "Initialize the crowdfund contract" +deadline="$(($(date +"%s") + 86400))" +soroban contract invoke \ + $ARGS \ + --id "$CROWDFUND_ID" \ + -- \ + initialize \ + --recipient "$TOKEN_ADMIN_ADDRESS" \ + --deadline "$deadline" \ + --target_amount "1000000000" \ + --token "$TOKEN_ADDRESS" + +echo "Done" +``` + +Here's a summary of what the `initialize.sh` script does: + +- Identifies the network (standalone or futurenet) based on user input +- Determines the Soroban RPC host URL depending on its execution environment (either inside the soroban-preview Docker container or locally) +- Sets the Soroban RPC URL based on the previously determined host URL +- Sets the Soroban network passphrase and Friendbot URL depending on the chosen network +- Adds the network configuration to Soroban using `soroban network add` +- Generates a token-admin identity using `soroban keys generate` +- Fetches the TOKEN_ADMIN_SECRET and TOKEN_ADMIN_ADDRESS from the newly generated identity +- Saves the TOKEN_ADMIN_SECRET and TOKEN_ADMIN_ADDRESS in the .soroban directory +- Funds the token-admin account using Friendbot +- Deploy the Stellar asset contract with `soroban contract asset deploy` and stores the resulting TOKEN_ID +- Builds the crowdfund contract with `make build` and deploys it using `soroban contract deploy`, storing the returned CROWDFUND_ID +- Initializes the crowdfund contract by invoking the initialize function with necessary parameters +- Prints "Done" to signify the end of the initialization process + +By leveraging automated initialization, you can streamline the setup process for your Soroban Dapp, ensuring it is correctly deployed and initialized. diff --git a/docs/smart-contracts/guides/dapps/react.mdx b/docs/smart-contracts/guides/dapps/react.mdx new file mode 100644 index 000000000..ef2f0cc3f --- /dev/null +++ b/docs/smart-contracts/guides/dapps/react.mdx @@ -0,0 +1,140 @@ +--- +title: Create a frontend for your dapp using React +--- + +import { Alert } from "@site/src/components/Alert"; + +This section elaborates on how the frontends from your dapp can interact with the example contracts and access chain data, and connect to a freighter wallet. This will be illustrated by utilizing libraries provided by [`@soroban-react`](https://soroban-react.gitbook.io/index/), a simple, powerful framework for building modern Dapps using React. `@soroban-react` was created and is maintained by an amazing member of the community! + + + +This guide will demonstrate how an [example crowdfund dapp] frontend was developed with React. While much of the code is specific to this project, the principles demonstrated should be educational enough to get you started. + + + +Below is a list of the libraries used throughout the frontend code and their respective imports: + +```jsx +import { SorobanReactProvider } from "@soroban-react/core"; +import { testnet, sandbox, standalone } from "@soroban-react/chains"; +import { freighter } from "@soroban-react/freighter"; +import { ChainMetadata, Connector } from "@soroban-react/types"; +import type { + WalletChain, + ChainMetadata, + ChainName, +} from "@soroban-react/types"; +import { useSorobanReact } from "@soroban-react/core"; +``` + +These imports include `SorobanReactProvider` from `@soroban-react/core`, which is a context provider used to pass the SorobanReact instance to other components. You also import several types such as `WalletChain`, `ChainMetadata`, and `ChainName`, which help to maintain type safety within our application. + +## React Components and Prop Passing + +React thrives on its component-based architecture. Components are reusable pieces of code that return a React element to be rendered on the page. A typical React application consists of multiple components working harmoniously to create a dynamic user interface. + +Let's look at a component from the the [example crowdfund dapp], the [`MintButton` component](https://github.com/stellar/soroban-example-dapp/blob/07504b922bc75a48e5220711aea2cb4962f90367/components/molecules/form-pledge/index.tsx#L27): + +```tsx +function MintButton({ + account, + decimals, + symbol, +}: { + account: string; + decimals: number; + symbol: string; +}) { + const [isSubmitting, setSubmitting] = useState(false); + const { activeChain, server } = useNetwork(); + const networkPassphrase = activeChain?.networkPassphrase ?? ""; + const { sendTransaction } = useSendTransaction(); + const amount = BigNumber(100); + + return +
+
+ {images.map((image, index) => ( +
+ +
+ ))} +
+
+ + + ); +} diff --git a/src/components/atoms/UI/carousel/style.module.css b/src/components/atoms/UI/carousel/style.module.css new file mode 100644 index 000000000..58d6e904b --- /dev/null +++ b/src/components/atoms/UI/carousel/style.module.css @@ -0,0 +1,12 @@ +.image_holder { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px 12px; + gap: 8px; + border-radius: 8px; + border: 0; + height: auto; + color: #ffffff; +} diff --git a/src/components/atoms/UI/loading/index.tsx b/src/components/atoms/UI/loading/index.tsx new file mode 100644 index 000000000..7dfd772fc --- /dev/null +++ b/src/components/atoms/UI/loading/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import Image from "next/image"; +import { LoadingSvg } from "../../../assets/icons"; + +export interface SpacerProps { + size: number; +} + +export function Loading({ size }: SpacerProps) { + return loading...; +} diff --git a/src/components/atoms/UI/switcher/index.tsx b/src/components/atoms/UI/switcher/index.tsx new file mode 100644 index 000000000..53d262959 --- /dev/null +++ b/src/components/atoms/UI/switcher/index.tsx @@ -0,0 +1,34 @@ +import React, { ChangeEvent, useState } from "react"; +import styles from "./style.module.css"; + +interface SwitcherProps { + id: string; + labelText?: string; + onChange?: (value: boolean) => void; +} + +export default function Switcher({ id, labelText, onChange }: SwitcherProps) { + const [isSwitched, setIsSwitched] = useState(false); + const switcherClasses = isSwitched + ? `${styles.switcher} ${styles.switcherOn}` + : styles.switcher; + + const changeHandler = ({ target }: ChangeEvent) => { + setIsSwitched(target.checked); + onChange && onChange(target.checked); + }; + + return ( +
+ {labelText} + +
+ ); +} diff --git a/src/components/atoms/UI/switcher/style.module.css b/src/components/atoms/UI/switcher/style.module.css new file mode 100644 index 000000000..8cbc0a1c8 --- /dev/null +++ b/src/components/atoms/UI/switcher/style.module.css @@ -0,0 +1,51 @@ +.switcherWrapper { + display: flex; + align-items: center; +} + +.switcher { + display: block; + width: 24px; + height: 16px; + border: 2px solid #369EA7; + border-radius: 16px; + background-color: transparent; + position: relative; + transition: all .2s ease-in-out; + cursor: pointer; +} + +.switcher::before { + content: ''; + display: block; + width: 4px; + height: 4px; + border: 2px solid #369EA7; + background-color: transparent; + border-radius: 50%; + position: absolute; + top: 2px; + left: 2px; +} + +.switcherOn { + background-color: #369EA7; +} + +.switcher.switcherOn::before { + background-color: #FFFFFF; + border-color: #FFFFFF; + left: unset; + right: 2px; +} + +.switcherLabel { + color: #585858; + font-size: 14px; + margin-right: 10px; +} + +.switcherInput { + width: 0; + height: 0; +} \ No newline at end of file diff --git a/src/components/atoms/challenge-card/challenge-icons.tsx b/src/components/atoms/challenge-card/challenge-icons.tsx new file mode 100644 index 000000000..3cc5833c1 --- /dev/null +++ b/src/components/atoms/challenge-card/challenge-icons.tsx @@ -0,0 +1,55 @@ +import React from "react"; + +/* Constants with icons markup to render inside challenge cards +In such way we can change styles of the icon on card hover */ +export const iconBulb = ( + + + + + +); + +export const iconWallet = ( + + + + + + + + + +); diff --git a/src/components/atoms/challenge-card/index.tsx b/src/components/atoms/challenge-card/index.tsx new file mode 100644 index 000000000..3cf306397 --- /dev/null +++ b/src/components/atoms/challenge-card/index.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import styles from "./style.module.css"; +import { iconBulb, iconWallet } from "./challenge-icons"; +import { ChallengeInfo } from "../../../interfaces/challenge"; + +interface ChallengeCardProps { + challenge: ChallengeInfo; +} + +interface ChallengeConfig { + icon: JSX.Element; + route: string; + lastCheckpointRoute?: string; +} + +enum ActionBtnTitle { + SEE_DETAILS = "See details", + CONTINUE = "Continue", + PENDING = "Pending", + COMPLETED = "Completed", +} + +/* Config with challenge icon, page route and last checkpoint route in +case the challenge might have Pending status and require PR submission */ +const challengeConfig: { [key: string]: ChallengeConfig } = { + 0: { + icon: iconBulb, + route: "/docs/learn/interactive/dapps/challenges/challenge-0-crowdfund", + lastCheckpointRoute: + "/docs/learn/interactive/dapps/challenges/challenge-0-crowdfund#checkpoint-7--check-your-work", + }, + 1: { + icon: iconWallet, + route: "/docs/learn/interactive/dapps/challenges/challenge-1-payment", + }, + 2: { + icon: iconBulb, + route: "/docs/learn/interactive/dapps/challenges/challenge-2-liquidity-pool", + }, + 3: { + icon: iconBulb, + route: "/docs/learn/interactive/dapps/challenges/challenge-3-oracle", + lastCheckpointRoute: + "/docs/learn/interactive/dapps/challenges/challenge-3-oracle#checkpoint-8--check-your-work", + }, +}; +const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +export function ChallengeCard({ challenge }: ChallengeCardProps) { + const { id, name, milestonesAmount: totalSteps } = challenge; + let actionBtnTitle: ActionBtnTitle = ActionBtnTitle.SEE_DETAILS; + let displayDate: string; + const progressValue = (Number(challenge.progress) * 100) / totalSteps || 0; + + const setDisplayDate = (date: number) => { + const resultDate = new Date(date); + const month = months[resultDate.getUTCMonth()]; + const day = resultDate.getUTCDate(); + const year = resultDate.getUTCFullYear(); + + return { + month, + day, + year, + }; + }; + + if (challenge.startDate) { + const { month, day, year } = setDisplayDate(challenge.startDate); + actionBtnTitle = ActionBtnTitle.CONTINUE; + displayDate = `Started on ${month}, ${day}, ${year}`; + } + + if (challenge.completedAt) { + const { month, day, year } = setDisplayDate(challenge.completedAt); + actionBtnTitle = ActionBtnTitle.PENDING; + displayDate = `Passed on ${month}, ${day}, ${year}`; + } + + if (challenge.isCompleted) { + const { month, day, year } = setDisplayDate(challenge.completedAt); + actionBtnTitle = ActionBtnTitle.COMPLETED; + displayDate = `Completed on ${month}, ${day}, ${year}`; + } + + const showCompleteNote = + actionBtnTitle === ActionBtnTitle.PENDING && + challengeConfig[id].lastCheckpointRoute; + + const shouldDisableAction = + actionBtnTitle === ActionBtnTitle.COMPLETED || + actionBtnTitle === ActionBtnTitle.PENDING; + + return ( +
  • +
    + {challengeConfig[id].icon} + +
    {progressValue.toFixed()}/100%
    +
    +
    +

    + {name} +

    + +
    +
    +
    +
    + + {showCompleteNote && ( +

    + In order to complete this challenge, check your CI/CD results on a + pull request created on{" "} + + this checkpoint + +

    + )} +
    + +
    + {displayDate} + + + {actionBtnTitle} + +
    +
  • + ); +} diff --git a/src/components/atoms/challenge-card/style.module.css b/src/components/atoms/challenge-card/style.module.css new file mode 100644 index 000000000..d8052adf6 --- /dev/null +++ b/src/components/atoms/challenge-card/style.module.css @@ -0,0 +1,141 @@ +.card { + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid #ECECEC; + background-color: #ffffff; + list-style: none; + padding: 16px; + font-family: var(--ifm-font-family-base); + transition: all .3s ease-in-out; +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cardTitle { + font-size: 18px; + font-weight: 600; + margin-bottom: 24px; +} + +.cardTitle a { + color: #222222; +} + +.cardContent { + padding: 32px 0 55px; + border-bottom: 1px solid #ECECEC; + position: relative; +} + +.cardFooter { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 16px; +} + +.date { + font-size: 12px; + color: #9F9F9F; +} + +.progress { + border-radius: 16px; + border: 1px solid #ECECEC; + background: #F9F9F9; + padding: 8px 16px; + font-size: 12px; + font-weight: 500; + line-height: 1; +} + +.action { + font-size: 14px; + font-weight: 600; + background-color: #FFD748; + color: #222222; + border-radius: 4px; + border: none; + padding: 16px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; +} + +.action[aria-disabled="true"] { + background-color: #ECECEC; + color: #9F9F9F; + pointer-events: none; +} + +.progressbar { + width: 100%; + height: 4px; + background-color: #222222; + display: flex; + align-items: center; +} + +.progressLine { + height: 4px; + background-color: #FFD234; +} + +.slider { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #FFD234; +} + +.cardNote { + font-size: 12px; + font-weight: 500; + margin: 15px 0 0; + position: absolute; + bottom: 6px; +} + +/* Styles on card hover */ + +.card:hover { + background-color: #FFD748; +} + +.card:hover .progress { + border-color: #222222; +} + +.card:hover .cardContent { + border-color: #ffffff; +} + +.card:hover .date { + color: #222222; +} + +.card:hover .action { + text-decoration: none; +} + +.card:hover .action:not([aria-disabled="true"]) { + background-color: #222222; + color: #ffffff; +} + +.card:hover circle { + fill: black; +} + +.card:hover .progressLine { + background-color: #ffffff; +} + +.card:hover .slider { + background-color: #ffffff; +} \ No newline at end of file diff --git a/src/components/atoms/challenge-contract-form/index.tsx b/src/components/atoms/challenge-contract-form/index.tsx new file mode 100644 index 000000000..738b121ee --- /dev/null +++ b/src/components/atoms/challenge-contract-form/index.tsx @@ -0,0 +1,124 @@ +import React, { + useState, + useEffect, + useContext, + ChangeEvent, + FormEvent, +} from "react"; +import BrowserOnly from "@docusaurus/BrowserOnly"; +import styles from "./style.module.css"; +import CompleteStepButton from "../complete-step-button"; +import UserChallengesContext, { + UserChallengesContextProps, +} from "../../../store/user-challenges-context"; +import { getActiveChallenge } from "../../../utils/get-active-challenge"; +import useAuth from "../../../hooks/useAuth"; + +interface ChallengeFormProps { + id: number; + address?: string; +} + +function ChallengeContractForm({ address, id }: ChallengeFormProps) { + const [savedContractId, setSavedContractId] = useState(""); + const [contractId, setContractId] = useState(""); + const [isStarted, setIsStarted] = useState(false); + const [formError, setFormError] = useState(null); + const { data } = useContext( + UserChallengesContext, + ); + const isSubmitBtnDisabled = !contractId || savedContractId === contractId; + + useEffect(() => { + if (address) { + const challenge = getActiveChallenge(data, id); + setSavedContractId(challenge?.contractId || ""); + setIsStarted(!!challenge?.startDate); + } + }, [address, savedContractId, data, id]); + + const changeHandler = (event: ChangeEvent) => { + setContractId(event.target.value); + }; + + const blurHandler = () => { + if (!contractId) { + setFormError("Mandatory field"); + } else { + setFormError(null); + } + }; + + if (!isStarted) { + return ( + <> + + Connect your wallet to track your progress and submit ContractId. + +
    + + ); + } + + return ( +
    + {savedContractId ? ( +

    ContractId submitted!

    + ) : null} + +
    e.preventDefault()} + > + + + + {savedContractId ? "Re-submit" : "Submit contractId"} + +
    + {formError} +
    + ); +} + +function InnerComponent({ id }: { id: number }) { + const { loading, address } = useAuth(); + + // if user is not logged in (address is undefined), render the Login button + if (loading) { + return ( +
    + Please connect to Testnet network. +
    +
    + ); + } + // if user is logged in and connected to the right network, + // render the ChallengeForm + return ; +} + +export function ParentChallengeContractForm({ id }: { id: number }) { + return ( + Please connect to Testnet network.}> + {() => } + + ); +} diff --git a/src/components/atoms/challenge-contract-form/style.module.css b/src/components/atoms/challenge-contract-form/style.module.css new file mode 100644 index 000000000..ad04774b1 --- /dev/null +++ b/src/components/atoms/challenge-contract-form/style.module.css @@ -0,0 +1,94 @@ +.challengeform { + display: flex; + flex-direction: row; + justify-content: left; + align-items: center; + gap: 16px; + margin-right: 1rem; +} + +/* .displayData { + display: flex; +} */ + +.button { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px 12px; + gap: 8px; + background: #1a1523; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 14px; + line-height: 22px; + color: #f5f5f5; + cursor: pointer; + align-self: baseline; +} + +.success { + display: flex; + flex-direction: column; + justify-content: center; + align-items: left; + gap: 8px; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 18px; + line-height: 22px; + color: #1a1523; + cursor: pointer; + align-self: baseline; +} + + +[data-theme="dark"] .button { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px 12px; + gap: 8px; + background: #767676; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 14px; + line-height: 22px; + color: #f5f5f5; + cursor: pointer; + align-self: baseline; +} + +.input { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + align-items: center; + padding: 11px; + gap: 8px; + background: #ffffff; + border-radius: 2px; + border: 0; + font-size: 16px; + color: #585858; + border: 1px solid #ECECEC; + cursor: pointer; +} + +.inputWithError { + border-color: #DF0101; +} + +.errorMessage { + position: absolute; + color: #DF0101; +} \ No newline at end of file diff --git a/src/components/atoms/challenge-form/index.tsx b/src/components/atoms/challenge-form/index.tsx new file mode 100644 index 000000000..25245d70d --- /dev/null +++ b/src/components/atoms/challenge-form/index.tsx @@ -0,0 +1,147 @@ +import React, { + useState, + useEffect, + useContext, + ChangeEvent, + FormEvent, +} from "react"; +import BrowserOnly from "@docusaurus/BrowserOnly"; +import styles from "./style.module.css"; +import CompleteStepButton from "../complete-step-button"; +import UserChallengesContext, { + UserChallengesContextProps, +} from "../../../store/user-challenges-context"; +import { getActiveChallenge } from "../../../utils/get-active-challenge"; +import useAuth from "../../../hooks/useAuth"; + +interface ChallengeFormProps { + id: number; + address?: string; +} + +function ChallengeForm({ address, id }: ChallengeFormProps) { + const [savedUrl, setSavedUrl] = useState(""); + const [url, setUrl] = useState(""); + const [isStarted, setIsStarted] = useState(false); + const [formError, setFormError] = useState(null); + const { data } = useContext( + UserChallengesContext, + ); + const isSubmitBtnDisabled = !url || savedUrl === url; + + useEffect(() => { + if (address) { + const challenge = getActiveChallenge(data, id); + setSavedUrl(challenge?.url || ""); + setIsStarted(!!challenge?.startDate); + } + }, [address, savedUrl, data, id]); + + const isValidUrl = (urlString: string): boolean => { + try { + return Boolean(new URL(urlString)); + } catch (e) { + return false; + } + }; + + const changeHandler = (event: ChangeEvent) => { + const inputValue = event.target.value; + const isVercelApp = inputValue.includes(".vercel.app"); + + setFormError(null); + + if (!isValidUrl(inputValue)) { + setFormError("Please enter a valid url"); + return; + } + + if (!isVercelApp) { + setFormError("URL should contain .vercel.app to complete the checkpoint"); + } else { + setUrl(inputValue); + } + }; + + const blurHandler = () => { + if (!url) { + setFormError("Mandatory field"); + } + }; + + if (!isStarted) { + return ( + <> + + Start the challenge to track your progress and submit the url. + +
    + + ); + } + + return ( +
    + {savedUrl ? ( +

    + Public url submitted! Your DApp is deployed to: + {savedUrl} +

    + ) : null} + +
    e.preventDefault()} + > + + + + {savedUrl ? "Re-submit" : "Submit url"} + +
    + {formError} +
    + ); +} + +function InnerComponent({ id }: { id: number }) { + const { loading, address } = useAuth(); + + // if user is not logged in (address is undefined), render the Login button + if (loading) { + return ( +
    + Please connect to Testnet or Futurenet network. +
    +
    + ); + } + // if user is logged in and connected to the right network, + // render the ChallengeForm + return ; +} + +export function ParentChallengeForm({ id }: { id: number }) { + return ( + Please connect to Testnet or Futurenet network.}> + {() => } + + ); +} diff --git a/src/components/atoms/challenge-form/style.module.css b/src/components/atoms/challenge-form/style.module.css new file mode 100644 index 000000000..ad04774b1 --- /dev/null +++ b/src/components/atoms/challenge-form/style.module.css @@ -0,0 +1,94 @@ +.challengeform { + display: flex; + flex-direction: row; + justify-content: left; + align-items: center; + gap: 16px; + margin-right: 1rem; +} + +/* .displayData { + display: flex; +} */ + +.button { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px 12px; + gap: 8px; + background: #1a1523; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 14px; + line-height: 22px; + color: #f5f5f5; + cursor: pointer; + align-self: baseline; +} + +.success { + display: flex; + flex-direction: column; + justify-content: center; + align-items: left; + gap: 8px; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 18px; + line-height: 22px; + color: #1a1523; + cursor: pointer; + align-self: baseline; +} + + +[data-theme="dark"] .button { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px 12px; + gap: 8px; + background: #767676; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 14px; + line-height: 22px; + color: #f5f5f5; + cursor: pointer; + align-self: baseline; +} + +.input { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + align-items: center; + padding: 11px; + gap: 8px; + background: #ffffff; + border-radius: 2px; + border: 0; + font-size: 16px; + color: #585858; + border: 1px solid #ECECEC; + cursor: pointer; +} + +.inputWithError { + border-color: #DF0101; +} + +.errorMessage { + position: absolute; + color: #DF0101; +} \ No newline at end of file diff --git a/src/components/atoms/challenges-list/index.tsx b/src/components/atoms/challenges-list/index.tsx new file mode 100644 index 000000000..2c449d6d2 --- /dev/null +++ b/src/components/atoms/challenges-list/index.tsx @@ -0,0 +1,96 @@ +import React, { useMemo, useState } from "react"; +import styles from "./style.module.css"; +import { Challenge, ChallengeInfo } from "../../../interfaces/challenge"; +import { ChallengeCard } from "../challenge-card"; +import Switcher from "../UI/switcher"; +import ConfirmModal from "../confirm-modal"; + +interface Props { + availableChallenges: Challenge[]; + userChallenges: ChallengeInfo[]; + onRefresh: () => void; + onReset?: () => void; +} + +export default function ChallengeList({ + availableChallenges, + userChallenges, + onRefresh, + onReset, +}: Props) { + const [onlyMine, setOnlyMine] = useState(false); + const [confirmReset, setConfirmReset] = useState(false); + + const onResetClick = () => { + setConfirmReset(true); + }; + + const onCancelClick = () => { + setConfirmReset(false); + }; + + const myChallanges = ( + <> + {userChallenges?.length ? ( + userChallenges?.map((challenge: ChallengeInfo) => { + return ; + }) + ) : ( +

    You haven't completed any challenges yet.

    + )} + + ); + + const allChallenges = useMemo( + () => + availableChallenges?.map((aChall: Challenge) => { + const inProgressChallenge = userChallenges.find( + (uChall: ChallengeInfo) => uChall.id === aChall.id, + ); + + if (inProgressChallenge) { + return ( + + ); + } + + return ; + }), + [availableChallenges, userChallenges], + ); + + return ( + <> +
    +
    + {userChallenges?.length > 0 ? ( + setOnlyMine(value)} + /> + ) : null} + +
    + {onReset && ( + + )} +
    + +
      + {onlyMine ? myChallanges : allChallenges} +
    + + {confirmReset && onReset && ( + + )} + + ); +} diff --git a/src/components/atoms/challenges-list/style.module.css b/src/components/atoms/challenges-list/style.module.css new file mode 100644 index 000000000..498a033fe --- /dev/null +++ b/src/components/atoms/challenges-list/style.module.css @@ -0,0 +1,54 @@ +.dashboardContent { + flex-grow: 2; + width: 75%; + margin: 32px auto; +} + +.challengeCards { + padding: 16px 0 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: max-content; + grid-gap: 30px; +} + +.listHeader { + display: flex; + justify-content: space-between; +} + +.dataControls { + display: flex; +} + +.refreshBtn { + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + background-color: transparent; + color: #369ea7; + border-radius: 4px; + border: 1px solid #369ea7; + padding: 12px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; +} + +.resetBtn { + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + background-color: transparent; + color: #df0101; + border-radius: 4px; + border: 1px solid #df0101; + padding: 12px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; +} + +div + .refreshBtn { + margin-left: 20px; +} diff --git a/src/components/atoms/complete-step-button/index.tsx b/src/components/atoms/complete-step-button/index.tsx new file mode 100644 index 000000000..da3a2a039 --- /dev/null +++ b/src/components/atoms/complete-step-button/index.tsx @@ -0,0 +1,236 @@ +import React, { + PropsWithChildren, + useContext, + useEffect, + useState, +} from "react"; +import { toast } from "react-toastify"; +import { useReward } from "react-rewards"; +import { AxiosResponse } from "axios"; +import useAuth from "../../../hooks/useAuth"; +import styles from "./styles.module.css"; +import UserChallengesContext, { + UserChallengesContextProps, +} from "../../../store/user-challenges-context"; +import { getActiveChallenge } from "../../../utils/get-active-challenge"; +import { + UserChallengeData, + ChallengeInfo, + UpdateProgressData, +} from "../../../interfaces/challenge"; +import { updateUserProgress } from "../../../services/challenges"; +import { getContractBalance } from "../../../utils/get-contract-balance"; + +interface CompleteStepButtonState { + isCompleted: boolean; + isLastStep: boolean; + isStarted: boolean; +} + +interface CompleteStepButtonProps extends PropsWithChildren { + type?: "button" | "submit"; + isDisabled?: boolean; + id: number; + progress: number; + url?: string; + contractId?: string; +} + +const milestoneToast = ( +
    + Smiley face + + Congratulations on your milestone! + +
    +); + +const completedToast = ( +
    + Smiley face + + Congratulations! Your challenge is completed successfully. + +
    +); + +const passedToast = ( +
    + Smiley face + + Congratulations on passing the challenge! Please, proceed to the next + checkpoint so that we can check your work. + +
    +); + +export default function CompleteStepButton({ + type, + isDisabled, + children, + id, + progress, + url, + contractId, +}: CompleteStepButtonProps) { + const [challenge, setChallenge] = useState(null); + const [state, setState] = useState({ + isCompleted: false, + isLastStep: false, + isStarted: false, + }); + const { data, updateProgress } = useContext( + UserChallengesContext, + ); + const { address } = useAuth(); + const { reward, isAnimating } = useReward( + `reward${id}-${progress}`, + "confetti", + { + elementCount: 150, + zIndex: 10000, + position: "absolute", + angle: 90, + lifetime: 300, + colors: [ + "#FFD748", + "#369EA7", + "#FF6534", + "#DF0101", + "#34CEFF", + "#AB56FF", + ], + }, + ); + + const isButtonDisabled = + (state.isCompleted && !state.isLastStep) || isDisabled || isAnimating; + + useEffect(() => { + setChallenge(getActiveChallenge(data, id)); + const isStepCompleted = !!challenge && challenge.progress >= progress; + const isLastCourseStep = !!(challenge?.milestonesAmount === progress); + + setState((prevState: CompleteStepButtonState) => { + return { + isCompleted: isStepCompleted, + isLastStep: isLastCourseStep, + isStarted: !!challenge?.startDate, + }; + }); + }, [challenge, data, progress, id]); + + const showToast = (template: JSX.Element) => { + toast(template, { + hideProgressBar: true, + position: "top-center", + autoClose: 3000, + }); + }; + + const postUserProgress = async (updatedItem: UpdateProgressData) => { + const response: AxiosResponse = await updateUserProgress( + updatedItem, + ); + updateProgress(response.data.challenge); + }; + + const lastStepHandler = async () => { + await postUserProgress({ + userId: address, + challengeId: id, + challengeProgress: progress, + url, + completedAt: Date.now(), + startDate: challenge?.startDate, + contractId: challenge?.contractId, + totalValueLocked: challenge?.totalValueLocked, + }); + + showToast(challenge?.isPullRequestRequired ? passedToast : completedToast); + reward(); + }; + + const completeStepHandler = async () => { + if (state.isLastStep) { + lastStepHandler(); + return; + } + + let balance = 0; + + // if funding step => get contract balance (except payment for now) + if (progress === 2 && id !== 1) { + if (challenge?.contractId) { + try { + const result = await getContractBalance( + challenge?.contractId, + address, + ); + if (!result) { + toast("No locked balance found!", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + + return; + } + + balance = result; + } catch (error) { + console.error(error); + + toast("No locked balance found!", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + return; + } + } else { + toast("No contract id found!", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + return; + } + } + + setState((prevState: CompleteStepButtonState) => { + return { + ...prevState, + isCompleted: true, + }; + }); + + await postUserProgress({ + userId: address, + challengeId: id, + challengeProgress: progress, + startDate: challenge?.startDate, + contractId: contractId || challenge?.contractId, + totalValueLocked: challenge?.totalValueLocked || balance, + }); + + showToast(milestoneToast); + }; + + return state.isStarted ? ( +
    + +
    + ) : null; +} diff --git a/src/components/atoms/complete-step-button/styles.module.css b/src/components/atoms/complete-step-button/styles.module.css new file mode 100644 index 000000000..8d9df306f --- /dev/null +++ b/src/components/atoms/complete-step-button/styles.module.css @@ -0,0 +1,39 @@ +.completeStep { + display: flex; + justify-content: center; + align-self: center; +} + +.completeStepButton { + font-family: var(--ifm-font-family-base); + font-size: 14px; + font-weight: 600; + background-color: #FFD748; + color: #222222; + border-radius: 4px; + border: none; + padding: 16px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; +} + +.completeStepButton:disabled { + background-color: #ECECEC; + color: #9F9F9F; + cursor: not-allowed; +} + +.notification { + display: flex; + align-items: center; + padding-left: 16px; +} + +.notificationText { + color: #222222; + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + margin-left: 8px; +} \ No newline at end of file diff --git a/src/components/atoms/confirm-modal/index.tsx b/src/components/atoms/confirm-modal/index.tsx new file mode 100644 index 000000000..d55d0c1b4 --- /dev/null +++ b/src/components/atoms/confirm-modal/index.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import styles from "./style.module.css"; + +interface Props { + onCancel: () => void; + onReset: () => void; +} + +export default function ConfirmModal({ onCancel, onReset }: Props) { + return ( + <> +
    +
    +

    Reseting progress!

    + It may take up to minute!
    + Are you sure you want to continue? +
    + + +
    +
    + + ); +} diff --git a/src/components/atoms/confirm-modal/style.module.css b/src/components/atoms/confirm-modal/style.module.css new file mode 100644 index 000000000..d855ac2c0 --- /dev/null +++ b/src/components/atoms/confirm-modal/style.module.css @@ -0,0 +1,63 @@ +.blurContainer { + position: absolute; + height: 100vh; + width: 100vw; + overflow: hidden; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; +} + +.modalContainer { + position: absolute; + width: 350px; + height: 200px; + top: calc(50% - 100px); + left: calc(50% - 175px); + background-color: #ffffff; + padding: 20px; + border-radius: 8px; + text-align: center; + z-index: 1001; +} + +.title { + font-size: 18px; + font-weight: 600; +} + +.buttons { + margin-top: 20px; + display: flex; + justify-content: center; + gap: 20px; +} + +.cancelBtn { + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + background-color: transparent; + color: #369ea7; + border-radius: 4px; + border: 1px solid #369ea7; + padding: 12px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; +} + +.resetBtn { + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + background-color: transparent; + color: #df0101; + border-radius: 4px; + border: 1px solid #df0101; + padding: 12px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; +} diff --git a/src/components/atoms/dashboard-header/index.tsx b/src/components/atoms/dashboard-header/index.tsx new file mode 100644 index 000000000..9520428a0 --- /dev/null +++ b/src/components/atoms/dashboard-header/index.tsx @@ -0,0 +1,104 @@ +import React , { useState }from "react"; +import { toast } from "react-toastify"; +import { Ranking } from "interfaces/challenge"; +import styles from "./style.module.css"; +import useAuth from "../../../hooks/useAuth"; + +const AVATAR = `/icons/icon-avatar-${Math.floor(Math.random() * 10) + 1}.svg`; + +interface Props { + totalCompleted: number; + ranking: Ranking; +} + +const DashboardHeader: React.FC = ({ totalCompleted, ranking }) => { + const { address, disconnect } = useAuth(); + + const [showContent, setShowContent] = useState(false); + const addressEnding = address?.substring(address.length, address.length - 4); + + const copyUserAddress = () => { + navigator.clipboard.writeText(address); + + toast("Copied to clipboard!", { + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + }; + return ( +
    +

    Your dashboard

    + +
      +
    • + User avatar icon + +
      + +
      +
      {address}
      + Copy address to clipboard +
      + +
      +
    • + +
    • + Star icon +
      + + completed challenges +
      +
    • + +
    • + Ranking icon +
      + + + {showContent && ( +
      + {!ranking.current || !ranking.total ? ( + + Start a challenge to get a rank + + ) : ( + <> + + leaderboard ranking + + )} +
      + )} +
      +
    • +
    +
    + ); +}; + +export default DashboardHeader; diff --git a/src/components/atoms/dashboard-header/style.module.css b/src/components/atoms/dashboard-header/style.module.css new file mode 100644 index 000000000..e5c7576f8 --- /dev/null +++ b/src/components/atoms/dashboard-header/style.module.css @@ -0,0 +1,114 @@ +.dashboardHeader { + background-image: url("/static/img/dashboard-bg.png"); + background-size: cover; + background-position: center; + height: 200px; + color: #ffffff; + padding: 40px 0 0 12%; +} + +.dashboardTitle { + text-decoration: underline; + margin-left: 10px; +} + +.dashboardTabs { +} + +.toggleButton { + font-family: var(--ifm-font-family-base); + font-size: 14px; + font-weight: 600; + background-color: #FFD748; + color: #222222; + border-radius: 4px; + border: none; + padding: 8px 8px; + width: max-content; + cursor: pointer; +} + +.userInfo { + display: flex; + padding: 10px 0 0 10px; +} + +.userInfoItem { + display: flex; + align-items: flex-start; + list-style: none; +} + +.userInfoItem:nth-child(1) { + margin-right: 150px; +} + +.userInfoItem:nth-child(2) { + margin-right: 24px; +} + +.userAddress { + position: relative; + max-width: 132px; + font-weight: 600; +} + +.userAddress::after { + content: attr(data-truncate); + position: absolute; + left: 100%; + top: 0; + white-space: nowrap; +} + +.userAddress div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.completedChallenges { + display: flex; + flex-direction: column; +} + +.completedChallenges label { + font-size: 20px; + font-weight: 600; +} + +.completedChallenges span { + font-size: 12px; + color: #bdbdbd; +} + +.copyIcon { + position: absolute; + top: 1px; + right: -67px; + cursor: pointer; +} + +.avatarIcon { + margin-right: 10px; +} + +.statsIcon { + width: 30px; + height: 30px; + background-color: #444444; + padding: 6px; + border-radius: 50%; + margin-right: 10px; +} + +.logoutButton { + color: #369ea7; + background-color: transparent; + padding: 12px 8px; + border: none; + font-family: var(--ifm-font-family-base); + font-size: 14px; + font-weight: 500; + cursor: pointer; +} diff --git a/src/components/atoms/index.tsx b/src/components/atoms/index.tsx new file mode 100644 index 000000000..d32fd5f86 --- /dev/null +++ b/src/components/atoms/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { useSorobanReact } from "@soroban-react/core"; +import styles from "./style.module.css"; + +export interface ConnectButtonProps { + label: string; + isHigher?: boolean; +} + +export function ConnectButton({ label, isHigher }: ConnectButtonProps) { + const { connect } = useSorobanReact(); + const openConnectModal = async () => { + await connect(); + }; + + return ( + + ); +} diff --git a/src/components/atoms/start-challenge-button/index.tsx b/src/components/atoms/start-challenge-button/index.tsx new file mode 100644 index 000000000..13da59ce5 --- /dev/null +++ b/src/components/atoms/start-challenge-button/index.tsx @@ -0,0 +1,91 @@ +import React, { useContext, useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { AxiosResponse } from "axios"; +import styles from "./style.module.css"; +import useAuth from "../../../hooks/useAuth"; +import UserChallengesContext, { + UserChallengesContextProps, +} from "../../../store/user-challenges-context"; +import { getActiveChallenge } from "../../../utils/get-active-challenge"; +import { + UserChallengeData, + UpdateProgressData, +} from "../../../interfaces/challenge"; +import { + fetchUserProgress, + updateUserProgress, +} from "../../../services/challenges"; + +interface StartChallengeButtonProps { + id: number; +} + +const startedToast = ( +
    + Smiley face + + You’ve joined the challenge! Good luck! + +
    +); + +export default function StartChallengeButton({ + id, +}: StartChallengeButtonProps) { + const { address, isConnected, connect } = useAuth(); + const [isStarted, setIsStarted] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { setData, updateProgress } = useContext( + UserChallengesContext, + ); + + useEffect(() => { + const fetchProgress = async () => { + setIsLoading(true); + try { + const response = await fetchUserProgress(address?.toString() || ""); + const challenges = response.data.challenges || []; + const challenge = getActiveChallenge(challenges, id); + setData(challenges); + setIsStarted(!!challenge?.startDate); + } finally { + setIsLoading(false); + } + }; + + if (address) { + fetchProgress(); + } + }, [address]); + + const startChallenge = async () => { + const updatedItem: UpdateProgressData = { + userId: address?.toString() || "", + challengeId: id, + challengeProgress: 0, + startDate: Date.now(), + }; + + const response: AxiosResponse = await updateUserProgress( + updatedItem, + ); + updateProgress(response.data.challenge); + + toast(startedToast, { + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + setIsStarted(true); + }; + + return ( + + ); +} diff --git a/src/components/atoms/start-challenge-button/style.module.css b/src/components/atoms/start-challenge-button/style.module.css new file mode 100644 index 000000000..2807b9aa8 --- /dev/null +++ b/src/components/atoms/start-challenge-button/style.module.css @@ -0,0 +1,37 @@ +.button { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 12px 8px; + margin-bottom: 20px; + gap: 8px; + background: #FFD748; + color: #222222; + border-radius: 4px; + border: 0; + font-family: var(--ifm-font-family-base); + font-weight: 600; + font-size: 14px; + cursor: pointer; +} + +.button:disabled { + background-color: #ECECEC; + color: #9F9F9F; + cursor: not-allowed; +} + +.notification { + display: flex; + align-items: center; + padding-left: 16px; +} + +.notificationText { + color: #222222; + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + margin-left: 8px; +} \ No newline at end of file diff --git a/src/components/atoms/style.module.css b/src/components/atoms/style.module.css new file mode 100644 index 000000000..fb19c1513 --- /dev/null +++ b/src/components/atoms/style.module.css @@ -0,0 +1,21 @@ +.button { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px 12px; + gap: 8px; + background: #1a1523; + border-radius: 8px; + border: 0; + height: 38px; + font-weight: 600; + font-size: 14px; + line-height: 22px; + color: #ffffff; + cursor: pointer; +} + +.higher { + height: 58px; +} \ No newline at end of file diff --git a/src/components/molecules/SortableTable/index.js b/src/components/molecules/SortableTable/index.js new file mode 100644 index 000000000..ef169bcbb --- /dev/null +++ b/src/components/molecules/SortableTable/index.js @@ -0,0 +1,33 @@ +import React, { useState } from "react" +import styles from "./style.module.css"; + +const arrowDown = ; +const arrowUp = ; + +const SortableTable = (props) => { + const [isAscending, setIsAscending] = useState(false); + const [sortColumn, setSortColumn] = useState(null) + + const sortTable = (val) => { + const sortDirection = col === val ? !isAscending : false + setIsAscending(sortDirection) + setSortColumn(val) + } + + return ( + + + + + + + + + + {props.children} + +
    TitleLinkTags
    + ) +} + +export default SortableTable diff --git a/src/components/molecules/SortableTable/style.module.css b/src/components/molecules/SortableTable/style.module.css new file mode 100644 index 000000000..3a41004c6 --- /dev/null +++ b/src/components/molecules/SortableTable/style.module.css @@ -0,0 +1,4 @@ +.sortable { + width: 100%; + display: table; +} diff --git a/src/components/molecules/leaderboard/index.tsx b/src/components/molecules/leaderboard/index.tsx new file mode 100644 index 000000000..3d5dd02e3 --- /dev/null +++ b/src/components/molecules/leaderboard/index.tsx @@ -0,0 +1,178 @@ +import React, { useState } from "react"; +import { Leaderboard as LeaderboardI } from "../../../interfaces/challenge"; +import { + LeaderboardColumn, + LeaderboardParams, +} from "../../../services/leaderboard"; +import styles from "./style.module.css"; + +type Props = { + userId?: string; + list: LeaderboardI[]; + isLoading: boolean; + onLoad: (params: LeaderboardParams) => void; +}; + +const PAGE_SIZE = 10; + +const arrowDown = ; +const arrowUp = ; + +const Leaderboard: React.FC = ({ userId, list, isLoading, onLoad }) => { + const [col, setCol] = useState(null); + const [isAsc, setIsAsc] = useState(false); + + const [pageNumber, setPageNumber] = useState(1); + + const maxUsers = list[0]?.ranking.total; + const maxPage = Math.ceil(maxUsers / PAGE_SIZE); + + const onSort = (val: LeaderboardColumn) => { + // if click on current column ? change direction : set default direction + const nextAsc = col === val ? !isAsc : false; + setIsAsc(nextAsc); + onLoad({ + colName: val, + direction: nextAsc ? "asc" : "desc", + }); + setCol(val); + setPageNumber(1); + }; + + const onReset = () => { + setIsAsc(true); + setCol(null); + onLoad({}); + setPageNumber(1); + }; + + const onNext = () => { + const nextPageNumber = pageNumber + 1; + setPageNumber(nextPageNumber); + onLoad({ + pageNumber: nextPageNumber, + ...(col ? { colName: col } : {}), + direction: isAsc ? "asc" : "desc", + }); + }; + + const onPrev = () => { + const nextPageNumber = pageNumber - 1; + setPageNumber(nextPageNumber); + onLoad({ + pageNumber: nextPageNumber, + ...(col ? { colName: col } : {}), + direction: isAsc ? "asc" : "desc", + }); + }; + + const arrow = !isAsc ? arrowDown : arrowUp; + + return ( +
    + + + + + + + + + + + {isLoading ? null : ( + + {list.map((item) => { + const isCurrent = userId === item.userId; + return ( + + + + + + + + ); + })} + + )} +
    + Place + Address onSort(LeaderboardColumn.TotalValueLocked)} + > + Total Value Locked {col === LeaderboardColumn.TotalValueLocked ? arrow : null} + onSort(LeaderboardColumn.ChallengesCompleted)} + > + Number of challenges{" "} + {col === LeaderboardColumn.ChallengesCompleted ? arrow : null} + onSort(LeaderboardColumn.MinutesSpent)} + > + Minutes spent{" "} + {col === LeaderboardColumn.MinutesSpent ? arrow : null} +
    +
    + {item.ranking.current} +
    + {isCurrent ? ( + <> + you are here! + Star icon + + ) : null} +
    {`${item.userId}`}{item.totalValueLocked} + {item.challengesCompleted === 0 ? ( + "In Progress" + ) : ( + <>{item.challengesCompleted} + )} + + {item.minutesSpent === 0 ? ( + "In Progress" + ) : ( + <>{item.minutesSpent} + )} +
    + {isLoading ?
    Loading
    : null} +
    + + +
    +
    + ); +}; + +export default Leaderboard; diff --git a/src/components/molecules/leaderboard/style.module.css b/src/components/molecules/leaderboard/style.module.css new file mode 100644 index 000000000..02a481034 --- /dev/null +++ b/src/components/molecules/leaderboard/style.module.css @@ -0,0 +1,125 @@ +.leaderboard { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; + padding: 32px 0; +} + +.leadTable { + width: 100%; + display: table; + font-family: "Inter"; + font-size: 14px; + font-weight: 500; +} + +table.leadTable td, +th { + background-color: #ffffff; +} + +.leadTableHead { + background-color: var(--secondary-black); + color: #ffffff; +} + +.leadTableHeadColumn { + width: 273px; + padding: 16px; + text-align: start; + border: 0.5px solid #b0b0b0; + cursor: pointer; + color: #585858; +} + +.leadTableHeadColumnPlace { + width: 273px; + padding: 16px; + text-align: start; + cursor: pointer; + color: #585858; +} + +.leadTableBody { + color: var(--secondary-black); + background-color: #ffffff; +} + +.leadTableBodyRow { + background-color: #ffffff; + border: none; +} + +.userRow { + border: 2px solid #ffd748; +} + +.rankingCell { + display: flex; + align-items: center; +} + +.userRankingCell { + padding-left: 7px; +} + +.rankingCellNum { + border: 1px solid black; + width: min-content; + padding: 2px; + border-radius: 50%; + min-width: 28px; + display: flex; + align-items: center; + justify-content: center; +} + +.userRankingCellNum { + border: none; + padding: 5px 10px; + background-color: #ffd748; + margin-right: 5px; +} + +.leaderboardText { + display: flex; + flex-direction: column; + position: absolute; + top: 15px; + left: 45%; +} + +.leaderboardText span { + font-size: 24px; + font-weight: 200; +} + +.leaderboardText strong { + color: #585858; + font-size: 22px; +} + +.paginationBlock { + display: flex; + gap: 10px; + align-self: flex-end; +} + +.paginationButton { + padding: 10px; + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + border-radius: 4px; + border: none; + cursor: pointer; +} + +.loading { + height: 536px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/constants.ts b/src/constants.ts index 390675e6f..dc6bc3097 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -23,3 +23,9 @@ export const CODE_LANGS = { typescript: 'TypeScript', yaml: 'YAML', }; + +export const FUTURENET_DETAILS = { + network: "FUTURENET", + networkUrl: "https://horizon-futurenet.stellar.org", + networkPassphrase: "Test SDF Future Network ; October 2022", + }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx new file mode 100644 index 000000000..ffa99107d --- /dev/null +++ b/src/hooks/useAuth.tsx @@ -0,0 +1,76 @@ +import { useContext, useState } from "react"; +import { toast } from "react-toastify"; +import UserChallengesContext, { + UserChallengesContextProps, +} from "../store/user-challenges-context"; + +// Import the Freighter library +import { isConnected, setAllowed, getPublicKey } from "@stellar/freighter-api"; + +const useAuth = () => { + const { address, setAddress } = useContext( + UserChallengesContext + ); + + const [loading, setLoading] = useState(false); + + const disconnect = () => { + setAddress(""); + }; + + const connect = async () => { + try { + setLoading(true); + + // Check if the user has Freighter installed + if (await isConnected()) { + // Prompt the user for authorization if needed + await setAllowed(); + + // Retrieve the user's public key + const publicKey = await getPublicKey(); + + // Store the user's public key in the context + setAddress(publicKey); + + setLoading(false); + + return true; + } else { + // Handle the case where Freighter is not installed + toast("Freighter is not installed!", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + return false; + } + } catch (e) { + console.error("Connection error", e); + + toast("Connection error!", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + return false; + } + }; + + const onConnectFreighter = () => { + // Call the connect function to initiate the Freighter connection + connect(); + }; + + return { + address, + isConnected: !!address, + loading, + connect: onConnectFreighter, + disconnect, + }; +}; + +export default useAuth; diff --git a/src/interfaces/challenge.ts b/src/interfaces/challenge.ts new file mode 100644 index 000000000..53788d89a --- /dev/null +++ b/src/interfaces/challenge.ts @@ -0,0 +1,52 @@ +export interface UserChallengeData { + userId: string; + challenge: ChallengeInfo; +} + +export interface UpdateProgressData { + userId: string; + challengeId: number; + challengeProgress: number; + url?: string; + startDate?: number; + completedAt?: number; + contractId?: string; + totalValueLocked?: number; +} + +export interface Ranking { + current: number; + total: number; +} + +export interface Leaderboard { + userId: string; + ranking: Ranking; + totalValueLocked: number; + minutesSpent: number; + challengesCompleted: number; +} + +export interface Challenge { + id: number; + name: string; + milestonesAmount: number; + isPullRequestRequired: boolean; +} + +export interface ChallengeInfo extends Challenge { + progress?: number; + url?: string; + contractId?: string; + startDate?: number; + completedAt?: number; + isCompleted?: boolean; + totalValueLocked?: number; +} + +export interface UserProgress { + userId: string; + completedChallenges: number; + ranking: Ranking; + challenges: ChallengeInfo[]; +} diff --git a/src/pages/docs/learn/interactive/dapps/challenges/challenge-0-crowdfund.mdx b/src/pages/docs/learn/interactive/dapps/challenges/challenge-0-crowdfund.mdx new file mode 100644 index 000000000..1e7539222 --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/challenges/challenge-0-crowdfund.mdx @@ -0,0 +1,355 @@ +--- +title: Crowdfund Dapp Challenge +description: Build and ship a Crowdfund Dapp! Beat the Challenge! +--- + +import mintToken from "@site/static/img/mintToken.png"; +import approveTokenMint from "@site/static/img/approveTokenMint.png"; +import updatedBalance from "@site/static/img/updatedBalance.png"; +import back100 from "@site/static/img/back100.png"; +import success from "@site/static/img/success.png"; +import backedResult from "@site/static/img/backedResult.png"; +import deployedDApp from "@site/static/img/deployedDApp.png"; +import { ParentChallengeForm } from "@site/src/components/atoms/challenge-form"; +import { ParentChallengeContractForm } from "@site/src/components/atoms/challenge-contract-form"; +import CompleteStepButton from "@site/src/components/atoms/complete-step-button"; +import StartChallengeButton from "@site/src/components/atoms/start-challenge-button"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import "./styles.css"; + + + +This challenge will guide you through the process of building and shipping a crowdfunding dapp on Stellar using Soroban. Unlike traditional crowdfunding applications, crowdfunding dapps (decentralized applications) provide the means for users to pledge funds to a crowdfund campaign directly from their digital wallets, without the need for intermediaries. + +In this challenge, you will learn how to deploy smart contracts to Futurenet, and how to interact with them through a web frontend. In this context, the term "ship" refers to finalizing the development process of your dapp, ensuring that it functions as expected, and is accessible for user interaction and testing through a hosted frontend. However, it's crucial to clarify that despite its functionality, the dapp is not promoted nor intended for deployment in a production-level setting on Futurenet. The challenge is designed for educational purposes, helping you understand how a dapp can be built and interacted with, with further customization and development, it has the potential to evolve into a full-fledged, ready-to-use crowdfunding solution. + +## Checkpoint 0: 📦 Install 📚 + +Start by installing the required dependencies. You'll also want to be sure you have the most updated version of Rust installed. + +Required: + +- `soroban-cli alias` (installed in the next step) +- `Node` v18: [Download Node](https://nodejs.org/en/download/) +- `Freighter Wallet`: [Freighter Wallet](https://freighter.app/) + +First, clone the Soroban Dapps Challenge repo and check out the `crowdfund` branch, which contains the code for the crowdfund smart contract that powers this dapp: + +```sh +git clone https://github.com/stellar/soroban-dapps-challenge.git +cd soroban-dapps-challenge +git checkout crowdfund +``` + +Then, install soroban-cli alias by running the following command: + +```sh +cargo install_soroban +``` + +Soroban CLI is the command line interface to Soroban. It allows you to build, deploy, and interact with smart contracts, configure identities, generate key pairs, manage networks, and more. The soroban-cli (alias) that is used in this challenge is a pinned version of the soroban-cli that is used in the Soroban Dapps Challenge. This ensures that the challenge is reproducible and that all participants are using the same version of Soroban. + +## Checkpoint 1: 🎬 Deploy Smart Contracts + +Now that you have the Crowdfund branch checked out, it's time to deploy the smart contracts to a Sandbox environment. Deploying a smart contract in a production setting involves submitting the contract code to the blockchain's main network ( Mainnet ), where it becomes part of the chain's immutable ledger. Deploying smart contracts to a Sandbox environment simulates that process without actually affecting Mainnet. When you deploy the smart contracts, you'll instead deploy to Futurenet, a test network with more cutting-edge features that have not yet been implemented in the Mainnet. + +In your terminal, load the contracts and initialize them in the Sandbox environment by running the following commands: + +```sh +npm run setup +``` + +If the command runs successfully, your terminal will return a series of messages notifying you about the successful initialization of the contracts and the post-installation sequence. + +```sh +Contract deployed successfully with ID: CBXHU5BWWTOCZRYX3DMSSKCFG7B3K2YG2I5F75ALPQ6GCY6ZES2XKLTI +Deploy the crowdfund contract +Contract deployed successfully with ID: CBKY7UN5VGD4LIQFOBOTSUSQWK67BZZTA23NIEVWSWRR5SAT26JQN2BN +Initialize the abundance token contract + +Initialize the crowdfund contract + +Done + +> soroban-example-dapp@0.1.0 build-contracts +... +``` + +The contract ID is a unique identifier for a smart contract deployed on a blockchain. This contract ID is used to interact with and reference the smart contract, allowing users to invoke functions from the smart contract, send transactions, or otherwise interact with the smart contract's functionalities and data stored on the blockchain. + +:::tip + +Please, save your deployed contract ID. You will need it to complete the challenge. + +::: + + + +## Checkpoint 2: 🤝 Connect the Frontend to the Backend + +Now that you have deployed the smart contract, it's time to check out the frontend of your dapp. The frontend is the browser interface where contributors to your crowdfund campaign will connect their digital wallets and pledge assets to the campaign's cause. + +First, start the development server: + +```sh +npm run dev +``` + +Now open your browser and visit [http://localhost:3000](http://localhost:3000). You should be able to see the frontend of your dapp. + +> Note: Follow the instructions below and ensure that you have funded your wallet address that you intend to use from browser, otherwise the dapp display will be blank an 'Account not found' will be printed on browser's console only. + +Now that you have the frontend running, it's time to connect it with the backend, your smart contract, that defines the rules and logic of the crowdfund campaign, including the function for accepting contributions. If you want to dig into the specifics of the contract, take a look at the video walkthrough of the contract code [here](https://youtu.be/vTz0CQYnMRQ?t=260&feature=shared). + +You will need to add some Futurenet network lumens to your wallet to interact with the dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create and or fund an account on Futurenet. Remember, these are test lumens for use on Futurenet and cannot be used on Mainnet. + +## Checkpoint 3: 🌟 Powering the Campaign + +Fuel the vision! In this step, you will learn how to mint tokens and fund the crowdfunding campaign. Minting tokens in a crowdfund dapp, while not always required, serves as a bootstrapping mechanism for the campaign, allowing the campaign to be funded with the minted tokens. + + + + + + + + + +#### Open Dapp and Mint + +Open the dapp frontend and click on the "Mint 100 ABND" button. + + + + + + + +#### Approve Transaction + +You should see a popup from Freighter asking you to sign the transaction. Click on "Approve" and wait for the transaction to be confirmed. + + + + + + + +#### Check Updated Balance + +You should see an updated balance in the pledge component. + + + + + + + + + + + + + + + +#### Fund the Campaign + +Now that you have your wallet set up, let's fund the crowdfunding campaign. Open the frontend and click on the "Back this project" button. You should see a popup from Freighter asking you to sign the transaction. + + + + + + + +#### Approve Transaction + +Click on "Approve" and wait for the transaction to be confirmed. Once the transaction is confirmed, you should see a success message. + + + + + + + +#### Check Updated Pledged Amount + +You should see an updated balance reflecting the amount you have pledged in the pledge component. + + + + + + + + + + + +> Note: These are test tokens for use with Futurenet and cannot be used on Mainnet. + + + Funding completed + + +## Checkpoint 4: 🚢 Ship it! 🚁 + +Now that your dapp is fully functional, its time to deploy it to a production environment. In this step, you will learn how to deploy your dapp to Vercel, a cloud platform for static sites that offers a quick and effective way to deploy the frontend of your dapp. This section requires that you have a [Vercel account] and the Vercel cli installed. + +If you don't have the Vercel cli installed, run the following command to install it globally: + +```sh +npm i --global vercel +``` + +[Vercel account]: https://vercel.com/login + +Next, you will need to remove the `target` directory to save space for the the deployment. Run the following command to remove the `target` directory: + +```sh +rm -rf target +``` + +> Note: You can build this directory again by running `soroban contract build` in the `contracts/abundance` directory. + +Then, remove any existing `.vercel` directory in your project to ensure that you are starting with a clean slate: + +```bash +rm -rf .vercel +``` + +Then, run the following command to deploy your example dapp: + +```bash +vercel --prod +``` + +Vercel will prompt you to link your local project to a new Vercel project. Follow the answers to the prompts below to ensure that your local project is correctly linked to a new Vercel project: + +```bash +? Set up “~/Documents/GitHub/test/soroban-dapps-challenge”? [Y/n] y +? Which scope should contain your project? +? Link to existing project? [y/N] n +? What’s your project’s name? +? In which directory is your code located? ./ +``` + +Then, continue through the prompts (you will not need to modify any settings) until you reach the completion prompt similar to the following: + +```sh +🔗 Linked to julian-dev28/soroban-example-dapp (created .vercel) +🔍 Inspect: https://vercel.com/julian-dev28/soroban-example-dapp/C1YZVQSqh6WcyR1uvxz8q2tW1tjD [2s] +✅ Production: https://soroban-example-dapp-rho.vercel.app [42s] +``` + +:::tip + +Please, save your production url, you will need it to complete the challenge. + +::: + +You can now visit the preview link to see your deployed dapp! 🎉 + + + +Remember, you must add Futurenet network lumens to your Freighter wallet to interact with the deployed example dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create your Freighter account on Futurenet. + +## Checkpoint 5: 💪 Pass the Challenge! + +Now it's time to submit your work! + +Submit your `Production` URL from the previous step into the challenge form to pass the challenge! + + + +
    + +:::info + +Join [our Community in Discord](https://discord.gg/stellardev) in case you have any issues or questions. + +::: + +## Checkpoint 6: ✅ Check your work! + +In order to successfully complete this challenge, your work needs to be checked. Please, follow the steps below: + +1. Fork [the challenge repository](https://github.com/stellar/soroban-dapps-challenge/fork). +2. Fill the `crowdfund/challenge/output.txt` file with your wallet address. The filled file should look as follows: + +```sh +Public Key: GBSXUXZSA2VEXN5VGOWE5ODAJLC575JCMWRJ4FFRDWSTRCJ123456789 +``` + +3. Create a Pull Request to the `stellar/soroban-dapps-challenge/crowdfund` branch. When the PR is created, CI actions will check the `crowdfund/challenge/output.txt` file data and update your progress. +4. Wait for the CI/CD pipeline results. +5. Fix errors if present: + +- find the error reason in the Crowdfund challenge CI results (you can find a link right in the pull request); +- return to your forked repository; +- fix errors and commit changes. The existing PR will be checked again. + +6. If the pipeline was successful, then congratulations! You completed the challenge!👏 + +Invite a friend to try out your dapp and ask them to provide feedback! + +## ⚔️ Side Quests + +🌐 Explore [Stellar Laboratory] to inspect your account assets on Futurenet. + +You should see something like this: + +```json + "balances": [ + { + "balance": "100.0000000", + "limit": "922337203685.4775807", + "buying_liabilities": "0.0000000", + "selling_liabilities": "0.0000000", + "last_modified_ledger": 148108, + "is_authorized": true, + "is_authorized_to_maintain_liabilities": true, + "asset_type": "credit_alphanum4", + "asset_code": "ABND", + "asset_issuer": "GBUGENT4FK4Y6FZNZEEGVZJLCHKNES23FRVOPPOI62RUF4WRSNOTSZV4" + }, + ] +``` + +[Stellar Laboratory]: https://laboratory.stellar.org/#explorer?network=futurenet + +## 📚 User Workflows Checklist + +During this exercise you should be able to: + +- Clone the example repo (Crowdfund Dapp) +- Choose your target amount and deadline +- Deploy your contract to Futurenet +- Deploy the example web ui somewhere (e.g. netlify, vercel, surge, etc.) + +Then via the web UI, you should be able to: + +- Connect your wallet +- See your current balance(s) +- See the current fundraising status (total amount & time remaining) +- See allowed assets +- Deposit an allowed asset +- See your deposit(s) appear on the page as the transactions are confirmed. +- "Live"-Update the page with the total amount with the new amount + +## 🛡️🗡️ Take On More Challenges + +View your progress and take on more challenges by visiting your [User Dashboard!](../dashboard) diff --git a/src/pages/docs/learn/interactive/dapps/challenges/challenge-1-payment.mdx b/src/pages/docs/learn/interactive/dapps/challenges/challenge-1-payment.mdx new file mode 100644 index 000000000..efe7faaca --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/challenges/challenge-1-payment.mdx @@ -0,0 +1,457 @@ +--- +title: Payment Dapp Challenge +description: Take on the challenge and master the Soroban Payment Dapp! +--- + +import connect_freighter from "@site/static/img/connect_freighter.png"; +import freighter_settings from "@site/static/img/freighter_settings.png"; +import add_token from "@site/static/img/add_token.png"; +import new_token from "@site/static/img/new_token.png"; +import manage_assets from "@site/static/img/manage_assets.png"; +import added_balance from "@site/static/img/added_balance.png"; +import aucb_balance from "@site/static/img/aucb_balance.png"; +import pmt_dest from "@site/static/img/pmt_dest.png"; +import next from "@site/static/img/next.png"; +import choose_token from "@site/static/img/choose_token.png"; +import select_token from "@site/static/img/select_token.png"; +import payment_settings from "@site/static/img/payment_settings.png"; +import confirm_pmt from "@site/static/img/confirm_pmt.png"; +import submit_pmt from "@site/static/img/submit_pmt.png"; +import end from "@site/static/img/end.png"; +import balance_rcvr from "@site/static/img/balance_rcvr.png"; +import { ParentChallengeForm } from "@site/src/components/atoms/challenge-form"; +import { ParentChallengeContractForm } from "@site/src/components/atoms/challenge-contract-form"; +import CompleteStepButton from "@site/src/components/atoms/complete-step-button"; +import StartChallengeButton from "@site/src/components/atoms/start-challenge-button"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import "./styles.css"; + + + +This challenge will guide you through the process of setting up, customizing, and deploying a Soroban Payment dapp, a blockchain-powered payment application designed to work with the Freighter wallet. Payment dapps are powerful because they offer users equitable and accessible means to send and receive payments. Transactions via a payment dapp are peer-to-peer, which means that no central authority, third-party, or bank oversees or controls the payment. This decentralization reduces payment fees, which are comparatively minimal on a blockchain, and transaction time, which is, via a payment dapp, almost instantaneous. What's more, the wallet integration in a payment dapp, like Freighter in this case, means that anyone with a smartphone and the wallet installed can use the payment dapp, no matter where they are in the world. + +## Checkpoint 0: 📦 Install Dependencies + +Before you begin, ensure you have the following installed on your system. You'll also want to be sure you have the most updated versions of Rust and Soroban installed. + +- `soroban-cli`: [Install soroban-cli](/docs/smart-contracts/getting-started/setup#install-the-soroban-cli) +- `Node` (>=16.14.0 < 17.0.0): [Download Node](https://nodejs.org/en/download/) +- `Yarn` (v1.22.5 or newer): [Install Yarn](https://yarnpkg.com/getting-started/install) +- `Freighter Wallet`: [Freighter Wallet](https://freighter.app/) + +Node and Yarn are package managers that let you install and manage dependencies during the dapp development process. Freighter is the wallet you will integrate into your payment dapp. + +## Checkpoint 1: 🚀 Clone the Repository + +Clone and set up the Soroban Dapps Challenge repository, which contains the Soroban Payment Dapp files. Then run yarn to install the dependencies. + +```bash +git clone https://github.com/stellar/soroban-dapps-challenge.git +cd soroban-dapps-challenge +git checkout payment +yarn +``` + +## Checkpoint 2: 🎬 Deploy Smart Contracts + +For this step you will need to clone and deploy the Soroban token smart contract from the [Soroban Examples repository](https://github.com/stellar/soroban-examples/tree/v20.0.0/token). This Soroban token smart contract, broken into several smaller modules (as is the custom for complex smart contracts like this one), enables you to create and manage tokens on Soroban. + +The Soroban token is a custom token that will be used to facilitate payments in the Payment Dapp. Tokens are essentially programmable assets on a blockchain, and smart contracts provide the automation and rules for these tokens. They allow for predefined conditions and actions related to the tokens, such as issuance, transfer, and more complex functions, ensuring the execution of these operations without the need for intermediaries. In the case of this Payment Dapp, you will use the Soroban token to initialize and mint "Demo Token" assets, or DT, that you can then use to make payments via the Payment Dapp. + +:::info Soroban Tokens are not the same as [Stellar Asset Contracts](https://soroban.stellar.org/docs/advanced-tutorials/stellar-asset-contract) which allow users to use their Stellar native asset balances in Soroban. If you are curious about the mechanics of Soroban Tokens and Stellar Asset Contracts, you can read more about them in the [Soroban Token Playground](https://token-playground.gitbook.io/guide/). ::: + +In a new terminal window, follow the steps below to build and deploy the token smart contract: + +```bash +git clone https://github.com/stellar/soroban-examples.git +cd soroban-examples/token +make +``` + +This will build the smart contracts and put them in the `token/target/wasm32-unknown-unknown/release` directory. + +Next, you will need to deploy the token smart contract to Futurenet. In order to deploy to future, you will need a Stellar account keypair (a public key and its corresponding secret key). Keep in mind that Freighter, where you can create and view your account's public key, intentionally does not allow you or any application to access your secret key. It's recommended therefore to generate a new Futurenet keypair using [Stellar Laboratory](https://laboratory.stellar.org/#account-creator?network=futurenet), fund the account, and then import the keypair's public key into your Freighter wallet. + +Once you have done this and are ready to deploy the token smart contract to Futurenet, open a terminal in the `soroban-examples/token` directory and follow the steps below: + +```bash +soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_token_contract.wasm \ + --source \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' +``` + +Deploying the token contract to Futurenet will return a contract ID that you will need to use in the next step to invoke the token smart contract and initialize the Soroban token as "Demo Token": + +```bash +soroban contract invoke \ + --id \ + --source-account \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' \ + -- initialize \ + --admin \ + --decimal 7 \ + --name "Demo Token" \ + --symbol "DT" +``` + +Lets take a look at what is happening here: + +- admin: This is the public key of the administrator account and corresponds to the secret key you used to deploy the contract in the previous step. It is the "master" account that has control over the token contract. + +- decimal: This decimal precision value is set to 7. This value indicates that your token will have 7 decimal places, providing fine-grained control and flexibility in transactions. + +- name: This value is set to "Demo Token," the name of your token written as a string. + +- symbol: Your token symbol is a short string that represents your token, in this case, "DT." + +Next, you will need to mint some tokens to your sender's account (the administrator account you used to deploy the contract and initialize the token above). To do this, run the following command: + +```bash +soroban contract invoke \ + --id \ + --source-account \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' \ + -- mint \ + --to \ + --amount 1000000000 +``` + +This will mint 100 DT tokens to the `to` address. You can check any address' balance by running the following command: + +```bash +soroban contract invoke \ + --id \ + --source-account \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' \ + -- balance \ + --id +``` + + + +## Checkpoint 3: 🖥️ Launch the Frontend + +In this checkpoint, you will make sure that the frontend of the Payment Dapp successfully communicates with the backend, allowing transactions to be created, signed, and submitted to the network. + +Open a terminal in the `soroban-dapps-challenge` directory and run the following command to launch the frontend of your dapp: + +```bash +yarn start +``` + +You should see the following output: + +```bash +$ webpack-dev-server --config config/webpack.dev.js + [webpack-dev-server] Project is running at: + [webpack-dev-server] Loopback: http://localhost:9000/ +... +``` + +Now open your browser and navigate to [`http://localhost:9000`](http://localhost:9000). You should see the Payment Dapp running in your browser. + + + connect + + +## Checkpoint 4: 🚀 Token Transfer Odyssey + +Strap in and get ready to send some tokens! In this step, you will use the Payment Dapp to send Soroban tokens to another account. + + + + + + + + +#### Add Soroban Token + +To add the newly minted DT token type to your wallet, open your Freighter wallet and click on the `Manage Assets` button at the bottom of the screen. + +manage assets + +Then click on the `Add Soroban token ` button and enter the token contract ID that was returned when you deployed the token smart contract. + +add token +
    +new token +
    + + + +#### Check Token Addition + +You should now see the Soroban token in your Freighter wallet. + +added balance + + +
    +
    + + + + + + +#### Connect Freighter and Select Account + +Back on your dapp's frontend webpage, make sure Freighter is connected and then select the account that will be used to send Soroban tokens. Click "next" to continue. + +next + + + + +#### Provide Token Transfer Details + +To send DT tokens via the Payment dapp, provide the public key of the account that will receive the Soroban tokens. (This could be another of your own Freighter accounts.) + +payment destination + +Input the token ID of the Soroban token. + +choose token + +Input the amount of Soroban tokens to send. + +select token + +Confirm the payment settings, which include the option to add a memo and show the transaction fee. + +payment settings + + + + +#### Confirm and Submit Transaction + +Review the transaction details to ensure accuracy and then click "Sign with Freighter". Freighter will prompt you to sign the transaction with your wallet's private key. + +confirm payment + +Once signed, click "Submit Payment." The transaction will be submitted to the network. + +submit payment + +The Payment Dapp will show a confirmation message once the transaction has been successfully submitted. This includes the XDR response, which can be decoded using [stellar laboratory](https://laboratory.stellar.org/#xdr-viewer?type=TransactionResult&network=futurenet). + +end + +You can now check the balance of the receiving account to ensure that the transaction was successful. + +balance receiver + +As stated before, you can also check the balance of an account with the soroban-cli by running the following command: + +```bash + soroban contract invoke \ + --id \ + --source-account \ + --rpc-url https://rpc-futurenet.stellar.org:443 \ + --network-passphrase 'Test SDF Future Network ; October 2022' \ + -- balance \ + --id +``` + +Output: + +```bash +"1000000000" +``` + + + + + +
    + + + Tokens Sent + + +## Checkpoint 5: 🚢 Ship it! 🚁 + +In this step, you will deploy your dapp to a hosting platform so that it can be accessed by anyone with an internet connection. You can use any hosting platform you like, but for demonstration purposes, this section will use [Vercel](https://vercel.com/). Vercel is a cloud platform for static sites and serverless functions that offers a free tier for developers. It also has a built-in integration with GitHub, which makes it easy to deploy your dapp directly from your GitHub repository. + +If you dont already have a [Vercel account], you will need to create one and link it to your GitHub account. + +[Vercel account]: https://vercel.com/login + +First install the Vercel cli: + +```bash +npm i --global vercel +``` + +Then, remove any existing `.vercel` directory in your project to ensure that you are starting with a clean slate: + +```bash +rm -rf .vercel +``` + +Next, you will need to create a new project on vercel. To do this, run the following command: + +```bash +vercel project add +``` + +For example: + +```bash +vercel project add soroban-react-payment +``` + +Next you will pull in the project settings locally by running the following command: + +```bash +vercel pull +``` + +Follow the answers to the prompts below to ensure that your local project is correctly linked to the target Vercel project: + +```bash +? Set up “~/Documents/GitHub/test/soroban-dapps-challenge”? [Y/n] y +? Which scope should contain your project? +? Link to existing project? [y/N] y +? What’s the name of your existing project? +``` + +After following the prompts, you should see something similar to the following output: + +```bash +... +🔗 Linked to julian-dev28/pmt-dapp (created .vercel) +> Downloading `development` Environment Variables for Project pmt-dapp +✅ Created .vercel/.env.development.local file [92ms] + +> Downloading project settings +✅ Downloaded project settings to ~/Documents/GitHub/test/soroban-dapps-challenge/.vercel/project.json [1ms] +``` + +Next, you will need to edit the `settings` section in `.vercel/project.json` to ensure that the `outputDirectory` is set to `build`: + +```diff + "settings": { + "createdAt": 1699390700432, + "framework": null, + "devCommand": null, + "installCommand": null, + "buildCommand": null, +- "outputDirectory": null, ++ "outputDirectory": "build", + "rootDirectory": null, + "directoryListing": false, + "nodeVersion": "18.x" + } +``` + +Next, run the following command to build your dapp: + +```bash +vercel build --prod +``` + +What does the `vercel build` command do? It builds your dapp for production, which means that it optimizes your code for performance and creates an optimized production build of your dapp in the `.vercel/output` directory. This is the directory that you will deploy to Vercel. + +The output of the `vercel build` command should look something like this: + +```bash +.. +$ webpack --config config/webpack.prod.js +asset 8a7edf3024865247d470.js 1.73 MiB [emitted] [immutable] [minimized] (name: index) 1 related asset +... +webpack compiled in 12408 ms (be8ba6cc95f4aec4d07b) +✨ Done in 13.16s. +✅ Build Completed in .vercel/output [14s] +``` + +Next, you will deploy your dapp to Vercel by running the following command: + +```bash +vercel deploy --prebuilt --prod +``` + +Using the `--prebuilt` flag tells Vercel to deploy the the build outputs in `.vercel/output` that you created in the previous step. + +Once the deployment is complete, you should see something similar to the following output: + +```bash +🔍 Inspect: https://vercel.com/julian-dev28/soroban-react-payment/9PwV2DvuXJ3FWag7eLbjqNAhCeCu [2s] +✅ Production: https://soroban-react-payment-ahtko9qd1-julian-dev28.vercel.app [2s] +``` + +:::tip + +Please, save your production url, you will need it to complete the challenge. + +::: + +You can now visit the preview link to see your deployed dapp! 🎉 + +Remember, you must add Futurenet network lumens to your Freighter wallet to interact with the deployed example dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create your Freighter account on Futurenet. + +## Checkpoint 6: ✅ Complete the Challenge! + +Now it's time to submit your work! + +Submit your `Production` URL from the previous step into the challenge form to pass the challenge! + + + +
    + +:::info + +Join [our Community in Discord](https://discord.gg/stellardev) in case you have any issues or questions. + +::: + +## Checkpoint 7: 💪 Share Your Accomplishment with the Community + +Don't forget to share your work with the community. Let others see what you've accomplished, receive feedback, and inspire others! + +## ⚔️ Side Quests + +🍴[Fork the Example Soroban Payment Dapp repo] and make your own changes to your Dapp. + +Consider customizing the code and submitting a pull request for the challenge. You can explore advanced features of the Example Soroban Payment Dapp, and Freighter wallet to take your skills to the next level. Show your creativity by adding unique functionalities, enhancing the user interface, or integrating with other APIs or services. Good luck! + +[Fork the Example Soroban Payment Dapp repo]: https://github.com/stellar/soroban-react-payment + +## 📚 User Workflows Checklist + +To ensure that you've covered all the key user actions during the challenge, follow this checklist: + +- Clone the repository +- Install dependencies +- Deploy and initialize the token smart contract +- Mint tokens to your account +- Launch the local frontend +- Add the Soroban token to Freighter +- Connect Freighter to the application +- Send tokens to another account +- Deploy the site with Vercel +- Submit your public key and URL + +## 🛡️🗡️ Take On More Challenges + +View your progress and take on more challenges by visiting your [User Dashboard!](../dashboard) diff --git a/src/pages/docs/learn/interactive/dapps/challenges/challenge-2-liquidity-pool.mdx b/src/pages/docs/learn/interactive/dapps/challenges/challenge-2-liquidity-pool.mdx new file mode 100644 index 000000000..15fbd6c69 --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/challenges/challenge-2-liquidity-pool.mdx @@ -0,0 +1,459 @@ +--- +title: Liquidity Pool Dapp Challenge +description: Deploy an example dapp, add liquidity! Beat the Challenge! +--- + +import mint_tokens from "@site/static/img/mint_tokens.png"; +import approveTokenMint from "@site/static/img/approveTokenMint.png"; +import updatedBalances from "@site/static/img/updatedBalances.png"; +import deposit50 from "@site/static/img/deposit50.png"; +import updatedBalances50 from "@site/static/img/updatedBalances50.png"; +import swap from "@site/static/img/swap.png"; +import swapComplete from "@site/static/img/swapComplete.png"; +import futurenetDeployment from "@site/static/img/futurenetDeployment.png"; +import wdraw from "@site/static/img/wdraw.png"; +import { ParentChallengeForm } from "@site/src/components/atoms/challenge-form"; +import { ParentChallengeContractForm } from "@site/src/components/atoms/challenge-contract-form"; +import CompleteStepButton from "@site/src/components/atoms/complete-step-button"; +import StartChallengeButton from "@site/src/components/atoms/start-challenge-button"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import "./styles.css"; + + + +A liquidity pool is a collection of tokens or digital assets deposited by users and held in a smart contract or dapp that can be used to provide essential liquidity to decentralized exchanges (DEXs) and other decentralized finance (DeFi) protocols. Since liquidity plays a crucial role in enabling DeFi systems, liquidity pools, where assets can be held, lent, borrowed, swapped, or traded (depending on dapp functionality), are fundamental to these systems. + +The functionality of this liquidity pool dapp will allow users to mint tokens, deposit liquidity, swap between asset types, and withdraw funds from the liquidity pool. This dapp challenge will walk you through the step-by-step process of creating and launching a liquidity pool dapp on Stellar using Soroban smart contracts. You will learn how to deploy smart contracts to a sandbox environment and interact with them through a web frontend. In this context, the term "ship" refers to finalizing the development process of your dapp, ensuring that it functions as expected and is accessible for user interaction and testing through a hosted frontend. Despite the end-to-end functionality of this challenge, this dapp is not promoted nor intended for deployment in a production-level setting on Futurenet, but rather is designed for educational purposes. + +## Checkpoint 0: 📦 Install 📚 + +Start by installing the required dependencies. You'll also want to be sure you have the most updated version of Rust installed. + +Required: + +- `soroban-cli alias` (installed in the next step) +- `Node` v18: [Download Node](https://nodejs.org/en/download/) +- `Freighter Wallet`: [Freighter Wallet](https://freighter.app/) + +First, clone the Soroban example dapp repo and navigate to the `liquidity-pool` directory: + +```bash +git clone https://github.com/stellar/soroban-dapps-challenge +cd soroban-dapps-challenge +git checkout liquidity-pool +``` + +Then, install soroban-cli alias by running the following command: + +```sh +cargo install_soroban +``` + +Soroban CLI is the command line interface to Soroban. It allows you to build, deploy, and interact with smart contracts, configure identities, generate key pairs, manage networks, and more. The soroban-cli alias that is used in this challenge is a pinned version of the soroban-cli that is used in the Soroban Dapps Challenge. Using the soroban-cli alias ensures that the challenge is reproducible and that all participants are using the same version of Soroban. + +## Checkpoint 1: 🎬 Deploy Smart Contracts + +Deploying a smart contract in a production setting involves submitting the contract code to the blockchain's main network (Mainnet), where it becomes part of the chain's immutable ledger. When you deploy the smart contracts in this challenge, you'll instead deploy to Futurenet, a test network with more cutting-edge features that have not yet been implemented in the Mainnet. Deploying smart contracts to a sandbox environment simulates the production-level deployment process without actually affecting Mainnet. + +Now that you have the Liquidity Pool branch checked out, load the contracts and initialize them in the sandbox environment by running the following commands in your terminal: + +```bash +./initialize.sh futurenet +``` + +If the command runs successfully, your terminal will return a series of messages notifying you about the successful initialization of the contracts and the post-installation sequence. + +```bash +Contract deployed successfully with ID: CBXHU5BWWTOCZRYX3DMSSKCFG7B3K2YG2I5F75ALPQ6GCY6ZES2XKLTI +Deploy the liquidity pool contract +Contract deployed successfully with ID: CBKY7UN5VGD4LIQFOBOTSUSQWK67BZZTA23NIEVWSWRR5SAT26JQN2BN +Initialize the abundance token contract + +Initialize the liquidity pool contract + +Done + +> soroban-example-dapp@0.1.0 build-contracts +... +``` + +The contract ID is a unique identifier for a smart contract deployed on a blockchain. This contract ID is used to interact with and reference the smart contract, allowing users to invoke functions from the smart contract, send transactions, or otherwise interact with the smart contract's functionalities and data stored on the blockchain. + +:::tip + +Please, save your deployed contract ID. You will need it to complete the challenge. + +::: + + + +## Checkpoint 2: 🤝 Connect the Frontend to the Backend + +Now that you have deployed the smart contract, it's time to check out the frontend of your dapp. The frontend is the browser interface where users will connect their digital wallets to make deposits into and withdrawals from the liquidity pool. The frontend is also where users will be able to see their balances and swap tokens. + +Because interacting with dapps requires both backend and frontend development, the Soroban Dapps Challenge includes the functionality to easily deploy a frontend interface of the dapps. Building out the frontend from scratch would typically involve creating a user interface (UI) and user experience (UX) design, as well as writing the code for the frontend. In this challenge, you will use the frontend that is already built for you. + +To set up the development server, navigate to the `frontend` folder of the soroban-dapps-challenge repository and run the following command: + +```bash +make setup && make start_dev +``` + +> Note: This may require admin privileges on some systems. + +Now open your browser and visit [http://localhost:5173](http://localhost:5173/). You should be able to see the frontend of your dapp. + +> Note: Follow the instructions below and ensure that you have funded your wallet address that you intend to use from browser. + +Now that you have the frontend running, it's time to connect it with the backend, your smart contract, that defines the rules and logic of the liquidity pool, including the token swap and liquidity pool functions. + +You will need to add some Futurenet network lumens to your wallet to interact with the dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create and or fund an account on Futurenet. Remember, these are test lumens for use on Futurenet and cannot be used on Mainnet. + +## Checkpoint 3: 🌊 Dive into the Liquidity Pool + +Embark on a tidal journey! In this step you will mint, deposit, swap, and withdraw tokens from the liquidity pool. Minting tokens, depositing liquidity, swapping between asset types, and withdrawing funds from the liquidity pool constitute the basic lifecycle of interacting with a DeFi protocol. + +In the context of liquidity pools, depositing and withdrawing assets involve connecting a digital wallet and submitting deposit/withdraw transactions. In this liquidity pool dapp challenge specifically, you will also need to mint the test tokens that you will then use to make deposits to the liquidity pool. (This minting of test tokens is different from liquidity pools where depositors may receive minted tokens in exchange for their deposited funds.) Perhaps one of the most important actions a user can take through liquidity pool dapps is swapping tokens. In fact, the ability to swap from one asset to another through a liqudity pool is a powerful feature of DeFi protocols: instead of exchanging assets through traditional financial institutions and intermediaries, users can have direct access to decentralized asset exchange via liquidity pool dapps, without needing a bank account or other traditional financial instruments. + + + + + + + + + +#### Mint USDC and BTC + +In order to use this liquidity pool dapp, you will need to mint test tokens which can then be used to make deposits and swaps via the frontend of the dapp. To mint USDC and BTC test tokens, open the dapp frontend and click the "MINT" button for USDC and BTC. + + + + + + +#### Approve Transaction + +You should see a popup from Freighter asking you to sign the transactions. Click on "Approve" and wait for the transaction to be confirmed. + + + + + + +#### Check Updated Balance + +You should see an updated balance in the account balance component. + + + + + + + + + + + + + + +#### Deposit into the Liquidity Pool + +Depositing assets into the liquidity pool involves users submitting deposit transactions via the frontend to deposit tokens from their wallet into the liquidity pool. In this dapp you will make a deposit of two asset types in order to swap between those asset types. In other DeFi protocols, users may also deposit liquidity into a liquidity pool in order to earn yields on their deposits. The intial deposit of liquidity into a liquidity pool is what sets the initial price of the tokens in the pool. For example, if a user deposits 37000 USDC and 1 BTC, the price of each BTC token will be 37000 USDC. + +Open the frontend, enter the desired token amounts, and click the "Deposit" button. You should see a popup from Freighter asking you to sign the transaction. + + + + + + +#### Approve Transaction + +Click on "Approve" and wait for the transaction to be confirmed. Once the transaction is confirmed, you should see your balances updated. + + + + + +#### Check Updated Balance + +You should see an updated balance in the amounts you have deposited in the account and reserve balance components, respectively. Following the example, you should see 50 USDC, 50 BTC, and 50 POOL. + + + + + + + + + + + + + + + +#### Swap Tokens + +Now that you have funded the liquidity pool, you can make a swap to easily exchange one token for another. Swaps in a liquidity pool usually depend on the relationship between two or more different tokens that can be exchanged with each other. Typical liquidity pools rely on a mathematical formula that determines the price of the tokens within the pool. With every deposit and withdraw transaction to or from the pool, the formula adjusts the token price of each token based on this formula. When a swap occurs, the liquidity pool uses the formula and the balances of each token with the pool to determine the swap value of each token relative to the others within the pool. + +This liquidity pool dapp challenge uses a [specific formula] in its smart contracts to enable swapping between tokens while ensuring that the liquidity pool remains balanced and that liquidity providers are compensated for their contributions to the pool (.3% of swap amount are sent to liquidity providers). Since this liquidity pool dapp currently only holds the two tokens you deposited in the previous step, the formula will reflect those token balances: the price of each token will be be relative to the amount of the other token in the pool. For example, if you deposited 50 USDC and 50 BTC, the price of each token will be 1:1. If you deposited 100 USDC and 50 BTC, the price of each token will be 2:1. + +[specific formula]: https://github.com/stellar/soroban-dapps-challenge/blob/f7cde6fc6cfce5470ebab9b7489c367c3306317f/contracts/liquidity-pool/src/lib.rs#L251 + +Also important to note is how slippage works in this dapp. Slippage refers to the maximum variation percentage accepted for the desired deposit amounts. The higher the percentage, the greater the chance of a successful transaction, but you may not get such a good price. Here, users can set the max slippage to their desired amount. + +To complete a swap between USDC and BTC test tokens, open the swap tab of the frontend, input the desired token swap amounts, and click the "Swap" button. You should see a popup from Freighter asking you to sign the transaction. + + + + + + + +#### Approve Transaction + +Click on "Approve" and wait for the transaction to be confirmed. + + + + + +#### Check Updated Balance + +Once the transaction is confirmed, you should see updated balances on the frontend. + + + + + + + + + + + + + + + +#### Withdraw Tokens from the Liquidity Pool + +Now that you have swapped tokens through the liquidity pool, you can make a withdrawal of your funds. + +The "Pool Share" slider in this dapp refers to the slider for the percentage of liquidity you want to withdraw from the pool. For example, if you have 100 USDC and 100 BTC in the pool, and you want to withdraw 50% of your liquidity, you would select 50% on the slider. The withdrawal amounts are relative to the estimated price of the tokens in the pool base on the conversion ratio set in the Deposit that created the liquidity pool. For example if a user deposits 50 USDC and 1 BTC, the price of USDC will be 50:1 BTC. If the user then withdraws 50% of their liquidity, they will receive 25 USDC and 0.5 BTC. The price of BTC in the pool will then be recalculated based on the token supply in the pool, potentially causing slippage and resulting in a price different from the original 50:1 ratio. + +Open the withdraw tab, select how much liquidity you want to remove with the sliding bar, and click the "Withdraw" button. You should see a popup from Freighter asking you to sign the transaction. + + + + + + + +#### Approve Transaction + +Click on "Approve" and wait for the transaction to be confirmed. + + + + + +#### Check Updated Balance + +Once the transaction is confirmed, you should see updated balances on the frontend. + + + + + + + + + +> Note: These are test tokens for use on Futurenet or Mainnet. + + + Transactions completed + + +## Checkpoint 4: 🚢 Ship It! 🚁 + +Now that your dapp is fully functional, its time to deploy it to a production environment. In this step, you will learn how to deploy your dapp to Vercel, a cloud platform for static sites that offers a quick and effective way to deploy the frontend of your dapp. This section requires that you have a [Vercel account] and install the Vercel CLI. + +[Vercel account]: https://vercel.com/login + +First, you will remove the target directory, as it is not used by Vercel to deploy your site. To do this, navigate to the `liquidity-pool` directory and run the following: + +``` +rm -rf target +``` + +> Note: You can build this directory again by running `soroban contract build` in the `contracts/abundance` directory. + +Next, you must move your `.soroban` directory to the frontend directory. + +From a terminal in the `liquidity-pool` directory, run the following command: + +```bash +mv .soroban frontend/.soroban +``` + +Then, you need to update the `package.json` file in the `frontend` directory to point to the new contract binding locations. + +```diff +-"liquidity-pool-contract": "file:../.soroban/contracts/liquidity-pool", +-"share-token-contract": "file:../.soroban/contracts/share-token", +-"token-a-contract": "file:../.soroban/contracts/token-a", +-"token-b-contract": "file:../.soroban/contracts/token-b", + ++"liquidity-pool-contract": "file:.soroban/contracts/liquidity-pool", ++"share-token-contract": "file:.soroban/contracts/share-token", ++"token-a-contract": "file:.soroban/contracts/token-a", ++"token-b-contract": "file:.soroban/contracts/token-b", +``` + +Next, you will use the Vercel CLI to complete your deployment. + +First, install the Vercel CLI: + +```bash +npm i --global vercel +``` + +Then, remove any existing `.vercel` directory in your project to ensure that you are starting with a clean slate: + +```bash +rm -rf .vercel +``` + +Then, run the following command to deploy your example dapp: + +```bash +vercel --prod +``` + +Vercel will prompt you to link your local project to a new Vercel project. Follow the answers to the prompts below to ensure that your local project is correctly linked to a new Vercel project: + +```bash +? Set up “~/Documents/GitHub/test/soroban-dapps-challenge”? [Y/n] y +? Which scope should contain your project? +? Link to existing project? [y/N] n +? What’s your project’s name? +? In which directory is your code located? ./ +``` + +Then, continue through the prompts until you see the following message regarding setting overrides: + +```bash +? Want to override the settings? [y/N] y +? Which settings would you like to override? (Press to select, to toggle all, to invert selection) +❯◯ Build Command + ◯ Development Command + ◯ Output Directory +``` + +Select each entry (type "a") and set the following values: + +**build command** + +```bash +cd frontend && make build +``` + +**development command** + +```bash +cd frontend && make start_dev +``` + +**output directory** + +```bash +frontend/dist +``` + +Once the deployment is complete, you should see a completion message similar to the following: + +```bash +🔗 Linked to julian-dev28/liquidity-pool (created .vercel) +🔍 Inspect: https://vercel.com/julian-dev28/liquidity-pool/FfsAJdgUR9LKH5EmGiuMCMYUMTi2 [2s] +✅ Production: https://liquidity-pool.vercel.app [54s] +``` + +:::tip + +Please, save your production url, you will need it to complete the challenge. + +::: + +You can now visit the preview link to see your deployed dapp! 🎉 + + + +Remember, you must add Futurenet network lumens to your Freighter wallet to interact with the deployed example dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create your Freighter account on Futurenet. + +## Checkpoint 5: ✅ Complete the Challenge! + +Now it's time to submit your work! + +Submit your `Production` URL from the previous step into the challenge form to pass the challenge! + + + +## Checkpoint 6: 💪 Flex! + +🍴 [Fork the Soroban Dapps Challenge repo] and make your own changes to the Liquidity Pool branch. + +Customize the code and submit a pull request for the Liquidity Pool Dapp Challenge. You can experiment with new fee strategies, improve the user interface, or integrate additional token pair options. + +Take this opportunity to showcase your skills and make your mark on the Liquidity Pool Dapp. Good luck! + +[Stellar Laboratory]: https://laboratory.stellar.org/#explorer?network=futurenet +[Fork the Soroban Dapps Challenge repo]: https://github.com/stellar/soroban-dapps-challenge/fork + +## 📚 User Workflows Checklist + +During this exercise, you should be able to: + +- Clone the example repo (Liquidity Pool Dapp) +- Deploy your contract to a sandbox environment. +- Deploy the example web UI somewhere (e.g., Netlify, Vercel, Surge, etc.) + +Then, via the web UI, you should be able to: + +- Connect your wallet +- See your current balance(s) +- Mint assets +- Deposit assets +- Swap assets +- Withdraw assets +- See your transaction(s) appear on the page as the transactions are confirmed + +## 🛡️🗡️ Take On More Challenges + +View your progress and take on more challenges by visiting your [User Dashboard!](../dashboard) diff --git a/src/pages/docs/learn/interactive/dapps/challenges/challenge-3-oracle.mdx b/src/pages/docs/learn/interactive/dapps/challenges/challenge-3-oracle.mdx new file mode 100644 index 000000000..1fc6bb04f --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/challenges/challenge-3-oracle.mdx @@ -0,0 +1,538 @@ +--- +title: Oracle Dapp Challenge +description: Get prices with the Oracle Dapp! Beat the Challenge! +--- + +import oracle_connect from "@site/static/img/oracle_connect.png"; +import oracle_home from "@site/static/img/oracle_home.png"; +import oracle_mintBal from "@site/static/img/oracle_mintBal.png"; +import oracle_approve from "@site/static/img/oracle_approve.png"; +import oracle_mint from "@site/static/img/oracle_mint.png"; +import oracle_calculate from "@site/static/img/oracle_calculate.png"; +import oracle_balance_contract from "@site/static/img/oracle_balance_contract.png"; +import oracle_balance_user from "@site/static/img/oracle_balance_user.png"; +import oracle_deployed from "@site/static/img/oracle_deployed.png"; +import { ParentChallengeForm } from "@site/src/components/atoms/challenge-form"; +import { ParentChallengeContractForm } from "@site/src/components/atoms/challenge-contract-form"; +import CompleteStepButton from "@site/src/components/atoms/complete-step-button"; +import StartChallengeButton from "@site/src/components/atoms/start-challenge-button"; +import "./styles.css"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + + + +This challenge will guide you through the process of building and shipping an oracle dapp on the Stellar network using Soroban. Oracle dapps (decentralized applications) provide the means for users to access real-world data and information from external sources and bring it on-chain. + +In this challenge, you will learn how to deploy smart contracts to Futurenet, and how to interact with them through a web frontend. In this context, the term "ship" refers to finalizing the development process of your dapp, ensuring that it functions as expected, and is accessible for user interaction and testing through a hosted frontend. However, it's crucial to clarify that despite its functionality, the dapp is not promoted nor intended for deployment in a production-level setting on Mainnet. The challenge is designed for educational purposes only, helping you understand how a dapp can be built and interacted with, with further customization and development, it has the potential to evolve into a full-fledged, ready-to-use oracle solution. + +## Checkpoint 0: 📦 Install 📚 + +Start by installing the required dependencies. + +Required: + +- `soroban-cli` 20.0.0-rc.4.1: [Install Soroban CLI](/docs/smart-contracts/getting-started/setup#install-the-soroban-cli) +- `Node` >=v18: [Download Node](https://nodejs.org/en/download/) +- `Freighter Wallet`: [Freighter Wallet](https://freighter.app/) + +First, clone the Soroban Dapps Challenge repo and check out the `oracle` branch, which contains the code for the oracle smart contract that powers this dapp: + +```sh +git clone https://github.com/stellar/soroban-dapps-challenge.git +cd soroban-dapps-challenge +git checkout oracle +``` + +:::tip If you haven't already installed the soroban-cli , you can do so by running the following command: + +```sh +cargo install --locked --version 20.0.0-rc.4.1 soroban-cli +``` + +::: + +Soroban CLI is the command line interface to Soroban. It allows you to build, deploy, and interact with smart contracts; configure identities; generate key pairs; manage networks; and more. + +## Checkpoint 1: 📝 Setup the `initialize.sh` Script + +The `initialize.sh` script is a shell script that will help you initialize the contracts and install dependencies. It is located in the top level directory. + +The `initialize.sh` script performs following actions: + +- Creates a new wallet to serve as the token admin +- Funds token admin wallet with test tokens +- Builds and deploys contracts using this wallet +- Creates typescript bindings +- Installs dependencies + +For this section, you'll need to configure parameters to initialize `donation` and `oracle` contracts. + +You'll start with `donation` contract: + +- First, Open `initialize.sh`: + +- Find the following part at the end of the file: + +```sh +echo "Initialize the DONATION contract" +``` + +- For the `--recipient` parameter, specify the wallet address that will be used to withdraw money from the donation contract. This may be your address. + +```sh +initialize\ +--recipient \ +``` + +Now for the `oracle` contract: + +- Find the following part of `initialize.sh`: + +```sh +echo "Initialize the ORACLE contract" +``` + +- As you can see, there are several parameters that need to be specified here. + +```sh + initialize \ + --caller \ + --pair_name BTC_USDT \ + --epoch_interval \ + --relayer +``` + +Here is a description of each parameter: + +- `` - the address that will become the owner of the contract. This may be your address. Only the contract owner will be able to change the `epoch_interval` and `relayer` +- `` - frequency (in seconds) of price updates +- `` - address of the wallet that will update the price (in the backend, in the CRON task). It is suggested to create a dedicated wallet for this ([be sure to fund with test Lumens](https://laboratory.stellar.org/#account-creator?network=futurenet)) + +## Checkpoint 2: 🎬 Deploy Smart Contracts + +Now that the initialization script is set up it's time to deploy the smart contracts to a Sandbox environment. Deploying a smart contract in a production setting involves submitting the contract code to the blockchain's main network ( Mainnet ), where it becomes part of the chain's immutable ledger. Deploying smart contracts to a Sandbox environment simulates that process without actually affecting Mainnet. When you deploy the smart contracts, you'll instead deploy to Futurenet, a test network with more cutting-edge features that have not yet been implemented in the Mainnet. + +To build and deploy the contracts in a Sandbox environment, as well as to compile the TypeScript bindings, run the following command in your terminal: + +```sh +npm run setup +``` + +> Note: This command may require write privileges in some cases. If you encounter a "Permission denied" error when running this command, run the following command to grant write privileges to the `initialize.sh` script: +> +> ```sh +> chmod +x initialize.sh +> ``` + +If the command runs successfully, your terminal will return a series of messages notifying you about the successful initialization of the contracts and the post-installation sequence. + +```sh +Deploy the BTC TOKEN contract +Contract deployed successfully with ID: CD5ZAJAPX5AAOB55G7SQEM63EQK5JRUZN2LUCDP66BRWV5HNGPDGJVDD +Deploy the DONATION contract +Contract deployed successfully with ID: CC7U75NM4FQQSPLCSX2FTUCAQ6VI6VBA5LDYSU3FRKXN3X2W3AL2SAJ3 +Deploy the ORACLE contract +Contract deployed successfully with ID: CBK3VIRHK5QGHLKFTWHJI2ABMW2SATMIDPNQSCICCDRJU6DIFEWHLDD2 +Initialize the BTC TOKEN contract + +Done +Initialize the DONATION contract + +Done +Initialize the ORACLE contract + +Done + +> soroban-oracle-project@0.0.0 build-contracts +... +``` + +:::tip + +Please, save your deployed contract ID, you will need it to complete the challenge. + +::: + + + +## Checkpoint 3: ⏯️ Create a CRON task + +A CRON task is a scheduled operation performed at specified intervals, functioning as a customizable backend service. Here, you will run a CRON task to update the BTC price within the Oracle contract. Data can be fetched from an API and set to the contract using the `set_price` function, in this case, the tutorial will use the [CryptoPrice API](https://api-ninjas.com/api/cryptoprice). This is a free API that provides real-time cryptocurrency prices, and it is used in this tutorial for demonstration purposes only. You are free to use any price feed API of your choice. + +The function that updates the price is already implemented in the `cron-script.ts` file. To retrieve the data from the price feed, you will need to specify the following parameters: + +- Secret key of wallet (relayer) that will fetch BTC price from API and set it to smart contract; +- Contract address of deployed Oracle Contract; +- `API_KEY` from https://api-ninjas.com/api/cryptoprice (for free). + +Here is an example of a filled `cron-script.ts` file: + +```ts +const API_NINJA_KEY = "xSuoJa0icEhaQzk3IdomNWFmXK0qNG8lNZofYwJ4"; + +const sourceSecretKey = + "SBDLGM7RKXU6WYM2DRDIBULWAG7I6QDO3AONLERKWZPDDPQMMQ26F7TM"; +const sourceKeypair = SorobanClient.Keypair.fromSecret(sourceSecretKey); +const sourcePublicKey = sourceKeypair.publicKey(); + +const contractId = "CBDRRNXD2UETJJL376B2VT3RS22YPF4NPWRDCMSBZAIQGWJIUM3U7552"; +``` + +To run the CRON task, navigate to the `cron` directory and run: + +```sh +npm install +node cron-script.ts +``` + +Once the CRON task is running, it will fetch the BTC price from the API and set it to the contract every 15 minutes. + +Here is an example of the output: + +```sh +Running a task every 15 minutes +Current Time: 2023-11-08T00:30:00.036Z +lastEpochNr 0 +deltaTimestamp 1699403400 +Need to update the value +fetched priceData 3531576000 +[updatePairPrice] View Transaction: https://futurenet.steexp.com/tx/5c68dd1eb6df7a926fa8fda3a3cd187d2855b74ceaa41469f69b33f81930c33a +[updatePairPrice] response.status: SUCCESS +value set! +``` + +You can specify the frequency of the CRON task by changing the `cron.schedule` parameter in the `cron-script.ts` file. + +For example, to run the task every 5 minutes, change the parameter to: + +```ts +cron.schedule("*/5 * * * *", async () => { + ... +}); +``` + +It's important to note that since the price feed is dependent on the CRON task, the price will not update until the CRON task has run at least once. Furthermore, the CRON task will need to keep running in order to keep the price feed updated. You can host the task on your localhost or on a server such as [Vercel](https://vercel.com/docs/cron-jobs). For this example, the CRON task is hosted on a local host. + +## Checkpoint 4: 🤝 Connect the Frontend to the Backend + +Now that you have the smart contracts deployed, and the cron job running, it's time to check out the frontend of your dapp. + +From the top level directory run the following command to start the development server: + +```sh +npm run dev +``` + +Now, open your browser and visit [http://localhost:5173](http://localhost:5173). You should be able to see the frontend of your dapp. + +> Note: Follow the instructions below and ensure that you have funded your wallet address that you intend to use from the browser. + +Now that you have the frontend running, it's time to connect it with your smart contracts. + +You will need to add some Futurenet network lumens to your wallet to interact with the dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create and or fund an account on Futurenet. + +> Note: These are test lumens for use with Futurenet and cannot be used on Mainnet + +## Checkpoint 5: 🧿 Oracle Insights + +Gaze into the crystal ball of APIs and retrive the price feed from the all connected internet. Data at your fingertips, at one time an abyss but pioneers have paved the way for many to cross. Here you will reach into the realm of off-chain data and pull out the price of BTC. In this section, you will mint tokens and deposit Bitcoin (BTC) into the donation contract. The conversion to USD will utilize the oracle price feed to ensure your donation is aligned with the current BTC to USD conversion rate. + + + + + + + + + +#### Connect a Wallet + +Open the dapp frontend and click the "connect" and choose the wallet you want to use. For this example, you will use Freighter. + + + connect + + + + + +#### Mint USDC and BTC + +Open the `Mint BTC Tokens` tab, enter the desired token amount, and click the "Mint" button. + + + + + + +#### Approve Transaction + +You should see a popup from Freighter asking you to sign the transactions. Click on "Approve" and wait for the transaction to be confirmed. + + + + + + +#### Check Updated Balance + +You should see an updated balance in the account balance component. + + + + + + + + + + + + + + +#### Deposit into the Donation Contract + +Open the frontend, enter the amount of BTC in USD that you would like to donate, then click the "Calculate" button. You should see the amount of BTC that you need to deposit in order to donate the specified amount in USD. For example, if you enter 35923.82 USD, you should see 1 BTC. + +connect + + + + +#### Approve Transaction + +Click on "Approve" and wait for the transaction to be confirmed. + + + + + +#### Check Updated Balance + +Once the transaction is confirmed, you should see the contract and your wallet balances update in the `Donate` and `Mint BTC Tokens` tabs, respectively. + +connect +connect + + + + + + + + + +> Note: These are test tokens for use with Futurenet and cannot be used on Mainnet. + + + Funding completed + + +## Checkpoint 6: 🚢 Ship it! 🚁 + +In this step, you will deploy your dapp to a hosting platform so that it can be accessed by anyone with an internet connection. Note that it's on futurenet, the network used for testing, not mainnet for production use. You can use any hosting platform you like, but for demonstration purposes, this section will use [Vercel](https://vercel.com/). Vercel is a cloud platform for static sites and serverless functions that offers a free tier for developers. It also has a built-in integration with GitHub, which makes it easy to deploy your dapp directly from your GitHub repository. + +If you don't already have a [Vercel account], you will need to create one and link it to your GitHub account. + +[Vercel account]: https://vercel.com/login + +First install the Vercel cli: + +```bash +npm i --global vercel +``` + +Then, remove any existing `.vercel` directory in your project to ensure that you are starting with a clean slate: + +```bash +rm -rf .vercel +``` + +Next, you will need to create a new project on vercel. To do this, run the following command: + +```bash +vercel project add +``` + +For example: + +```bash +vercel project add oracle +``` + +Next you will pull in the project settings locally by running the following command: + +```bash +vercel pull +``` + +Follow the answers to the prompts below to ensure that your local project is correctly linked to the target Vercel project: + +```bash +? Set up “~/Documents/GitHub/test/soroban-dapps-challenge”? [Y/n] y +? Which scope should contain your project? +? Link to existing project? [y/N] y +? What’s the name of your existing project? +``` + +After following the prompts, you should see something similar to the following output: + +```bash +... +🔗 Linked to julian-dev28/oracle (created .vercel) +> Downloading `development` Environment Variables for Project oracle +✅ Created .vercel/.env.development.local file [92ms] + +> Downloading project settings +✅ Downloaded project settings to ~/Documents/GitHub/test/soroban-dapps-challenge/.vercel/project.json [1ms] +``` + +Next, you will need to edit the `settings` section in `.vercel/project.json` to ensure that the `installCommand` is set to `npm i`: + +```diff + "settings": { + "createdAt": 1699390700432, + "framework": null, + "devCommand": null, +- "installCommand": null, ++ "installCommand": "npm i", + "buildCommand": null, + "outputDirectory": null, + "rootDirectory": null, + "directoryListing": false, + "nodeVersion": "18.x" + } +``` + +Next, run the following command to build your dapp: + +```bash +vercel build --prod +``` + +What does the `vercel build` command do? It builds your dapp for production, which means that it optimizes your code for performance and creates an optimized production build of your dapp in the `.vercel/output` directory. This is the directory that you will deploy to Vercel. + +The output of the `vercel build` command should look something like this: + +```bash +$ vite build +.. +dist/assets/index-91e5a562.js 490.69 kB │ gzip: 139.50 kB +dist/assets/ModelViewer-fde23dd9.browser.esm-c08cdb2e.js 826.05 kB │ gzip: 231.17 kB +dist/assets/index-46cc9288.js 3,107.82 kB │ gzip: 888.20 kB +... +✓ built in 11.72s +✨ Done in 12.14s. +✅ Build Completed in .vercel/output [19s] +``` + +Next, you will deploy your dapp to Vercel by running the following command: + +```bash +vercel deploy --prebuilt --prod +``` + +Using the `--prebuilt` flag tells Vercel to deploy the build outputs in `.vercel/output` that you created in the previous step. + +Once the deployment is complete, you should see something similar to the following output: + +```bash +🔍 Inspect: https://vercel.com/julian-dev28/oracle2/Fk7RKb3H4RX1d1kBtHukgppRcv2m [9s] +✅ Production: https://oracle2-3lrfgjzq9-julian-dev28.vercel.app [9s] +``` + +:::tip + +Please, save your production url, you will need it to complete the challenge. + +::: + +You can now visit the preview link to see your deployed dapp! 🎉 + +connect + +Remember, you must add Futurenet network lumens to your Freighter wallet to interact with the deployed example dapp. Visit https://laboratory.stellar.org/#account-creator?network=futurenet, and follow the instructions to create your Freighter account on Futurenet. + +## Checkpoint 7: 💪 Pass the Challenge! + +Now it's time to submit your work! + +Submit your `Production` URL from the previous step into the challenge form to pass the checkpoint! + + + +
    + +:::info + +Join [our Community in Discord](https://discord.gg/stellardev) in case you have any issues or questions. + +::: + +## Checkpoint 8: ✅ Check your work! + +In order to successfully complete this challenge, your work needs to be checked. Please, follow this steps: + +1. Fork [the challenge repository](https://github.com/stellar/soroban-dapps-challenge/fork). +2. Fill `oracle/challenge/output.txt` file with your wallet address. Filled file should look like: + +```sh +Public Key: GBSXUXZSA2VEXN5VGOWE5ODAJLC575JCMWRJ4FFRDWSTRCJ123456789 +``` + +3. Create a Pull Request to the `stellar/soroban-dapps-challenge/oracle` branch. When the PR will be created, CI actions will check the `oracle/challenge/output.txt` file data and update your progress. +4. Wait for the CI/CD pipeline results. +5. Fix errors if present: + +- find the error reason in the Oracle challenge CI results (you can find a link right in the pull request); +- return to your forked repository; +- fix errors and commit changes. The existing PR will be checked again. + +6. If the pipeline was successful, then congratulations! You completed the challenge!👏 + +Invite a friend to try out your dapp and ask them to provide feedback! + +## ⚔️ Side Quests + +🪬 Add a new feature to your dapp (e.g. add a new price feed, add a new token, etc.) + +🌐 Extend your dapp's functionality by allowing users to query and interact with historic oracle data directly through the user interface. + +💡 Develop a function to respond to a significant BTC price change. + +## 📚 User Workflows Checklist + +During this exercise you should be able to: + +- Clone the example repo (Oracle Dapp) +- Set the correct parameters in the `initialize.sh` script +- Deploy your contract to Futurenet +- Deploy the example web ui somewhere (e.g. netlify, vercel, surge, etc.) + +Then via the web UI, you should be able to: + +- Connect your wallet +- See the BTC to USD conversion rate +- See your current balance +- See the contract balance +- Mint an asset +- Deposit an asset +- See your deposit(s) appear on the page as the transactions are confirmed + +## 🛡️🗡️ Take On More Challenges + +View your progress and take on more challenges by visiting your [User Dashboard!](../dashboard) diff --git a/src/pages/docs/learn/interactive/dapps/challenges/styles.css b/src/pages/docs/learn/interactive/dapps/challenges/styles.css new file mode 100644 index 000000000..23ecc7ab0 --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/challenges/styles.css @@ -0,0 +1,31 @@ +.image-style { + border-radius: 10px; /* Rounded corners */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Drop shadow */ + transition: transform 0.3s; /* Smooth transition for hover effect */ + width: 40% + } + + .image-style_reg { + border-radius: 10px; /* Rounded corners */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Drop shadow */ + transition: transform 0.3s; /* Smooth transition for hover effect */ + } + + .image-style_lp { + border-radius: 10px; /* Rounded corners */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Drop shadow */ + transition: transform 0.3s; /* Smooth transition for hover effect */ + width: 85% + } + + .image-style:hover { + transform: translateY(-5px); /* Rise effect on hover */ + } + + .image-style_reg:hover { + transform: translateY(-5px); /* Rise effect on hover */ + } + + .image-style_lp:hover { + transform: translateY(-5px); /* Rise effect on hover */ + } \ No newline at end of file diff --git a/src/pages/docs/learn/interactive/dapps/dashboard/index.tsx b/src/pages/docs/learn/interactive/dapps/dashboard/index.tsx new file mode 100644 index 000000000..00bf42391 --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/dashboard/index.tsx @@ -0,0 +1,183 @@ +import React, { useEffect, useState } from "react"; +import Layout from "@theme/Layout"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import { toast } from "react-toastify"; +import { + LeaderboardParams, + fetchLeaderboard, +} from "@site/src/services/leaderboard"; +import styles from "./style.module.css"; +import { + fetchInitialChallenges, + fetchUserProgress, + resetUserProgress, +} from "@site/src/services/challenges"; +import useAuth from "@site/src/hooks/useAuth"; +import DashboardHeader from "@site/src/components/atoms/dashboard-header"; +import Leaderboard from "@site/src/components/molecules/leaderboard"; +import ChallengesList from "@site/src/components/atoms/challenges-list"; +import { + Challenge, + ChallengeInfo, + Leaderboard as LeaderboardI, + Ranking, +} from "@site/src/interfaces/challenge"; + +export default function Dashboard() { + const { address, isConnected, connect } = useAuth(); + + const [availableChallenges, setAvailableChallenges] = useState( + [], + ); + const [userChallenges, setUserChallenges] = useState([]); + const [leaderboard, setLeaderboard] = useState([]); + const [totalCompleted, setTotalCompleted] = useState(0); + const [ranking, setRanking] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isLeaderboardLoading, setIsLeaderboardLoading] = + useState(false); + + const fetchUserChallenges = async () => { + setIsLoading(true); + try { + const result = await fetchUserProgress(address); + setUserChallenges(result.data?.challenges || []); + setTotalCompleted(result.data?.completedChallenges || 0); + setRanking(result.data?.ranking || null); + } catch (e) { + toast("Something went wrong! Please reload", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + } finally { + setIsLoading(false); + } + }; + + const fetchOnlyLeaderboard = async (params: LeaderboardParams) => { + try { + setIsLeaderboardLoading(true); + const result = await fetchLeaderboard(params); + setLeaderboard(result?.data); + } catch (e) { + toast("Something went wrong! Please reload", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + } finally { + setIsLeaderboardLoading(false); + } + }; + + const onReset = async () => { + try { + setIsLoading(true); + await resetUserProgress(address); + } catch (error) { + toast("Something went wrong! Please try again", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + const fetchData = async () => { + try { + setIsLoading(true); + + const result = await Promise.allSettled([ + fetchInitialChallenges(), + fetchLeaderboard({}), + fetchUserProgress(address), + ]); + + if (result[0].status === "fulfilled") { + setAvailableChallenges(result[0].value.data || []); + } + if (result[1].status === "fulfilled") { + setLeaderboard(result[1].value.data || []); + } + if (result[2].status === "fulfilled") { + setUserChallenges(result[2].value.data?.challenges || []); + setTotalCompleted(result[2].value.data?.completedChallenges || 0); + setRanking(result[2].value.data?.ranking || null); + } + + if ( + result[0].status === "rejected" || + result[1].status === "rejected" || + result[2].status === "rejected" + ) { + throw new Error("Some request error"); + } + setIsLoading(false); + } catch (error) { + setIsLoading(false); + toast("Something went wrong! Please reload", { + type: "error", + hideProgressBar: true, + position: "top-center", + autoClose: 2000, + }); + } + }; + fetchData(); + }, [address]); + + return ( + +
    + {isConnected && address ? ( + + ) : null} + +
    + {isLoading ? ( +

    We're loading the list of challenges...

    + ) : ( + + + + + + + + + )} +
    + + {!isConnected || !address ? ( +
    + Want to take part in our challenges? + +
    + ) : null} +
    +
    + ); +} diff --git a/src/pages/docs/learn/interactive/dapps/dashboard/style.module.css b/src/pages/docs/learn/interactive/dapps/dashboard/style.module.css new file mode 100644 index 000000000..8fd09515e --- /dev/null +++ b/src/pages/docs/learn/interactive/dapps/dashboard/style.module.css @@ -0,0 +1,45 @@ +.dashboard { + display: flex; + flex-direction: column; + flex: 1; + background-color: #F9F9F9; +} + +.dashboardContent { + flex-grow: 2; + width: 75%; + margin: 32px auto; +} + +.dashboardFooter { + display: flex; + justify-content: flex-end; + align-items: center; + flex: 0; + background-color: #FFFFFF; + color: #585858; + padding: 16px 10%; +} + +.challengeCards { + padding: 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: max-content; + grid-gap: 30px; +} + +.loginButton { + font-size: 14px; + font-weight: 500; + font-family: var(--ifm-font-family-base); + background-color: #369EA7; + color: #FFFFFF; + border-radius: 4px; + border: none; + padding: 12px 8px; + line-height: 0.8; + width: max-content; + cursor: pointer; + margin-left: 32px; +} diff --git a/src/pages/index.mdx b/src/pages/index.mdx index 13d297da5..9049dd581 100644 --- a/src/pages/index.mdx +++ b/src/pages/index.mdx @@ -18,17 +18,17 @@ If you can’t find answers to your questions in the docs, search for your answe ### Docs -This section walks you through building on Stellar without smart contracts. Learn basic Stellar functions such as creating accounts and making payments, how to issue assets on the network, how to build an application with the JavaScript SDK, Wallet SDK, and more. +This section walks you through building on Stellar without smart contracts. Learn basic Stellar functions such as creating accounts and making payments, how to issue assets on the network, how to build an application with the JavaScript SDK, Wallet SDK, and more. ### Smart contracts -Smart contracts on Stellar launched on Mainnet following a successful validator vote on February 20th, 2024. Stellar's smart contract platform includes the smart contract environment, a Rust SDK, a CLI, and an RPC server. +Smart contracts on Stellar launched on Mainnet following a successful validator vote on February 20th, 2024. Stellar's smart contract platform includes the smart contract environment, a Rust SDK, a CLI, and an RPC server. This section details how to get started by writing a Hello World contract, then dives deeper into smart contracts on Stellar with various example contracts and how-to guides. Also, learn how to use Stellar assets in smart contracts with the Stellar Asset Contract or how to create your own smart contract token. ### Learn -Find all informational and conceptual content here. Learn about Stellar fundamentals like how accounts and transactions function, dive deeper into the functionality of each operation, discover how fees work, and more. +Find all informational and conceptual content here. Learn about Stellar fundamentals like how accounts and transactions function, dive deeper into the functionality of each operation, discover how fees work, and more. ### Tools @@ -40,7 +40,7 @@ A quick look at useful information such as the various network data (for Mainnet ### Network -Discover various data availability options (RPC, Hubble, and Horizon), how to use the Anchor Platform or Stellar Disbursement Platform, and how to set up a Core Node. +Discover various data availability options (RPC, Hubble, and Horizon), how to use the Anchor Platform or Stellar Disbursement Platform, and how to set up a Core Node. ## Contribute to the docs and leave feedback diff --git a/src/services/challenges.ts b/src/services/challenges.ts new file mode 100644 index 000000000..1a86b61a7 --- /dev/null +++ b/src/services/challenges.ts @@ -0,0 +1,34 @@ +import { AxiosResponse } from "axios"; +import { + Challenge, + UserChallengeData, + UpdateProgressData, + UserProgress, +} from "../interfaces/challenge"; +import { httpClient } from "./http-client"; + +export const fetchInitialChallenges = async () => { + return await httpClient.get("/challenges"); +}; + +export const fetchUserProgress = async (userId: string) => { + return await httpClient.get("/users", { + validateStatus: (status) => { + return (status >= 200 && status < 300) || status === 404; + }, + params: { userId }, + }); +}; + +export const resetUserProgress = async (userId: string) => { + return await httpClient.delete("/users", { + params: { userId }, + }); +}; + +export const updateUserProgress = async (challenge: UpdateProgressData) => { + return await httpClient.post< + Partial, + AxiosResponse + >("/", challenge); +}; diff --git a/src/services/http-client.ts b/src/services/http-client.ts new file mode 100644 index 000000000..181862d15 --- /dev/null +++ b/src/services/http-client.ts @@ -0,0 +1,66 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; + +const headers: Readonly> = { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + "Access-Control-Allow-Credentials": true, +}; + +class HttpClient { + private instance: AxiosInstance | null = null; + + private get http(): AxiosInstance { + return this.instance != null ? this.instance : this.initHttp(); + } + + get>( + url: string, + config?: AxiosRequestConfig, + ): Promise { + return this.http.get(url, config); + } + + post>( + url: string, + data?: T, + config?: AxiosRequestConfig, + ): Promise { + return this.http.post(url, data, config); + } + + put>( + url: string, + data?: T, + config?: AxiosRequestConfig, + ): Promise { + return this.http.put(url, data, config); + } + + patch>( + url: string, + data?: T, + config?: AxiosRequestConfig, + ): Promise { + return this.http.patch(url, data, config); + } + + delete>( + url: string, + config?: AxiosRequestConfig, + ): Promise { + return this.http.delete(url, config); + } + + private initHttp() { + const http = axios.create({ + baseURL: + "https://soroban-dapps-challenge-wrangler.julian-martinez.workers.dev", + headers, + }); + + this.instance = http; + return http; + } +} + +export const httpClient = new HttpClient(); diff --git a/src/services/leaderboard.ts b/src/services/leaderboard.ts new file mode 100644 index 000000000..5a70adcd0 --- /dev/null +++ b/src/services/leaderboard.ts @@ -0,0 +1,31 @@ +import { Leaderboard } from "../interfaces/challenge"; +import { httpClient } from "./http-client"; + +export enum LeaderboardColumn { + ChallengesCompleted = "challengesCompleted", + MinutesSpent = "minutesSpent", + TotalValueLocked = "totalValueLocked", +} + +export type LeaderboardParams = { + colName?: LeaderboardColumn; + direction?: "asc" | "desc"; + pageNumber?: number; +}; + +export const fetchLeaderboard = async ({ + colName, + direction = "asc", + pageNumber, +}: LeaderboardParams) => { + return await httpClient.get("/leaderboard", { + params: { + ...(colName + ? { + sort: `${colName},${direction}`, + } + : {}), + ...(pageNumber ? { pageNumber } : {}), + }, + }); +}; diff --git a/src/sidebar-generator.js b/src/sidebar-generator.js new file mode 100644 index 000000000..35b9329b3 --- /dev/null +++ b/src/sidebar-generator.js @@ -0,0 +1,30 @@ +module.exports = async ({ defaultSidebarItemsGenerator, ...args }) => { + + // Get the sidebar items that are generated by default + const sidebarItems = await defaultSidebarItemsGenerator({ ...args }) + + // Find the "Interactive Learning" category + const interactiveLearning = sidebarItems.find( + (item) => + item.type === 'category' && item.label.toLowerCase() === 'interactive learning' + ) + + // Find the "Dapps Challenge" category within "Interactive Learning" + const dappsChallenge = interactiveLearning?.items.find( + (item) => + item.type === 'category' && item.label.toLowerCase() === 'dapps challenge' + ) + + // If the Dapps Challenge has been found, insert a link to the Dashboard in + // the sidebar. + if (dappsChallenge) { + dappsChallenge.items.splice(1, 0, { + type: 'link', + href: '/docs/learn/interactive/dapps/dashboard', + label: 'Dapps Challenge Dashboard' + }) + } + + // return the sidebar items + return sidebarItems +}; diff --git a/src/store/UserChallengesContextProvider.tsx b/src/store/UserChallengesContextProvider.tsx new file mode 100644 index 000000000..11c01cf0b --- /dev/null +++ b/src/store/UserChallengesContextProvider.tsx @@ -0,0 +1,108 @@ +import React, { PropsWithChildren, useReducer } from "react"; +import UserChallengesContext, { + UserChallengesContextProps, +} from "./user-challenges-context"; +import { ChallengeInfo } from "../interfaces/challenge"; + +interface ChallengesState { + data: ChallengeInfo[]; + address: string; +} + +interface Action { + type: string; + data?: ChallengeInfo[]; + item?: ChallengeInfo; + payload?: string; +} + +enum ActionType { + SET_DATA = "SET_DATA", + UPDATE_PROGRESS = "UPDATE_PROGRESS", + SET_ADDRESS = "SET_ADDRESS", +} + +const defaultState: ChallengesState = { + data: [], + address: "", +}; + +const challengesReducer = (state: ChallengesState, action: Action) => { + if (action.type === ActionType.SET_DATA && action.data) { + return { + ...state, + data: [...action.data], + }; + } + + if (action.type === ActionType.UPDATE_PROGRESS && action.item) { + const { id } = action.item; + const existingItemIdx = state.data.findIndex( + (item: ChallengeInfo) => item.id === id, + ); + const updatedChallenges: ChallengeInfo[] = [...state.data]; + + const existedItem = state.data[existingItemIdx]; + + if (existedItem) { + updatedChallenges[existingItemIdx] = action.item; + } else { + updatedChallenges.push(action.item); + } + + return { + ...state, + data: updatedChallenges, + }; + } + + if (action.type === ActionType.SET_ADDRESS && action.payload) { + return { + ...state, + address: action.payload, + }; + } + + return defaultState; +}; + +const UserChallengesContextProvider = (props: PropsWithChildren) => { + const [state, dispatchAction] = useReducer(challengesReducer, defaultState); + + const setDataHandler = (data: ChallengeInfo[]) => { + dispatchAction({ + type: ActionType.SET_DATA, + data, + }); + }; + + const updateProgressHandler = (item: ChallengeInfo) => { + dispatchAction({ + type: ActionType.UPDATE_PROGRESS, + item, + }); + }; + + const setAddress = (address: string) => { + dispatchAction({ + type: ActionType.SET_ADDRESS, + payload: address, + }); + }; + + const challengesCtx: UserChallengesContextProps = { + data: state.data, + address: state.address, + setAddress, + setData: setDataHandler, + updateProgress: updateProgressHandler, + }; + + return ( + + {props.children} + + ); +}; + +export default UserChallengesContextProvider; diff --git a/src/store/user-challenges-context.ts b/src/store/user-challenges-context.ts new file mode 100644 index 000000000..20fb421d5 --- /dev/null +++ b/src/store/user-challenges-context.ts @@ -0,0 +1,20 @@ +import React from "react"; +import { ChallengeInfo } from "../interfaces/challenge"; + +export type UserChallengesContextProps = { + data: ChallengeInfo[]; + address: string; + setData: (data: ChallengeInfo[]) => void; + updateProgress: (item: ChallengeInfo) => void; + setAddress: (address: string) => void; +}; + +const UserChallengesContext = React.createContext({ + data: [], + address: "", + setData: () => {}, + updateProgress: () => {}, + setAddress: () => {}, +}); + +export default UserChallengesContext; diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx new file mode 100644 index 000000000..d51f2bc13 --- /dev/null +++ b/src/theme/Root.tsx @@ -0,0 +1,27 @@ +import React, { PropsWithChildren } from "react"; +import { futurenet, sandbox, standalone, testnet } from "@soroban-react/chains"; +import { SorobanReactProvider } from "@soroban-react/core"; +import { freighter } from "@soroban-react/freighter"; +import { ChainMetadata, Connector } from "@soroban-react/types"; +import { ToastContainer } from "react-toastify"; +import UserChallengesContextProvider from "../store/UserChallengesContextProvider"; +import "react-toastify/dist/ReactToastify.css"; +import "./style.module.css"; + +const chains: ChainMetadata[] = [sandbox, futurenet, testnet, standalone]; +const connectors: Connector[] = [freighter()]; + +export default function Root({ children }: PropsWithChildren) { + return ( + + + + {children} + + ); +} diff --git a/src/theme/style.module.css b/src/theme/style.module.css new file mode 100644 index 000000000..0925ae0b5 --- /dev/null +++ b/src/theme/style.module.css @@ -0,0 +1,3 @@ +:root { + --toastify-toast-width: 400px; +} \ No newline at end of file diff --git a/src/utils/get-active-challenge.ts b/src/utils/get-active-challenge.ts new file mode 100644 index 000000000..0a96003a1 --- /dev/null +++ b/src/utils/get-active-challenge.ts @@ -0,0 +1,8 @@ +import { ChallengeInfo } from "../interfaces/challenge"; + +export const getActiveChallenge = ( + data: ChallengeInfo[], + challengeId: number, +): ChallengeInfo | undefined => { + return data.find((item: ChallengeInfo) => item.id === challengeId); +}; diff --git a/src/utils/get-contract-balance.ts b/src/utils/get-contract-balance.ts new file mode 100644 index 000000000..01d8b20fe --- /dev/null +++ b/src/utils/get-contract-balance.ts @@ -0,0 +1,53 @@ +import { + Contract, + scValToBigInt, + TransactionBuilder, + TimeoutInfinite, + Address, + SorobanRpc +} from "@stellar/stellar-sdk"; +import { FUTURENET_DETAILS } from "../constants"; + +const XLM_DECIMALS = 7; + +const BASE_FEE = "100"; +const RPC_URLS: { [key: string]: string } = { + FUTURENET: "https://rpc-futurenet.stellar.org/", +}; +const server = new SorobanRpc.Server(RPC_URLS[FUTURENET_DETAILS.network]); + +function formatAmount( + undivided: bigint, + decimals: number = XLM_DECIMALS, +): string { + const n = + undivided.valueOf() < BigInt(Number.MAX_SAFE_INTEGER) + ? Number(undivided) / 10 ** decimals + : undivided.valueOf() / 10n ** BigInt(decimals); + return String(n); +} + +export const getContractBalance = async ( + contractId: string, + address: string, +): Promise => { + const account = await server.getAccount(address); + const contract = new Contract(contractId); + const params = [new Address(address).toScVal()]; + + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: FUTURENET_DETAILS.networkPassphrase, + }) + .addOperation(contract.call("balance", ...params)) + .setTimeout(TimeoutInfinite) + .build(); + + const response = await server.simulateTransaction(transaction); + if (!SorobanRpc.Api.isSimulationSuccess(response)) { + throw response; + } + + const balanceStr = formatAmount(scValToBigInt(response.result!.retval)); + return +balanceStr; +}; diff --git a/static/icons/icon-avatar-1.svg b/static/icons/icon-avatar-1.svg new file mode 100644 index 000000000..ea1b49d65 --- /dev/null +++ b/static/icons/icon-avatar-1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/icons/icon-avatar-10.svg b/static/icons/icon-avatar-10.svg new file mode 100644 index 000000000..f9dbe03eb --- /dev/null +++ b/static/icons/icon-avatar-10.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/static/icons/icon-avatar-2.svg b/static/icons/icon-avatar-2.svg new file mode 100644 index 000000000..424086be8 --- /dev/null +++ b/static/icons/icon-avatar-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/icons/icon-avatar-3.svg b/static/icons/icon-avatar-3.svg new file mode 100644 index 000000000..7ff3975fb --- /dev/null +++ b/static/icons/icon-avatar-3.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/icons/icon-avatar-4.svg b/static/icons/icon-avatar-4.svg new file mode 100644 index 000000000..196614905 --- /dev/null +++ b/static/icons/icon-avatar-4.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/static/icons/icon-avatar-5.svg b/static/icons/icon-avatar-5.svg new file mode 100644 index 000000000..dcc41b291 --- /dev/null +++ b/static/icons/icon-avatar-5.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/icons/icon-avatar-6.svg b/static/icons/icon-avatar-6.svg new file mode 100644 index 000000000..ed5eb9bd7 --- /dev/null +++ b/static/icons/icon-avatar-6.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/icons/icon-avatar-7.svg b/static/icons/icon-avatar-7.svg new file mode 100644 index 000000000..1ff0731ba --- /dev/null +++ b/static/icons/icon-avatar-7.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/icon-avatar-8.svg b/static/icons/icon-avatar-8.svg new file mode 100644 index 000000000..451db5ec2 --- /dev/null +++ b/static/icons/icon-avatar-8.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/icon-avatar-9.svg b/static/icons/icon-avatar-9.svg new file mode 100644 index 000000000..97c7192a4 --- /dev/null +++ b/static/icons/icon-avatar-9.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/static/icons/icon-copy.svg b/static/icons/icon-copy.svg new file mode 100644 index 000000000..70ff20f5a --- /dev/null +++ b/static/icons/icon-copy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/icon-ranking.svg b/static/icons/icon-ranking.svg new file mode 100644 index 000000000..8e1b01051 --- /dev/null +++ b/static/icons/icon-ranking.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/icons/icon-star-yellow.svg b/static/icons/icon-star-yellow.svg new file mode 100644 index 000000000..f41ea0939 --- /dev/null +++ b/static/icons/icon-star-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/icons/icon-star.svg b/static/icons/icon-star.svg new file mode 100644 index 000000000..cb3b513ce --- /dev/null +++ b/static/icons/icon-star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/smiley-face-1.svg b/static/icons/smiley-face-1.svg new file mode 100644 index 000000000..6c44d9d9e --- /dev/null +++ b/static/icons/smiley-face-1.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/static/icons/smiley-face-2.svg b/static/icons/smiley-face-2.svg new file mode 100644 index 000000000..be057548a --- /dev/null +++ b/static/icons/smiley-face-2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 004dc7a16..9be95a66f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1357,6 +1357,14 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@creit-tech/xbull-wallet-connect@github:Creit-Tech/xBull-Wallet-Connect": + version "0.2.0" + resolved "https://codeload.github.com/Creit-Tech/xBull-Wallet-Connect/tar.gz/a93e2e0d97c61bbd83fedcca0c71feef9f28b2d3" + dependencies: + rxjs "^7.5.5" + tweetnacl "^1.0.3" + tweetnacl-util "^0.15.1" + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -2661,16 +2669,141 @@ p-map "^4.0.0" webpack-sources "^3.2.2" +"@soroban-react/chains@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@soroban-react/chains/-/chains-9.0.4.tgz#51130f42f39437246e6e4e95d809949a883d75e4" + integrity sha512-pvkGwxdOY/uK28j6ou6+1T6cw99KQJ4UhgSA759vVEBo0YyVgPJ8t1Qs3EhZ/k1oUA2+BdCJt/D9w/hTUsYgzA== + dependencies: + "@soroban-react/types" "^9.0.4" + "@stellar/stellar-sdk" "11.1.0" + +"@soroban-react/core@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@soroban-react/core/-/core-9.0.4.tgz#8efaa448d1e9361b71af707f9c08b232e575f48c" + integrity sha512-tCKn6f92+89gzNdGmBm7EJBWjWLC0cTubiXT32xBh/xzuVymE3eow8AtjfOXYP7Whb3l+LEKc1nAgIGWVnWfSQ== + dependencies: + "@soroban-react/freighter" "^9.0.4" + "@soroban-react/types" "^9.0.4" + "@soroban-react/xbull" "^1.0.1" + "@stellar/stellar-sdk" "11.1.0" + +"@soroban-react/events@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@soroban-react/events/-/events-9.0.4.tgz#76942419497fb06808f2471f893d48a5f61184e3" + integrity sha512-rVYNwBtR1O5ujxgHYbsdq723QNjvfePEzyaHV2AxueW2iZO/8ACB/IKKpQbaNKhaGDxmWHG263ZofFnnMAA1Lg== + dependencies: + "@soroban-react/core" "^9.0.4" + "@soroban-react/types" "^9.0.4" + soroban-client "1.0.0-beta.4" + +"@soroban-react/freighter@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@soroban-react/freighter/-/freighter-9.0.4.tgz#62b05b217162c27c1562c3e4f988b06b487b36a6" + integrity sha512-WVh8l8jIMlQZJQTT/5fVH9rjEzj48b/bbY9FKshz3JYjV/cs+nPpXjijkEIodzCdKiLat077go3cy0ExVS8HOw== + dependencies: + "@soroban-react/types" "^9.0.4" + "@stellar/freighter-api" "1.7.1" + +"@soroban-react/types@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@soroban-react/types/-/types-8.0.0.tgz#14cb25911d2d45e3448a0fc6c0b2a6ae16b4851e" + integrity sha512-t8eohOlmb4LAD1e3bfI01KKK7oCcm468QKsKrD0fL67fnZYcx9K61tSCqwWEbFCWGMGPkwaOwI3sE/bYKKDMIQ== + +"@soroban-react/types@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@soroban-react/types/-/types-9.0.4.tgz#c0cae5aa59ed212059a489312daf8279464d3ab1" + integrity sha512-WghuBJb6bG9ntHyFfXBu0XhpZEqRmrDZjUK3edCt4z9vSwusGiC/37ALzfJANA0LTbyysRs0S08SPJB1Hl+lgg== + dependencies: + "@stellar/stellar-sdk" "11.1.0" + +"@soroban-react/xbull@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@soroban-react/xbull/-/xbull-1.0.1.tgz#0468bf06e92d7d44c35265c42c93769f1066fe96" + integrity sha512-dk/qoF6CsRMgKWyO++woJmq5bTgo9ZCGBtGaNFOiljliQ7bX6MzvMoeZSwTvNNJZhL18AWpaiWmxCvv2bJJoQg== + dependencies: + "@creit-tech/xbull-wallet-connect" "github:Creit-Tech/xBull-Wallet-Connect" + "@soroban-react/types" "8.0.0" + stellar-sdk "11.1.0" + "@stellar/eslint-config@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@stellar/eslint-config/-/eslint-config-2.1.2.tgz#95c223d10a5dfc2c89486e76cc7c9f1b56df3564" integrity sha512-5aUkncDMmx0SvVlZD4rld5snGKt3mc0Gno1Jik3Pp31HUmpgrkRUD3ZZekEOqB9mDKadZhQZNNsS0jhyuXaayw== +"@stellar/freighter-api@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@stellar/freighter-api/-/freighter-api-1.7.1.tgz#d62b432abc7e0140a6025cd672455ecee7b3199a" + integrity sha512-XvPO+XgEbkeP0VhP0U1edOkds+rGS28+y8GRGbCVXeZ9ZslbWqRFQoETAdX8IXGuykk2ib/aPokiLc5ZaWYP7w== + +"@stellar/freighter-api@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@stellar/freighter-api/-/freighter-api-2.0.0.tgz#488915a4aa0cec8c9a3fc84ef31e21cd5ec41343" + integrity sha512-j/R7MLPL8S3QhwOEdAxSl7MgWBTXWlOXQKQyXR8mPk1JMKKR4tF8e4U+Fs9TPQH0HZoYqfVDvLOOUrTMMY058Q== + +"@stellar/js-xdr@^3.0.1", "@stellar/js-xdr@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@stellar/js-xdr/-/js-xdr-3.1.1.tgz#be0ff90c8a861d6e1101bca130fa20e74d5599bb" + integrity sha512-3gnPjAz78htgqsNEDkEsKHKosV2BF2iZkoHCNxpmZwUxiPsw+2VaXMed8RRMe0rGk3d5GZe7RrSba8zV80J3Ag== + "@stellar/prettier-config@^1.0.1": version "1.2.0" resolved "https://registry.yarnpkg.com/@stellar/prettier-config/-/prettier-config-1.2.0.tgz#b27c411e0c4c63b2d76332c239084e682c37468f" integrity sha512-oL9qJ7+7aWnImpbcldroQrvtMCZ9yx4JL/tmDZ860RpBQd2ahkc8bX6/k2ehFK8gpb9ltYu4mtU49wufUuYhGg== +"@stellar/stellar-base@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@stellar/stellar-base/-/stellar-base-10.0.1.tgz#cf4458e081f694109422521562e53e642c29991b" + integrity sha512-BDbx7VHOEQh+4J3Q+gStNXgPaNckVFmD4aOlBBGwxlF6vPFmVnW8IoJdkX7T58zpX55eWI6DXvEhDBlrqTlhAQ== + dependencies: + "@stellar/js-xdr" "^3.0.1" + base32.js "^0.1.0" + bignumber.js "^9.1.2" + buffer "^6.0.3" + sha.js "^2.3.6" + tweetnacl "^1.0.3" + optionalDependencies: + sodium-native "^4.0.1" + +"@stellar/stellar-base@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@stellar/stellar-base/-/stellar-base-11.0.1.tgz#f6eba54e62aa3e827e55d8338b92cdbf5535cfd9" + integrity sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA== + dependencies: + "@stellar/js-xdr" "^3.1.1" + base32.js "^0.1.0" + bignumber.js "^9.1.2" + buffer "^6.0.3" + sha.js "^2.3.6" + tweetnacl "^1.0.3" + optionalDependencies: + sodium-native "^4.0.10" + +"@stellar/stellar-sdk@11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-11.1.0.tgz#4a619fa645a7392204b6e80cb0a7f3ef94cdd077" + integrity sha512-Ufw+4udr7lqyzPIhqSAzBTgcl/YlgFZLgeBlDr5ZZy1v+g7AT4dOZFurcCrHt7Pz8DGtVcxNX7GLxYLdOC3GIg== + dependencies: + "@stellar/stellar-base" "10.0.1" + axios "^1.6.0" + bignumber.js "^9.1.2" + eventsource "^2.0.2" + randombytes "^2.1.0" + toml "^3.0.0" + urijs "^1.19.1" + +"@stellar/stellar-sdk@^11.3.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz#7cb010651846a07e1853e0fe30e430ece4da340b" + integrity sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w== + dependencies: + "@stellar/stellar-base" "^11.0.1" + axios "^1.6.8" + bignumber.js "^9.1.2" + eventsource "^2.0.2" + randombytes "^2.1.0" + toml "^3.0.0" + urijs "^1.19.1" + "@stellar/tsconfig@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@stellar/tsconfig/-/tsconfig-1.0.2.tgz#18e9b1a1d6076e116bb405d11fc034401155292d" @@ -4248,6 +4381,15 @@ axios@^0.25.0: dependencies: follow-redirects "^1.14.7" +axios@^1.6.0, axios@^1.6.8: + version "1.6.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" + integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -4374,6 +4516,11 @@ base16@^1.0.0: resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ== +base32.js@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" + integrity sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -4402,6 +4549,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bignumber.js@^9.1.1, bignumber.js@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -4994,6 +5146,11 @@ clsx@^1.1.0, clsx@^1.1.1, clsx@^1.2.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" @@ -6270,7 +6427,7 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.2.2: +deepmerge@^4.0.0, deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -7648,7 +7805,7 @@ fnv-plus@^1.3.1: resolved "https://registry.yarnpkg.com/fnv-plus/-/fnv-plus-1.3.1.tgz#c34cb4572565434acb08ba257e4044ce2b006d67" integrity sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw== -follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.7: +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.7, follow-redirects@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== @@ -9386,6 +9543,11 @@ js-levenshtein@^1.1.6: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-xdr@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/js-xdr/-/js-xdr-3.1.1.tgz#38624252272ad520e37ab32b0f7b580136bc7bab" + integrity sha512-FaNXtfzwbc9MedMK0AqAa5A32U33Bbxta9zedSBHHiJmknK7hqXM0W5qrVVa/ID6Trw8/XW31Bca6XtdG5BV/Q== + js-yaml@3.14.1, js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -9652,6 +9814,11 @@ liquid-json@0.3.1: resolved "https://registry.yarnpkg.com/liquid-json/-/liquid-json-0.3.1.tgz#9155a18136d8a6b2615e5f16f9a2448ab6b50eea" integrity sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ== +load-script@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" + integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== + loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" @@ -10087,6 +10254,11 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -10764,6 +10936,11 @@ node-forge@^1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp-build@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== + node-polyfill-webpack-plugin@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz#141d86f177103a8517c71d99b7c6a46edbb1bb58" @@ -11960,6 +12137,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -12181,7 +12363,7 @@ react-error-overlay@^6.0.11, react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== -react-fast-compare@^3.1.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: +react-fast-compare@^3.0.1, react-fast-compare@^3.1.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== @@ -12325,6 +12507,17 @@ react-overflow-list@^0.5.0: dependencies: react-use "^17.3.1" +react-player@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.16.0.tgz#89070700b03f5a5ded9f0b3165d4717390796481" + integrity sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ== + dependencies: + deepmerge "^4.0.0" + load-script "^1.0.0" + memoize-one "^5.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.0.1" + react-redux@^7.2.0: version "7.2.9" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" @@ -12337,6 +12530,11 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" +react-rewards@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-rewards/-/react-rewards-2.0.4.tgz#617f6c1bb591f74bb0e0455cc6ff355ee6d36665" + integrity sha512-Lw7gIhD8yPDzC6boaVmcXwuTHRLSLAdqB3kZc+29YWvdHWsuc3fdAZlxI8Cm8fvD8fhP+3JkZBtzX224czw15w== + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -12405,6 +12603,13 @@ react-textarea-autosize@^8.3.2: use-composed-ref "^1.3.0" use-latest "^1.2.1" +react-toastify@^10.0.5: + version "10.0.5" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e" + integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw== + dependencies: + clsx "^2.1.0" + react-universal-interface@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" @@ -12999,7 +13204,7 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^7.5.4: +rxjs@^7.5.4, rxjs@^7.5.5: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -13333,7 +13538,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.8: +sha.js@^2.3.6, sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -13570,6 +13775,24 @@ sockjs@^0.3.21, sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" +sodium-native@^4.0.1, sodium-native@^4.0.10: + version "4.1.1" + resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-4.1.1.tgz#109bc924dd55c13db87c6dd30da047487595723c" + integrity sha512-LXkAfRd4FHtkQS4X6g+nRcVaN7mWVNepV06phIsC6+IZFvGh1voW5TNQiQp2twVaMf05gZqQjuS+uWLM6gHhNQ== + dependencies: + node-gyp-build "^4.8.0" + +soroban-client@1.0.0-beta.4: + version "1.0.0-beta.4" + resolved "https://registry.yarnpkg.com/soroban-client/-/soroban-client-1.0.0-beta.4.tgz#f2e0b0b1c4511bd04381a3cff3a45cfba7d0dc31" + integrity sha512-M1jLCwQtWZkQIJ6U72nYpvj+giYnB2/Vw4E1DBaiCgg5iWIWatDto+QTI/aUR9m7fNGTt/AhFtQzhjksK1rFkQ== + dependencies: + axios "^1.6.0" + bignumber.js "^9.1.1" + buffer "^6.0.3" + stellar-base v10.0.0-beta.4 + urijs "^1.19.1" + sort-css-media-queries@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.0.4.tgz#b2badfa519cb4a938acbc6d3aaa913d4949dc908" @@ -13761,6 +13984,33 @@ std-env@^3.0.1: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== +stellar-base@v10.0.0-beta.4: + version "10.0.0-beta.4" + resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-10.0.0-beta.4.tgz#818d3b5dd702a7d18f1db47a72837cec80616716" + integrity sha512-3EXDFHSahVDMTHrHiFOO8kFf5KN+AL4x5kd5rxjElElPG+385cyWDbO83GrNmDGU/u9/XiVL+riJjz5gQTv6RQ== + dependencies: + base32.js "^0.1.0" + bignumber.js "^9.1.2" + buffer "^6.0.3" + js-xdr "^3.0.0" + sha.js "^2.3.6" + tweetnacl "^1.0.3" + optionalDependencies: + sodium-native "^4.0.1" + +stellar-sdk@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/stellar-sdk/-/stellar-sdk-11.1.0.tgz#04df0be3bfee2ffd1db068c92dc4f3ec27309103" + integrity sha512-fIdo77ogpU+ecHgs59pk9velpXd4F/ch0DzOI4QZw8zVZApc3oeNWP3+X6ui7BWpeRHAGsP2CHQzBLxm0JTIgg== + dependencies: + "@stellar/stellar-base" "10.0.1" + axios "^1.6.0" + bignumber.js "^9.1.2" + eventsource "^2.0.2" + randombytes "^2.1.0" + toml "^3.0.0" + urijs "^1.19.1" + stickyfill@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stickyfill/-/stickyfill-1.1.1.tgz#39413fee9d025c74a7e59ceecb23784cc0f17f02" @@ -14237,6 +14487,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + totalist@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" @@ -14324,6 +14579,16 @@ tty-browserify@^0.0.1: resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== +tweetnacl-util@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" + integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -14720,6 +14985,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urijs@^1.19.1: + version "1.19.11" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" + integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ== + urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"