diff --git a/.github/workflows/guides.yml b/.github/workflows/guides.yml index 9deead06b..bc1109bba 100644 --- a/.github/workflows/guides.yml +++ b/.github/workflows/guides.yml @@ -33,13 +33,19 @@ jobs: - name: Lint Check run: markdownlint --config .markdownlint.yaml --ignore-path .markdownlintignore '**/*.mdx' - ## QUICKSTART TEST - quickstart-test: + ## QUICKSTART E2E TESTS + quickstart-e2e-tests: timeout-minutes: 30 - name: build-quickstart-contract + name: Quickstart E2E Tests runs-on: ubuntu-latest - steps: + strategy: + matrix: + # note: must match the names in test.spec.ts + guide: + - "dev quickstart" + - "intro to sway" + steps: # SETUP - uses: actions/checkout@v3 with: @@ -74,26 +80,13 @@ jobs: - name: Set Default Beta-4 Toolchain run: fuelup toolchain install beta-4 && fuelup default beta-4 - # BUILD & RUN QUICKSTART CONTRACT TEST - - name: Build Contract - run: forc build --path ./docs/guides/examples/quickstart/counter-contract - - name: Run contract tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --manifest-path ./docs/guides/examples/quickstart/counter-contract/Cargo.toml - - name: Check Cargo fmt & clippy - run: | - cd docs/guides/examples/quickstart/counter-contract - cargo fmt --all --check - cargo clippy --all-targets --all-features + # RUN E2E TESTS + - name: Run Playwright tests for ${{ matrix.guide }} + run: xvfb-run --auto-servernum pnpm test:guides --grep "${{ matrix.guide }}" - # RUN QUICKSTART E2E TEST - - name: Run Playwright tests - run: xvfb-run --auto-servernum pnpm test:guides - uses: actions/upload-artifact@v2 if: always() with: - name: playwright-report + name: playwright-report-${{ matrix.guide }} path: playwright-report/ retention-days: 30 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 435676a2a..e818fc528 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,9 @@ [submodule "docs/fuel-specs"] path = docs/fuel-specs url = https://github.com/FuelLabs/fuel-specs.git +[submodule "docs/guides/examples/intro-to-sway"] + path = docs/guides/examples/intro-to-sway + url = https://github.com/FuelLabs/intro-to-sway.git [submodule "docs/latest/sway"] path = docs/latest/sway url = https://github.com/FuelLabs/sway.git diff --git a/docs/about-fuel/networks/beta-3.md b/docs/about-fuel/networks/beta-3.md index 7dd4a51a6..335b4fd8e 100644 --- a/docs/about-fuel/networks/beta-3.md +++ b/docs/about-fuel/networks/beta-3.md @@ -54,7 +54,7 @@ This installs the following components and versions: To set the `beta-3` toolchain as your default, run ```console -$ fuelup default beta-3 +fuelup default beta-3 default toolchain set to 'beta-3-aarch64-apple-darwin' ``` diff --git a/docs/guides/docs/guides.json b/docs/guides/docs/guides.json index 77053b7c0..79b557868 100644 --- a/docs/guides/docs/guides.json +++ b/docs/guides/docs/guides.json @@ -11,6 +11,12 @@ "featured": true, "tags": ["Getting Started", "Full Stack"] }, + "intro_to_sway": { + "title": "Introduction to Sway for Javascript developers", + "description": "Learn Sway fundamentals by building a marketplace dApp.", + "featured": false, + "tags": ["Full Stack"] + }, "running_a_node": { "title": "Running a Node", "description": "Run a local Fuel node.", diff --git a/docs/guides/docs/installation/index.mdx b/docs/guides/docs/installation/index.mdx index de7a6fa59..9ba7989b3 100644 --- a/docs/guides/docs/installation/index.mdx +++ b/docs/guides/docs/installation/index.mdx @@ -24,13 +24,13 @@ The Fuel toolchain is built on top of the Rust programming language. To install Run the following command in your shell; this downloads and runs `rustup-init.sh`, which in turn downloads and runs the correct version of the `rustup-init` executable for your platform. -{/*install_rust_command:example:start*/} +{/*ANCHOR: install_rust_command*/} ```console curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -{/*install_rust_command:example:end*/} +{/*ANCHOR: install_rust_command*/} Check the official Rust documentation to get more information on [installing the Rust toolchain](https://www.rust-lang.org/tools/install). @@ -51,13 +51,13 @@ This will install `forc`, `forc-client`, `forc-fmt`, `forc-lsp`, `forc-wallet` a πŸ‘‰ Just paste the following line in your terminal and press *Enter*. -{/*install_fuelup_command:example:start*/} +{/*ANCHOR: install_fuelup_command*/} ```sh curl --proto '=https' --tlsv1.2 -sSf https://install.fuel.network/fuelup-init.sh | sh ``` -{/*install_fuelup_command:example:end*/} +{/*ANCHOR_END: install_fuelup_command*/} > 🚧 Be aware that currently we do not natively support Windows. If you wish to use `fuelup` on Windows, please use Windows Subsystem for Linux. @@ -142,13 +142,13 @@ To properly interact with the {props.fuelTestnetInlineCode} network it is necess πŸ‘‰ Run the following command to install the {props.fuelTestnetInlineCode} toolchain: {/*install_testnet:example:end*/} -{/*install_testnet_command:example:start*/} +{/*ANCHOR: install_testnet_command*/} ```console fuelup toolchain install beta-4 ``` -{/*install_testnet_command:example:end*/} +{/*ANCHOR_END: install_testnet_command*/} If the toolchain was successfully installed, you will see this output: @@ -162,13 +162,13 @@ The toolchain was installed correctly, however is not in use yet. Next, you need πŸ‘‰ Set {props.fuelTestnetInlineCode} as your default toolchain with the following command: {/*set_default_testnet:example:end*/} -{/*set_default_testnet_command:example:start*/} +{/*ANCHOR: set_default_testnet_command*/} ```console fuelup default beta-4 ``` -{/*set_default_testnet_command:example:end*/} +{/*ANCHOR_END: set_default_testnet_command*/} You will get the following output indicating that you have successfully set {props.fuelTestnetInlineCode} as your default toolchain. diff --git a/docs/guides/docs/intro-to-sway/checkpoint.mdx b/docs/guides/docs/intro-to-sway/checkpoint.mdx new file mode 100644 index 000000000..a65d540c5 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/checkpoint.mdx @@ -0,0 +1,73 @@ +--- +title: Checkpoint +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Checkpoint + +## Sway Contract Checkpoint + +If you have followed the previous steps correctly your `main.sw` marketplace contract should look like this: + + + +## Building the contract + +Here's a polished version of your instructions: + +To format your contract, execute the command: + + + +```sh +forc fmt +``` + +To compile your contract, navigate to the contract folder and run: + + + +```sh +forc build +``` + +Congratulations! You've successfully written a full contract in Sway! + +Post-compilation, the system will automatically generate `abi.json`, `storage_slots.json`, and `contract.bin`. You can locate these files in the following directory: + +```sh +contract/out/debug/* +``` + +## Deploying the contract + +For detailed steps on deploying this contract, refer to the official Fuel developer quickstart guide: +[Deploy the Contract](/guides/quickstart/building-a-smart-contract/#deploy-the-contract) + +To deploy, use the following command if you've already set up the forc-wallet and have testnet funds in your account. If not, follow the instructions above. + +```sh +forc deploy --testnet +``` + +After deploying, remember to save your contract ID. You'll need it for frontend integration. diff --git a/docs/guides/docs/intro-to-sway/contract-abi.mdx b/docs/guides/docs/intro-to-sway/contract-abi.mdx new file mode 100644 index 000000000..b4af94227 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/contract-abi.mdx @@ -0,0 +1,40 @@ +--- +title: ABI +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Defining the ABI + +Next, we will define our ABI. ABI stands for Application Binary Interface. In a Sway contract, it serves as an outline of all the functions within the contract. For each function, you need to specify its name, input types, return types, level of storage access, and if it's payable. + +The ABI for our contract is structured as follows. Write the ABI provided below into your `main.sw` file: + + + + + +Don't be worried about understanding the specifics of each function at this moment. We will dive into detailed explanations in the "Functions" section. + +## Functions Structure + +A function is defined using the `fn` keyword. In Sway, snake case is the convention, so instead of naming a function `myFunction`, you would name it `my_function`. + +If the function returns a value, its return type must be defined using a skinny arrow. Additionally, if the function has parameters, their types must also be specified. Semicolons are *required* at the end of each statement. + +If a function either reads from or writes to storage, you need to specify the access level above the function using annotations like `#[storage(read)]` or `#[storage(read, write)]`. + +For functions that are expected to receive funds when called, such as the `buy_item` function, the `#[payable]` annotation is required. diff --git a/docs/guides/docs/intro-to-sway/contract-errors.mdx b/docs/guides/docs/intro-to-sway/contract-errors.mdx new file mode 100644 index 000000000..56fa0d6d9 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/contract-errors.mdx @@ -0,0 +1,40 @@ +--- +title: Errors +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Defining Error Handling + +Enumerations, commonly referred to as enums, are a type that can represent one of several possible variants. Within our contract, we can employ enums to craft custom error messages, facilitating more precise error handling within functions. + +Copy the custom error block into your `main.sw` file: + + + + + +Within our contract, we can anticipate various scenarios where we'd want to throw an error and halt the transaction: + +1. Someone might attempt to pay for an item using an incorrect currency. +2. An individual could try to purchase an item without possessing sufficient coins. +3. Someone other than the owner might attempt to withdraw funds from the contract. + +For each situation, we can define specific return types for the errors: + +- For the `IncorrectAssetId` error, we can return the submitted asset id, which is of type `AssetId`. +- In the case of the `NotEnoughTokens` error, we can define the return type as `u64` to indicate the number of coins involved. +- For the `OnlyOwner` error, we can utilize the `Identity` of the message sender as the return value. diff --git a/docs/guides/docs/intro-to-sway/contract-functions.mdx b/docs/guides/docs/intro-to-sway/contract-functions.mdx new file mode 100644 index 000000000..ebb26b1a2 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/contract-functions.mdx @@ -0,0 +1,337 @@ +--- +title: Functions +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Defining the Contract Functions + +Finally, it's time to compose our contract functions. Begin by copying and pasting the ABI we outlined earlier. It's crucial to ensure that the functions within the contract *exactly* align with the ABI; otherwise, the compiler will generate an error. Now, substitute the semicolons at the conclusion of each function with curly brackets. Also, modify `abi SwayStore` to `impl SwayStore for Contract`, as demonstrated below: + + + +This guide will first show each of the completed functions above. Then, we'll break it down to explain each part, clarify specific syntax, and discuss fundamental concepts in Sway. + +## 1. Listing an item + +Our first function enables sellers to list an item for sale. They can specify the item's price and provide a string that references externally-stored data about the item. + + + +### Updating list storage + +The initial step involves incrementing the `item_counter` in storage, which will serve as the item's ID. In Sway, all storage variables are contained within the `storage` keyword, ensuring clarity and preventing conflicts with other variable names. This also allows developers to easily track when and where storage is accessed or altered. The standard library in Sway provides `read()`, `write()`, and `try_read()` methods to access or manipulate contract storage. It's advisable to use `try_read()` when possible to prevent potential issues arising from accessing uninitialized storage. In this case, we read the current count of listed items, modify it, and then store the updated count back into storage, making use of the well-organized and conflict-free storage system. + +When a function returns an `Option` or `Result` type, we can use `unwrap()` to access its inner value. For instance, `try_read()` returns an `Option` type. If it yields `Some`, we get the contained value; but if it returns `None`, the contract call is immediately halted. + + + +### Getting the message sender + +Next, we'll retrieve the `Identity` of the account listing the item. + +To obtain the `Identity`, utilize the `msg_sender` function from the standard library. The `msg_sender` represents the address of the entity (be it a user address or another contract address) initiating the current function call. + +This function yields a `Result`, which is an enum type that can either be OK or an error. Use the `Result` type when anticipating a value that might result in an error. For example in the case of `msg_sender` when an external caller is involved and the coin input owners differ, identifying the caller becomes impossible. In such edge cases, an `Err(AuthError)` is returned. + +```sway +enum Result { + Ok(T), + Err(E), +} +``` + +In Sway, you can define a variable using either `let` or `const`. + + + +To retrieve the inner value, you can use the `unwrap` method. It returns the contained value if the `Result` is OK and triggers a panic if the result indicates an error. + +### Creating a new item + +You can instantiate a new item using the `Item` struct. Use the `item_counter` value from storage as the ID, set the price and metadata based on the input parameters, and initialize `total_bought` to 0. + +Since the `owner` field requires an `Identity` type, you should utilize the sender value obtained from `msg_sender()`. + + + +### Updating a StorageMap + +Lastly, add the item to the `item_map` within storage using the `insert` method. Utilize the same ID for the key and designate the item as its corresponding value. + + + +## 2. Buying an item + +Next, we aim to allow buyers to purchase listed items. To achieve this, we'll need to: + +1. Accept the desired item ID from the buyer as a function parameter. +2. Ensure the buyer is paying the correct price with valid coins. +3. Increase the `total_bought` count for that item. +4. Deduct a contract fee from the item's cost and transfer the remaining amount to the seller. + + + +### Verifying payment + +We can use the `msg_asset_id` function from the standard library to obtain the asset ID of the coins being transferred in the transaction. + + + +Next, we'll utilize the `require` statement to ensure the sent asset is the correct one. + +The `require` statement accepts two arguments: a condition, and a value that's logged when the condition is false. Should the condition evaluate as false, the entire transaction is rolled back, leaving no changes. + +In this case, the condition checks if the `asset_id` matches the `BASE_ASSET_ID`β€”the default asset associated with the base blockchain, imported from the standard library. For example, if the base blockchain is Ethereum, the base asset would be ETH. + +If there's a mismatch in the asset, for instance, if someone attempts to purchase an item using a different coin, we'll trigger the custom error previously defined, passing along the `asset_id`. + + + +Next, we can use the `msg_amount` function from the standard library to retrieve the quantity of coins transmitted by the buyer within the transaction. + + + +To ensure the sent amount is not less than the item's price, we should retrieve the item details using the `item_id` parameter. + +To obtain a value for a specific key in a storage map, the `get` method is handy, wherein the key value is passed. For mapping storage access, the `try_read()` method is utilized. As this method produces a `Result` type, the `unwrap` method can be applied to extract the item value. + + + +In Sway, all variables are immutable by default, whether declared with `let` or `const`. To modify the value of any variable, it must be declared mutable using the `mut` keyword. Since we plan to update the item's `total_bought` value, it should be defined as mutable. + +Additionally, it's essential to ensure that the quantity of coins sent for the item isn't less than the item's price. + + + +### Updating buy storage + +We can increase the item's `total_bought` field value and subsequently reinsert it into the `item_map`. This action will replace the earlier value with the revised item. + + + +### Transferring payment + +Lastly, we can process the payment to the seller. It's recommended to transfer assets only after all storage modifications are completed to prevent [reentrancy attacks](/docs/sway/blockchain-development/calling_contracts/#handling-re-entrancy). + +For items reaching a specific price threshold, a fee can be deducted using a conditional `if` statement. The structure of `if` statements in Sway mirrors that in JavaScript except for the brackets `()`. + + + +In the aforementioned if-condition, we assess if the transmitted amount surpasses 100,000,000. For clarity in large numbers like `100000000`, we can represent it as `100_000_000`. If the foundational asset for this contract is ETH, this equates to 0.1 ETH given that Fuel uses a 9 decimal system. + +Should the amount exceed 0.1 ETH, a commission is determined and then deducted from the total. + +To facilitate the payment to the item's owner, the `transfer` function is utilized. This function, sourced from the standard library, requires three parameters: the Identity to which the coins are sent, the coin's asset ID, and the coin quantity for transfer. + +## 3. Get an item + +To get the details for an item, we can create a read-only function that returns the `Item` struct for a given item ID. + + + +To return a value in a function, you can use the `return` keyword, similar to JavaScript. Alternatively, you can omit the semicolon in the last line to return that value like in Rust. Although both methods are effective. + +```sway +fn my_function(num: u64) -> u64{ + // returning the num variable + return num; + + // this would also work: + num +} +``` + +## 4. Initialize the owner + +This method sets the owner's `Identity` for the contract but only once. + + + +To ensure that this function can only be called once, specifically right after the contract's deployment, it's imperative that the owner's value remains set to `None`. We can achieve this verification using the `is_none` method, which assesses if an Option type is `None`. + +It's also important to note the potential risk of [front running](https://scsfg.io/hackers/frontrunning/) in this context this code has not been audited. + + + +To assign the `owner` as the message sender, it's necessary to transform the `Result` type into an `Option` type. + + + +Finally, we'll return the `Identity` of the message sender. + + + +## 5. Withdraw funds + +The `withdraw_funds` function permits the owner to withdraw any accumulated funds from the contract. + + + +First, we'll ensure that the owner has been initialized to a specific address. + + + +Next, we'll verify that the individual attempting to withdraw the funds is indeed the owner. + + + +Additionally, we can confirm the availability of funds for withdrawal using the `this_balance` function from the standard library. This function returns the current balance of the contract. + + + +Lastly, we'll transfer the entire balance of the contract to the owner. + + + +## 6. Get the total items + +The final function we'll introduce is `get_count`. This straightforward getter function returns the value of the `item_counter` variable stored in the contract's storage. + + + +## Review + +The `SwayStore` contract implementation in your `main.sw` should now look like this, following everything else we have previously written: + + + + diff --git a/docs/guides/docs/intro-to-sway/contract-imports.mdx b/docs/guides/docs/intro-to-sway/contract-imports.mdx new file mode 100644 index 000000000..45bb3e7ef --- /dev/null +++ b/docs/guides/docs/intro-to-sway/contract-imports.mdx @@ -0,0 +1,46 @@ +--- +title: Imports +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Imports + +The [Sway standard library](https://fuellabs.github.io/sway/master/std/) provides several utility types and methods we can use in our contract. To import a library, you can use the `use` keyword and `::`, also called a namespace qualifier, to chain library names like this: + + + +You can also group together imports using curly brackets: + + + +For this contract, here is what needs to be imported. Copy this to your `main.sw` file: + + + + + +We'll go through what each of these imports does as we use them in the next steps. diff --git a/docs/guides/docs/intro-to-sway/contract-storage.mdx b/docs/guides/docs/intro-to-sway/contract-storage.mdx new file mode 100644 index 000000000..bf7b0cc63 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/contract-storage.mdx @@ -0,0 +1,58 @@ +--- +title: Storage +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Defining The Storage Block + +Next, we'll introduce the storage block. This is where you store all persistent state variables in your contract. + +Variables declared within a function and not saved in the storage block will be discarded once the function completes its execution. Add the storage block below to your `main.sw` file: + + + + + +The first variable we've stored is `item_counter`, a number initialized to 0. This counter can be used to track the total number of items listed. + +## StorageMap + +A `StorageMap` is a unique type that permits the saving of key-value pairs within a storage block. + +To define a storage map, you need to specify the types for both the key and the value. For instance, in the example below, the key type is `u64`, and the value type is an `Item` struct. + + + +Here, we are creating a mapping from the item's ID to the `Item` struct. Using this, we can retrieve information about an item using its ID. + +## Options + +Here, we are defining the `owner` variable as one that can either be `None` or hold an `Identity`. + + + +If you want a value to be potentially null or undefined under specific conditions, you can employ the `Option` type. It's an enum that can take on either `Some(value)` or `None`. The keyword `None` indicates the absence of a value, while `Some` signifies the presence of a stored value. diff --git a/docs/guides/docs/intro-to-sway/contract-structs.mdx b/docs/guides/docs/intro-to-sway/contract-structs.mdx new file mode 100644 index 000000000..f864c6147 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/contract-structs.mdx @@ -0,0 +1,54 @@ +--- +title: Structs +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Defining an Item Struct + +Struct is short for structure, which is a data structure similar to an object in JavaScript. You define a struct with the `struct` keyword in Sway and define the fields of a struct inside curly brackets. + +The core of our program is the ability to list, sell, and get `items`. + +Let's define the Item type as shown below to write into your `main.sw` file: + + + + + +The item struct will contain an ID, price, the owner's identity, a string representing a URL or identifier for off-chain data about the item (such as its description and photos), and a "total bought" counter to track the overall number of purchases. + +## Types + +The `Item` struct uses three types: `u64`, `str`, and `Identity`. + +`u64`: a 64-bit unsigned integer. + +In Sway, there are four native types of numbers: + +- `u8`: an 8-bit unsigned integer. +- `u16`: a 16-bit unsigned integer. +- `u32`: a 32-bit unsigned integer. +- `u64`: a 64-bit unsigned integer. +- `u256`: a 256-bit unsigned integer. + +An unsigned integer means there is no `+` or `-` sign, making the value always positive. `u64` is the default type used for numbers in Sway. + +In JavaScript, there are two types of integers: `number` and `BigInt`. The primary difference between these types is that `BigInt` can store much larger values. Similarly, each numeric type in Sway has its maximum value that can be stored. + +`String Array`: a string is a built-in primitive type in Sway. The number inside the square brackets indicates the size of the string. + +`Identity`: an enum type that represents either a user's `Address` or a `ContractId`. In Sway, a contract and an EOA (Externally Owned Account) are distinctly differentiated. Both are type-safe wrappers for `b256`. diff --git a/docs/guides/docs/intro-to-sway/explore-fuel.mdx b/docs/guides/docs/intro-to-sway/explore-fuel.mdx new file mode 100644 index 000000000..bc985ac89 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/explore-fuel.mdx @@ -0,0 +1,35 @@ +--- +title: Explore Fuel +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Congrats on completing the intro to Sway guide! πŸŽ‰ + +Encountering issues? A useful initial step is to align your code with the repository's and address any discrepancies. Check out the project's repository [here](https://github.com/FuelLabs/intro-to-sway/tree/main). πŸ” + +Excited about your achievement? Share it with us on Twitter [@fuel_network](https://twitter.com/fuel_network). By doing so, you could gain access to an exclusive community of developers, receive an invitation to our upcoming Fuel dinner, or even get insider information about the project. Keep an eye out for surprises! πŸ‘€ + +## Keep building on Fuel + +Ready to keep building? You can dive deeper into Sway and Fuel in the resources below: + +πŸ“˜ [Read the Sway Book](/docs/sway/) + +✨ [Build a frontend with the TypeScript SDK](/docs/fuels-ts/) + +πŸ¦€ [Write tests with the Rust SDK](/docs/fuels-rs/) + +πŸ”§ [Learn how to use Fuelup](/docs/fuelup/) + +πŸƒβ€ [Follow the Fuel Quickstart](/guides/quickstart/) + +πŸ“– [See Example Sway Applications](https://github.com/FuelLabs/sway-applications) + +🐦 [Follow Sway Language on Twitter](https://twitter.com/SwayLang) + +πŸ‘Ύ [Join the Fuel Discord](https://discord.com/invite/xfpK4Pe) + +❓ [Ask questions in the Fuel Forum](https://forum.fuel.network/) diff --git a/docs/guides/docs/intro-to-sway/index.mdx b/docs/guides/docs/intro-to-sway/index.mdx new file mode 100644 index 000000000..4d72f531d --- /dev/null +++ b/docs/guides/docs/intro-to-sway/index.mdx @@ -0,0 +1,44 @@ +--- +title: Intro to Sway +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Introduction to Sway Language for JavaScript Developers + +If you're familiar with JavaScript and have a basic understanding of blockchain fundamentals, you can swiftly grasp how to build full-stack decentralized applications on Fuel using Sway. Once you get a handle on Sway's essentials, you'll be able to begin building your own dapp. + +Within this tutorial, we will be crafting a Sway contract for an online marketplace similar to Amazon, where: + +1. Sellers can list products. +2. Buyers can purchase those products. + + +![intro to sway app](/images/intro-to-sway.gif) + + +One of the compelling features of smart contracts is their immutability and permissionless nature. This ensures that no single entity can modify or adjust the rules of the marketplace after its deployment. For instance, once a product is listed in the contract, the deployer cannot suddenly alter its status. Similarly, if a commission amount is hardcoded into the contract, it remains fixed, preventing any changes to the commission charged for products. + +Furthermore, the contract remains open for interaction by anyone. This universality allows any individual to engage with the marketplace using their custom frontend without requiring permission. + +In this tutorial, our attention will be specifically directed towards the `contract` program type. This is just one of the [four program types](/docs/sway/sway-program-types/) inherent to the Sway language. + +## What is Sway? + +Sway is a strongly-typed programming language based on Rust, designed for authoring smart contracts on the Fuel blockchain. It leverages Rust's performance, control, and safety attributes, making it suitable for a blockchain virtual machine environment that's optimized for gas costs and contract security. + +Sway is bolstered by a robust compiler and toolchain. These tools simplify the complexities and ensure that your code is efficient, secure, and performs optimally. + +What truly distinguishes Sway is the exceptional suite of tools built around it. These tools are meticulously designed to convert contracts into full-stack dapps, ensuring a seamless and unparalleled developer experience. + +πŸ“š [Sway Standard Library](https://fuellabs.github.io/sway/master/std/): A native library equipped with useful types and methods. + +πŸ§‘β€πŸ”§ [Fuelup](https://docs.fuel.network/docs/fuelup/): The official Fuel toolchain manager aids in installing and managing different versions. + +πŸ¦€ [Fuel's Rust SDK](https://docs.fuel.network/docs/fuels-rs/): Test and interact with your Sway contracts using Rust. + +⚑ [Fuel's TypeScript SDK](https://docs.fuel.network/docs/fuels-ts/): Test and interact with your Sway contracts using TypeScript. + +πŸ”­ [Fuel Indexer](https://docs.fuel.network/docs/indexer/): Craft your own indexer to organize and query on-chain data. diff --git a/docs/guides/docs/intro-to-sway/prerequisites.mdx b/docs/guides/docs/intro-to-sway/prerequisites.mdx new file mode 100644 index 000000000..21ad34372 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/prerequisites.mdx @@ -0,0 +1,167 @@ +--- +title: Prerequisites +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Prerequisites + +## Installation + + + + + + + + + + + + + + + + + + + +Additionally for this guide, ensure you're using Node.js/npm version `18.14.1` or higher (up to node.js `19.0.0`). Check your Node.js version with: + +```console +node -v +``` + +Update node.js if your version is below `18.14.1`. + +## Project Setup + +Start with a new empty folder and name it `fuel-project`. + + + +```sh +mkdir fuel-project +``` + +Go into the `fuel-project` folder: + +```sh +cd fuel-project +``` + +Within your terminal start by creating a new sway project called `contract`: + + + +```sh +forc new contract +``` + +Your project structure generated from the `forc` command should like this: + + + +```console +tree contract +``` + +```console +contract +β”œβ”€β”€ Forc.toml +└── src + └── main.sw + +1 directory, 2 files +``` + +Move into your contract folder: + +```sh +cd contract +``` + +Open up the `contract` folder in VSCode, and inside the `src` folder you should see a file called `main.sw`. This is where you will write your Sway contract. + +Since we're creating a brand new contract you can delete everything in this file except for the `contract` keyword. + + + + + +The first line of the file is specifically reserved to inform the compiler whether we are writing a contract, script, predicate, or library. To designate the file as a contract, use the `contract` keyword. diff --git a/docs/guides/docs/intro-to-sway/rust-sdk.mdx b/docs/guides/docs/intro-to-sway/rust-sdk.mdx new file mode 100644 index 000000000..5121ad5d6 --- /dev/null +++ b/docs/guides/docs/intro-to-sway/rust-sdk.mdx @@ -0,0 +1,211 @@ +--- +title: Rust Testing +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Testing the contract + +## Generating a Test Template in Rust + +To create your own test template using Rust, follow these steps with `cargo-generate` in the contract project directory: + +1. Install `cargo-generate`: + +```bash +cargo install cargo-generate +``` + +{/*markdownlint-disable*/} +2. Generate the template: +{/*markdownlint-disable*/} + + + +```bash +cargo generate --init fuellabs/sway templates/sway-test-rs --name contract +``` + +## Imports + +We will be changing the existing `harness.rs` test file that has been generated. Firstly we need to change the imports. By importing the Fuel Rust SDK you will get majority of the functionalities housed within the prelude. + + + + + +Always compile your contracts after making any changes. This ensures you're working with the most recent `contract-abi` that gets generated. + + + + + +## Initializing Functions + +When writing tests for Sway, two crucial objects are required: the contract instance and the wallets that interact with it. This helper function ensures a fresh start for every new test case so copy this into your test file. It will export the deployed contracts, the contract ID, and all the generated wallets for this purpose. + + + + + +### Contract Storage and Binary + +Besides the ABI, which outlines the interaction protocol with the smart contract, it's imperative to also load the contract's storage and binary. These three components are vital for the successful creation and deployment of the contract instance, guaranteeing precise testing in the following phases. + + + +## Test Cases + +Given the immutable nature of smart contracts, it's not only vital to test basic functionalities but also imperative to thoroughly cover all potential edge cases. Let's write the test cases at the bottom of our harness.rs file. + +### Setting Owner + +For this test case, we use the contract instance and use the SDK's `.with_account()` method. This lets us impersonate the first wallet. To check if the owner has been set correctly, we can see if the address given by the contract matches wallet 1's address. If you want to dig deeper, looking into the contract storage will show if wallet 1's address is stored properly. + + + + + +### Setting Owner Once + +An edge case we need to be vigilant about is an attempt to set the owner twice. We certainly don't want unauthorized ownership transfer of our contract! To address this, we've included the following line in our Sway contract: `require(owner.is_none(), "owner already initialized");` +This ensures the owner can only be set when it hasn't been previously established. To test this, we create a new contract instance: initially, we set the owner using wallet 1. Any subsequent attempt to set the owner with wallet 2 should be unsuccessful. + + + + + +### Buying and Selling in the Marketplace + +It's essential to test the basic functionalities of a smart contract to ensure its proper operation. For this test, we have two wallets set up: + +1. The first wallet initiates a transaction to list an item for sale. This is done by calling the `.list_item()` method, specifying both the price and details of the item they're selling. +2. The second wallet proceeds to purchase the listed item using the `.buy_item()` method, providing the index of the item they intend to buy. + +Following these transactions, we'll assess the balances of both wallets to confirm the successful execution of the transactions. + + + + + +### Withdraw Owner Fees + +Most importantly, as the creator of the marketplace, you need to ensure you're compensated. Similar to the previous tests, we'll invoke the relevant functions to make an exchange. This time, we'll verify if you can extract the difference in funds. + + + + + +## Running the Tests + +To run the test located in `tests/harness.rs`, use: + + + +```bash +cargo test +``` + +If you wish to print outputs to the console during tests, execute: + +```bash +cargo test -- --nocapture +``` + +Now that we're confident in the functionality of our smart contract, it's time to build a frontend. This will allow users to seamlessly interact with our new marketplace! diff --git a/docs/guides/docs/intro-to-sway/typescript-sdk.mdx b/docs/guides/docs/intro-to-sway/typescript-sdk.mdx new file mode 100644 index 000000000..f9724e28f --- /dev/null +++ b/docs/guides/docs/intro-to-sway/typescript-sdk.mdx @@ -0,0 +1,696 @@ +--- +title: Typescript Frontend +category: Intro to Sway +parent: + label: All Guides + link: /guides +--- + +# Building the Frontend + +## Setup + +Initialize a new React app with TypeScript in the same parent folder as your contract using the command below. + + + +```sh +npx create-react-app frontend --template typescript +``` + +Let's go into the frontend folder: + +```sh +cd frontend +``` + +Next, install the fuels Typescript and wallet SDKs in the frontend folder: + + + +```sh +npm install fuels@0.66.1 @fuel-wallet/sdk@0.13.7 @fuel-wallet/react@0.13.7 --save +``` + +Generate types from your contract with `fuels typegen` by running: + + + +```sh +npx fuels typegen -i ../contract/out/debug/*-abi.json -o ./src/contracts +``` + +Open the `src/App.tsx` file, and replace the boilerplate code with the template below: + + + + + +At the top of the file, change the `CONTRACT_ID` to the contract ID that you deployed earlier and set as a constant. + + + +Copy and paste the CSS code below in your `App.css` file to add some simple styling. + + + + + +### Connecting to the contract + +Wrap your components with `FuelProvider` from `@fuel-wallet/react` to enable Fuel's custom React hooks for wallet functionalities in `index.tsx`. + + + + + +React hooks from the Fuel Wallet package are used in order to connect our wallet to the dapp. In the `App` function, we can call these hooks like this: + + + +The `wallet` variable from the `useWallet` hook will have the type `FuelWalletLocked`. + +You can think of a locked wallet as a user wallet you can't sign transactions for, and an unlocked wallet as a wallet where you have the private key and are able to sign transactions. + + + +The `useMemo` hook is used to connect to our contract with the connected wallet. + + + +If the user doesn't have the `fuel` object in their window, we know that they don't have the Fuel wallet extension installed. +If they have it installed, we can then check if the wallet is connected. + + + +Now we have our contract connection ready. You can console log the contract here to make sure this is working correctly. + +### UI + +In our app we're going to have two tabs: one to see all of the items listed for sale, and one to list a new item for sale. + +We use another state variable called `active` that we can use to toggle between our tabs. We can set the default tab to show all listed items. + + + +Below the header, we added a nav section to toggle between the two options. + + + +Next we can create our components to show and list items. + +### Listing an Item + +Create a new folder in the `src` folder called `components`. + + + +```sh +mkdir components +``` + +Then create a file inside called `ListItem.tsx`. + + + +```sh +touch ListItem.tsx +``` + +At the top of the file, import the `useState` hook from `react`, the generated contract ABI from the `contracts` folder, and `bn` (big number) type from `fuels`. + + + + + +This component will take the contract we made in `App.tsx` as a prop, so let's create an interface for the component. + + + + + +We can set up the template for the function like this. + + + + + +To list an item, we'll create a form where the user can input the metadata string and price for the item they want to list. +Let's start by adding some state variables for the `metadata` and `price`. We can also add a `status` variable to track the submit status. + + + + + +We need to add the `handleSubmit` function. +We can use the contract prop to call the `list_item` function and pass in the `price` and `metadata` from the form. + + + + + +Under the heading, add the code below for the form: + + + + + +Now, try listing an item to make sure this works. +You should see the message `Item successfully listed!`. + +### Show All Items + +Next, let's create a new file called `AllItems.tsx` in the `components` folder. + + + +```sh +touch AllItems.tsx +``` + +Copy and paste the template code below for this component: + + + + + +Here we can get the item count to see how many items are listed, and then loop through each of them to get the item details. + +First, let's create some state variables to store the number of items listed, an array of the item details, and the loading status. + + + + + +Next, let's fetch the items in a `useEffect` hook. +Because these are read-only functions, we can simulate a dry-run of the transaction by using the `get` method instead of `call` so the user doesn't have to sign anything. + + + + + +If the item count is greater than `0` and we are able to successfully load the items, we can map through them and display an item card. + +The item card will show the item details and a buy button to buy that item, so we'll need to pass the contract and the item as props. + + + + + +### Item Card + +Now let's create the item card component. +Create a new file called `ItemCard.tsx` in the components folder. + + + +```sh +touch ItemCard.tsx +``` + +After, copy and paste the template code below. + + + + + +Add a `status` variable to track the status of the buy button. + + + + + +Create a new async function called `handleBuyItem`. +Because this function is payable and transfers coins to the item owner, we'll need to do a couple special things here. + +Whenever we call any function that uses the transfer or mint functions in Sway, we have to append the matching number of variable outputs to the call with the `txParams` method. Because the `buy_item` function just transfers assets to the item owner, the number of variable outputs is `1`. + +Next, because this function is payable and the user needs to transfer the price of the item, we'll use the `callParams` method to forward the amount. With Fuel you can transfer any type of asset, so we need to specify both the amount and the asset ID. + + + + + +Then add the item details and status messages to the card. + + + + + +Now you should be able to see and buy all of the items listed in your contract. + +### Checkpoint + +Ensure that all your files are correctly configured by examining the code below. If you require additional assistance, refer to the repository [here](https://github.com/FuelLabs/intro-to-sway/tree/main/frontend) + +`App.tsx` + + + +`AllItems.tsx` + + + +`ItemCard.tsx` + + + +`ListItem.tsx` + + + +### Run your project + +Inside the `fuel-project/frontend` directory run: + +' --name 'react-dapp' --cwd ./guides-testing/fuel-project/frontend" +}} +/> + +```console +npm start +``` + +```console +Compiled successfully! + +You can now view frontend in the browser. + + Local: http://localhost:3000 + On Your Network: http://192.168.4.48:3000 + +Note that the development build is not optimized. +To create a production build, use npm run build. +``` + +And that's it for the frontend! You just created a whole dapp on Fuel! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guides/docs/nav.json b/docs/guides/docs/nav.json index f6e688ee7..4b7911592 100644 --- a/docs/guides/docs/nav.json +++ b/docs/guides/docs/nav.json @@ -3,5 +3,6 @@ "installation": ["Fuel Github Codespace"], "quickstart": ["Building a Smart Contract", "Building a Frontend"], "running_a_node": ["Running a Local Node", "Running a Beta-4 Node"], - "migration_guide": ["Breaking Changes Log", "Testnet Migration"] + "migration_guide": ["Breaking Changes Log", "Testnet Migration"], + "intro_to_sway": ["Introduction to Sway", "Prerequisites", "Imports", "Structs", "ABI", "Storage", "Errors", "Functions", "Checkpoint", "Rust Testing", "Typescript Frontend", "Explore Fuel"] } diff --git a/docs/guides/docs/quickstart/building-a-smart-contract.mdx b/docs/guides/docs/quickstart/building-a-smart-contract.mdx index e58d222d1..202141dac 100644 --- a/docs/guides/docs/quickstart/building-a-smart-contract.mdx +++ b/docs/guides/docs/quickstart/building-a-smart-contract.mdx @@ -144,7 +144,10 @@ action={{ /> ```console -$ tree counter-contract +tree counter-contract +``` + +```console counter-contract β”œβ”€β”€ Forc.toml └── src @@ -297,7 +300,10 @@ action={{ /> ```console -$ tree . +tree . +``` + +```console . β”œβ”€β”€ Forc.lock β”œβ”€β”€ Forc.toml @@ -365,7 +371,10 @@ action={{ /> ```console -$ tree . +tree . +``` + +```console . β”œβ”€β”€ Cargo.toml β”œβ”€β”€ Forc.lock @@ -423,7 +432,7 @@ action={{ diff --git a/docs/guides/docs/running-a-node/running-a-beta-4-node.mdx b/docs/guides/docs/running-a-node/running-a-beta-4-node.mdx index 8ea667a56..fe9aad5f3 100644 --- a/docs/guides/docs/running-a-node/running-a-beta-4-node.mdx +++ b/docs/guides/docs/running-a-node/running-a-beta-4-node.mdx @@ -97,7 +97,7 @@ Note that using other network endpoints will result in the relayer failing to st Generate a new P2P key pairing by running the following command: ```sh -$ fuel-core-keygen new --key-type peering +fuel-core-keygen new --key-type peering { "peer_id":"16Uiu2HAm8kCaJaaKTujrSwdBxyCELTY979KYaP9YBkWVLTGTo7Bf", "secret":"361b3275a3dd4150ea4c786b8dff7822205331e56ac2e73c32b17cb295978c8c", @@ -125,7 +125,7 @@ TODO Add `--enable-relayer` to list of arguments later */} ```sh -$ fuel-core run \ +fuel-core run \ --service-name {ANY_SERVICE_NAME} \ --keypair {P2P_SECRET} \ --relayer {RPC_ENDPOINT} \ diff --git a/docs/guides/examples/intro-to-sway b/docs/guides/examples/intro-to-sway new file mode 160000 index 000000000..620dbb43e --- /dev/null +++ b/docs/guides/examples/intro-to-sway @@ -0,0 +1 @@ +Subproject commit 620dbb43ea93a7c997ceefbde4d17e6f37d52e4d diff --git a/docs/guides/examples/quickstart/counter-contract/tests/harness.rs b/docs/guides/examples/quickstart/counter-contract/tests/harness.rs index dcb75ef89..8f3b86707 100644 --- a/docs/guides/examples/quickstart/counter-contract/tests/harness.rs +++ b/docs/guides/examples/quickstart/counter-contract/tests/harness.rs @@ -1,4 +1,4 @@ -// ANCHOR: contract-test-all +/* ANCHOR: contract-test-all */ use fuels::{prelude::*, types::ContractId}; // Load abi from json @@ -58,4 +58,4 @@ async fn test_increment() { assert_eq!(result.value, 1); } // ANCHOR_END: contract-test -// ANCHOR_END: contract-test-all +/* ANCHOR_END: contract-test-all */ diff --git a/docs/intro/glossary.mdx b/docs/intro/glossary.mdx index 28be9ba33..b9a34875e 100644 --- a/docs/intro/glossary.mdx +++ b/docs/intro/glossary.mdx @@ -212,7 +212,7 @@ A cryptographic key that is generated from its associated private key and can be A receipt is a data object that is emitted during a transaction and contains information about that transaction. -## Re-entrancy attack +## Reentrancy attack A type of attack in which the attacker is able to recursively call a contract function so that the function is exited before it is fully executed. This can result in the attacker being able to withdraw more funds than intended from a contract. diff --git a/docs/latest/fuel-specs b/docs/latest/fuel-specs index 6bdea6274..640390108 160000 --- a/docs/latest/fuel-specs +++ b/docs/latest/fuel-specs @@ -1 +1 @@ -Subproject commit 6bdea627421452eedaad1ca1efc5da8c773c1c14 +Subproject commit 640390108a0441906327cdf9750901233ba4f6bd diff --git a/docs/latest/fuels-wallet b/docs/latest/fuels-wallet index 536eae873..70fbc482f 160000 --- a/docs/latest/fuels-wallet +++ b/docs/latest/fuels-wallet @@ -1 +1 @@ -Subproject commit 536eae8733e573610abe59da377e80ed8a3d3b2a +Subproject commit 70fbc482f6b170a5d5ddbbc384b91b73fe45ec16 diff --git a/docs/latest/fuelup b/docs/latest/fuelup index 4eaa6f57a..00278ee2c 160000 --- a/docs/latest/fuelup +++ b/docs/latest/fuelup @@ -1 +1 @@ -Subproject commit 4eaa6f57a4b6af8af84f5c4f6917d79ed14126d3 +Subproject commit 00278ee2c128302934b5b7da028d20fcf5d0c69f diff --git a/playwright.config.ts b/playwright.config.ts index 8c5b4efb4..43c30a5b5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,7 +11,7 @@ import { defineConfig, devices } from '@playwright/test'; */ export default defineConfig({ testDir: './tests', - timeout: 60000 * 5, + timeout: 60000 * 10, expect: { timeout: 8000, }, @@ -20,7 +20,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: 1, + workers: 2, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09adeae93..b8da9d37b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -497,7 +497,7 @@ packages: ts-pattern: 4.3.0 unified: 10.1.2 yaml: 2.3.2 - zod: 3.22.3 + zod: 3.22.4 transitivePeerDependencies: - '@effect-ts/otel-node' - esbuild @@ -11124,8 +11124,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - /zod@3.22.3: - resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false /zustand@4.4.1(@types/react@18.2.21)(react@18.2.0): diff --git a/public/images/intro-to-sway.gif b/public/images/intro-to-sway.gif new file mode 100644 index 000000000..fea342c35 Binary files /dev/null and b/public/images/intro-to-sway.gif differ diff --git a/spell-check-custom-words.txt b/spell-check-custom-words.txt index e34b4d303..dcc81fc9f 100644 --- a/spell-check-custom-words.txt +++ b/spell-check-custom-words.txt @@ -1,62 +1,88 @@ -SDK -SDKs -testnet -testnets -mainnet -config -JSON +ABI abigen +ABIs +async blockchain blockchain's -ABI -ABIs +bytecode +CLI +codespace +codespaces +config +customizations +dapp +dapps +deployer +dev +enum +enums +EOA ERC +ETH Ethereum Ethereum's -natively -toolchain -toolchains -relayer -relayers +faucetLink +forc +frontend +fuelTestnet +fuelTestnetInlineCode +Fuelup +fuelup +FuelVM +fullstack +FVM +getter +Github +GQLPlaygroundLink +GraphQL +graphQL +hardcoded +incrementing Infura -RPC -Sepolia +JSON +LSP +mainnet +markdownlint +namespace +natively +nav +params +permissionless PoA PoS PoW -validator -validators -codespace -codespaces -Github quickstart -forc -bytecode -struct -Fuelup -fuelup -LSP -graphQL -GraphQL +reentrancy +relayer +relayers repo -customizations -VSCode -frontend -dapp -fullstack -TypeScript -params -VM -FuelVM -FVM -stateful +RPC runnable -CLI -dev +SDK +SDKs +SDK's +Sepolia +stateful +StorageMap +struct +Structs +testnet +testnets TODO +toolchain +toolchains +TypeScript +UI +validator +validators +VM +VSCode +webhooks +js +npm fuelTestnetInlineCode fuelTestnet faucetLink GQLPlaygroundLink xlarge -fuelCoreVersion \ No newline at end of file +fuelCoreVersion diff --git a/src/components/TestAction.tsx b/src/components/TestAction.tsx index 406e3fde7..76aec3052 100644 --- a/src/components/TestAction.tsx +++ b/src/components/TestAction.tsx @@ -50,6 +50,9 @@ export default function TestAction({ id, action }: TestActionProps) { data-element-name={ action.name === 'clickByRole' ? action.elementName : null } + data-testid={action.name === 'clickByTestId' ? action.testId : null} + data-selector={action.name === 'writeBySelector' ? action.selector : null} + data-value={action.name === 'writeBySelector' ? action.value : null} data-initial-index={ action.name === 'checkIfIsIncremented' ? action.initialIndex : null } diff --git a/src/lib/plugins/code-import.ts b/src/lib/plugins/code-import.ts index b838cbd7d..f3f431b69 100644 --- a/src/lib/plugins/code-import.ts +++ b/src/lib/plugins/code-import.ts @@ -13,6 +13,12 @@ import { FUEL_TESTNET } from '~/src/config/constants'; import { getEndCommentType } from './text-import'; import type { CommentTypes } from './text-import'; +interface Block { + content: string; + lineStart: number; + lineEnd: number; +} + function toAST(content: string) { return acorn.parse(content, { ecmaVersion: 'latest', @@ -47,62 +53,100 @@ function extractLines( } } +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + function extractCommentBlock( content: string, comment: string, commentType: CommentTypes, trim: string -) { +): Block { const lines = content.split(EOL); - let lineStart = 1; - let lineEnd = 1; + let lineStart = -1; + let lineEnd = -1; + const anchorStack: string[] = []; - const endCommentType = getEndCommentType(commentType); + const endCommentType = getEndCommentType(commentType) || ''; + + const startAnchorRegex = new RegExp( + `${escapeRegExp(commentType)}\\s*ANCHOR\\s*:\\s*${escapeRegExp( + comment + )}\\s*${escapeRegExp(endCommentType)}` + ); + const endAnchorRegex = new RegExp( + `${escapeRegExp(commentType)}\\s*ANCHOR_END\\s*:\\s*${escapeRegExp( + comment + )}\\s*${escapeRegExp(endCommentType)}` + ); for (let i = 0; i < lines.length; i++) { - const startLineA = `${commentType}ANCHOR:${comment}${endCommentType}`; - const endLineA = `${commentType}ANCHOR_END:${comment}${endCommentType}`; - const startLineB = `${commentType}${comment}:example:start${endCommentType}`; - const endLineB = `${commentType}${comment}:example:end${endCommentType}`; - const cleanLine = lines[i].replace(/\s+/g, ''); - const start = cleanLine === startLineA || cleanLine === startLineB; - if (start) { - lineStart = i + 1; - } else { - const end = cleanLine === endLineA || cleanLine === endLineB; - if (end) { + if (startAnchorRegex.test(lines[i])) { + if (lineStart === -1) { + lineStart = i; + } + anchorStack.push('anchor'); + } else if (endAnchorRegex.test(lines[i])) { + anchorStack.pop(); + if (anchorStack.length === 0 && lineEnd === -1) { lineEnd = i; + break; } } } - if (trim === 'true') { - const startShift = lines[lineStart + 1].includes('```') ? 2 : 1; - const endShift = lines[lineEnd - 2].includes('```') ? 2 : 1; - lineStart = lineStart + startShift; - lineEnd = lineEnd - endShift; - } - - if (lineStart < 0) { + // Check if lineStart and lineEnd were found, otherwise set to default + if (lineStart === -1) { lineStart = 0; } - if (lineEnd < 0) { - lineEnd = lines.length; + if (lineEnd === -1) { + lineEnd = lines.length - 1; } - const newLines = lines.slice(lineStart, lineEnd); + if (trim === 'true') { + // Adjust lineStart and lineEnd to exclude the anchor comments + // and the code block markers (```), if present. + lineStart = + lines.findIndex( + (line, index) => index > lineStart && line.includes('```') + ) + 1; + lineEnd = lines.findIndex( + (line, index) => index > lineStart && line.includes('```') + ); + lineEnd = lineEnd === -1 ? lines.length : lineEnd; + } + + let newLines = lines.slice(lineStart, lineEnd); + newLines = newLines.filter((line) => !line.includes('ANCHOR')); + + // Dedent the lines here: + const toDedent = minWhitespace(newLines); + if (toDedent > 0) { + newLines = dedent(newLines, toDedent); + } - const linesContent = newLines - .filter((line) => !line.includes('ANCHOR')) - .join('\n'); + const linesContent = newLines.join(EOL).replace(/\n{3,}/g, '\n\n'); return { - content: linesContent, + content: linesContent.trim(), lineStart, lineEnd, }; } +function minWhitespace(lines: string[]): number { + return lines + .filter((line) => line.trim() !== '') // ignore blank lines + .map((line) => line.match(/^(\s*)/)[0].length) + .reduce((min, curr) => Math.min(min, curr), Infinity); +} + +function dedent(lines: string[], amount: number): string[] { + const regex = new RegExp(`^\\s{${amount}}`); + return lines.map((line) => line.replace(regex, '')); +} + function getLineOffsets(str: string) { const regex = /\r?\n/g; const offsets = [0]; diff --git a/tests/test.spec.ts b/tests/test.spec.ts index 232a56147..869eac060 100644 --- a/tests/test.spec.ts +++ b/tests/test.spec.ts @@ -24,4 +24,39 @@ test.describe('Guides', () => { stopServers(); context.close(); }); + + test('intro to sway', async ({ context, extensionId, page }) => { + const PREREQUISITES_PAGE_URL = 'guides/intro-to-sway/prerequisites'; + const IMPORTS_PAGE_URL = 'guides/intro-to-sway/contract-imports'; + const STRUCTS_PAGE_URL = 'guides/intro-to-sway/contract-structs'; + const ABI_PAGE_URL = 'guides/intro-to-sway/contract-abi'; + const STORAGE_PAGE_URL = 'guides/intro-to-sway/contract-storage'; + const ERRORS_PAGE_URL = 'guides/intro-to-sway/contract-errors'; + const FUNCTIONS_PAGE_URL = 'guides/intro-to-sway/contract-functions'; + const CHECKPOINT_PAGE_URL = 'guides/intro-to-sway/checkpoint'; + const FUELS_RS_PAGE_URL = 'guides/intro-to-sway/rust-sdk'; + const FUELS_TS_PAGE_URL = 'guides/intro-to-sway/typescript-sdk'; + + // SETUP + stopServers(); + await useFuelWallet(context, extensionId, page); + await setupFolders('fuel-project'); + await startServers(page); + + // TEST CONTRACT + await runTest(page, context, PREREQUISITES_PAGE_URL); + await runTest(page, context, IMPORTS_PAGE_URL); + await runTest(page, context, STRUCTS_PAGE_URL); + await runTest(page, context, ABI_PAGE_URL); + await runTest(page, context, STORAGE_PAGE_URL); + await runTest(page, context, ERRORS_PAGE_URL); + await runTest(page, context, FUNCTIONS_PAGE_URL); + await runTest(page, context, CHECKPOINT_PAGE_URL); + await runTest(page, context, FUELS_RS_PAGE_URL); + await runTest(page, context, FUELS_TS_PAGE_URL); + + // SHUT DOWN + stopServers(); + context.close(); + }); }); diff --git a/tests/utils/checks.ts b/tests/utils/checks.ts index 4cc560cc7..53dc4d305 100644 --- a/tests/utils/checks.ts +++ b/tests/utils/checks.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { expect } from './fixtures'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const saved: any[] = []; +let saved: any[] = []; export function checkIfIsIncremented(initialIndex: number, finalIndex: number) { console.log('INITIAL Index:', initialIndex); @@ -15,6 +15,7 @@ export function checkIfIsIncremented(initialIndex: number, finalIndex: number) { console.log('INITIAL:', initial); console.log('FINAL:', final); const isIncremented = final === initial + 1; + saved = []; expect(isIncremented).toBeTruthy(); } diff --git a/tests/utils/runTest.ts b/tests/utils/runTest.ts index 9ee940422..403e9fbde 100644 --- a/tests/utils/runTest.ts +++ b/tests/utils/runTest.ts @@ -81,6 +81,12 @@ export async function runTest( .getByRole(step['data-role'], { name: step['data-element-name'] }) .click(); break; + case 'clickByTestId': + await page.getByTestId(step['data-testid']).click(); + break; + case 'writeBySelector': + await page.fill(step['data-selector'], 'hello world'); + break; case 'walletApproveConnect': await walletConnect(context); break; diff --git a/tests/utils/types.ts b/tests/utils/types.ts index cbbf67fe0..7015f978b 100644 --- a/tests/utils/types.ts +++ b/tests/utils/types.ts @@ -9,6 +9,8 @@ export type Action = | Reload | GetByLocatorAndSave | ClickByRole + | ClickByTestId + | WriteBySelector | WalletApprove | WalletApproveConnect | CheckIfIsIncremented; @@ -91,6 +93,17 @@ export type ClickByRole = { elementName: string; }; +export type ClickByTestId = { + name: 'clickByTestId'; + testId: string; +}; + +export type WriteBySelector = { + name: 'writeBySelector'; + selector: string; + value: string; +}; + // approves a pending transaction export type WalletApprove = { name: 'walletApprove' };