diff --git a/README.md b/README.md index 6b922f290..7af6d09fe 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -# ๐Ÿšฉ Challenge {challengeNum}: {challengeEmoji} {challengeTitle} +# ๐Ÿšฉ Challenge #2: ๐Ÿต Token Vendor ๐Ÿค– -{challengeHeroImage} +![readme-2](./packages/nextjs/public/hero2.png) -A {challengeDescription}. +๐Ÿค– Smart contracts are kind of like "always on" _vending machines_ that **anyone** can access. Let's make a decentralized, digital currency. Then, let's build an unstoppable vending machine that will buy and sell the currency. We'll learn about the "approve" pattern for ERC20s and how contract to contract interactions work. -๐ŸŒŸ The final deliverable is an app that {challengeDeliverable}. -Deploy your contracts to a testnet then build and upload your app to a public web server. Submit the url on [SpeedRunStark.com](https://speedrunstark.com/)! +๐Ÿต Create `YourToken.cairo` smart contract that inherits the **ERC20** token standard from OpenZeppelin. Set `your token` to `_mint()` **1000** \* (10^18) tokens to the `recipient` account address. Then create a `Vendor.cairo` contract that sells `your token` using a `buy_tokens()` function. -๐Ÿ’ฌ Meet other builders working on this challenge and get help in the {challengeTelegramLink} +๐ŸŽ› Edit the frontend that invites the user to input an amount of tokens they want to buy. We'll display a preview of the amount of ETH it will cost with a confirm button. + +๐ŸŒŸ The final deliverable is an app that lets users purchase your ERC20 token, transfer it, and sell it back to the vendor. Deploy your contracts on your public chain of choice and then `yarn vercel` your app to a public web server. + +> ๐Ÿ’ฌ Meet other builders working on this challenge or get help in the [Builders telegram chat](https://t.me/+wO3PtlRAreo4MDI9)! --- @@ -30,44 +33,248 @@ Make sure you have the compatible versions otherwise refer to [Scaffold-Stark Re Then download the challenge to your computer and install dependencies by running: ```sh -git clone https://github.com/Quantum3-Labs/speedrunstark.git {challengeName} -cd {challengeName} -git checkout {challengeName} +git clone https://github.com/Quantum3-Labs/speedrunstark.git token-vendor +cd token-vendor +git checkout token-vendor yarn install ``` -> in the same terminal, start your local network (a local instance of a blockchain): +> in the same terminal, start your local network (a blockchain emulator in your computer): -```sh +```bash yarn chain ``` > in a second terminal window, ๐Ÿ›ฐ deploy your contract (locally): ```sh -cd +cd token-vendor yarn deploy ``` > in a third terminal window, start your ๐Ÿ“ฑ frontend: ```sh -cd +cd token-vendor yarn start ``` ๐Ÿ“ฑ Open to see the app. -> ๐Ÿ‘ฉโ€๐Ÿ’ป Rerun `yarn deploy --reset` whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address. +> ๐Ÿ‘ฉโ€๐Ÿ’ป Rerun `yarn deploy` whenever you want to deploy new contracts to the frontend. +--- + +## Checkpoint 1: ๐ŸตYour Token ๐Ÿ’ต + +> ๐Ÿ‘ฉโ€๐Ÿ’ป Edit `YourToken.cairo` to reuse the **ERC20** token standard from OpenZeppelin. To accomplish this, you can use [`Cairo Components`](https://book.cairo-lang.org/ch16-02-00-composability-and-components.html) to embed the `ERC20` logic inside your contract. + +> Mint **2000** (\* 10 \*\* 18) to your frontend address using the `constructor()`. In devnet, by default we choose the first pre-deployed account: `0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691`, to deploy the contracts. In order to complete this checkpoint, you need to connect to devnet using the same address. In testnet, you can use your own address to deploy the contracts. Edit the .env file in the `snfoundry` package to set the `ACCOUNT_ADDRESS_SEPOLIA` to your own address. + +(Your frontend address is the address in the top right of ) + +> You can `yarn deploy` to deploy your contract until you get it right. + +### ๐Ÿฅ… Goals + +- [ ] Can you check the `balance_of()` your frontend address in the `Debug Contracts` tab? (**YourToken** contract) +- [ ] Can you `transfer()` your token to another account and check _that_ account's `balance_of`? + +![debugContractsYourToken](./packages/nextjs/public/ch2-YourToken.png) + +> ๐Ÿ’ฌ Hint: In Devnet, use the `switch account` feature to select a different pre-deployed account address and try sending to that new address. Can use the `transfer()` function in the `Debug Contracts` tab. + +--- + +## Checkpoint 2: โš–๏ธ Vendor ๐Ÿค– + +> ๐Ÿ‘ฉโ€๐Ÿ’ป Edit the `Vendor.cairo` contract with a `buy_tokens()` function + +Create a price variable named `tokensPerEth` set to **100**: + +```cairo +const TokensPerEth: u256 = 100; +``` + +> ๐Ÿ“ The `buy_tokens()` function in `Vendor.cairo` should use `eth_amount_wei` value and `tokensPerEth` to calculate an amount of tokens to `transfer`(self.your_token.read().transfer()) to `recipient`. + +> ๐Ÿ“Ÿ Emit **event** `BuyTokens {buyer: ContractAddress, eth_amount: u256, tokens_amount: u256}` when tokens are purchased. + +Edit `packages/snfoundry/scripts-ts/deploy.ts` to deploy the `Vendor` (uncomment Vendor deploy lines). + +Implement `tokens_per_eth` function in `Vendor.cairo` that returns the `tokensPerEth` value. + +Uncomment the `Buy Tokens` sections in `packages/nextjs/app/token-vendor/page.tsx` to show the UI to buy tokens on the Token Vendor tab. + +### ๐Ÿฅ… Goals + +- [ ] When you try to buy tokens from the vendor, you should get an error: **'u256_sub Overflow'**. This error is related to the `transfer_from`, `transfer`, approve function in the `YourToken` contract. + +โš ๏ธ You might face this error because the Vendor contract doesn't have any `YourToken` yet!. You can create an `assert` in the `buy_tokens()` function to check if the Vendor has enough tokens to sell. + +โš”๏ธ Side Quest: send tokens from your frontend address to the Vendor contract address and _then_ try to buy them. + +> โœ๏ธ We can't hard code the vendor address like we did above when deploying to the network because we won't know the vendor address at the time we create the token contract. + +> โœ๏ธ Then, edit `packages/snfoundry/scripts-ts/deploy.ts` to transfer 1000 tokens to vendor address. + +```ts + await deployer_v6.execute( + [ + { + contractAddress: your_token.address, + calldata: [ + vendor.address, + { + low: 1_000_000_000_000_000_000_000n, //1000 * 10^18 + high: 0, + } + ], + entrypoint: "transfer", + } + ], + { + maxFee: 1e18 + } + ); +``` + +> ๐Ÿ”Ž Look in `packages/nextjs/app/token-vendor/page.tsx` for code to uncomment to display the Vendor ETH and Token balances. + +> You can `yarn deploy` to deploy your contract until you get it right. + +![TokenVendorBuy](./packages/nextjs/public/ch2-TokenVendorBalance.png) + +### ๐Ÿฅ… Goals + +- [ ] Does the `Vendor` address start with a `balanceOf` **1000** in `YourToken` on the `Debug Contracts` tab? +- [ ] Can you buy **10** tokens for **0.1** ETH? +- [ ] Can you transfer tokens to a different account? + +> ๐Ÿ“ Edit `Vendor.cairo` to reuse _Ownable_ component from OpenZeppelin. + +```cairo + #[storage] + struct Storage { + ... + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } +``` + +In `Vendor.cairo` you will need to add one more input parameter to setup the `owner` in the `constructor()`. + +> โœ๏ธ Then, edit `packages/snfoundry/scripts-ts/deploy.ts` to deploy the `Vendor` contract with the `owner` address. + +```ts + const vendor = await deployContract( + { + token_address: your_token.address, + owner: deployer.address, + }, + "Vendor" + ); +``` + +### ๐Ÿฅ… Goals -๐Ÿ” Now you are ready to edit your smart contract `{YourCollectible.cairo}` in `packages/snfoundry/contracts` +- [ ] Is your frontend address the `owner` of the `Vendor`? + +> ๐Ÿ“ Finally, add a `withdraw()` function in `Vendor.cairo` that lets the owner withdraw all the ETH from the vendor contract. + +### ๐Ÿฅ… Goals + +- [ ] Can **only** the `owner` withdraw the ETH from the `Vendor`? + +### โš”๏ธ Side Quests + +- [ ] What if you minted **2000** and only sent **1000** to the `Vendor`? --- +## Checkpoint 3: ๐Ÿค” Vendor Buyback ๐Ÿคฏ + +๐Ÿ‘ฉโ€๐Ÿซ The hardest part of this challenge is to build your `Vendor` to buy the tokens back. + +๐Ÿง The reason why this is hard is the `approve()` pattern in ERC20s. + +๐Ÿ˜• First, the user has to call `approve()` on the `YourToken` contract, approving the `Vendor` contract address to take some amount of tokens. + +๐Ÿคจ Then, the user makes a _second transaction_ to the `Vendor` contract to `sellTokens(amount_tokens: u256)`. + +๐Ÿค“ The `Vendor` should call `fn transfer_from(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool` and if the user has approved the `Vendor` correctly, tokens should transfer to the `Vendor` and ETH should be sent to the user. + +๐Ÿคฉ You can use `useScaffoldMultiWriteContract.ts` to call `approve` and `buy / sell tokens` + +> ๐Ÿ“ Edit `Vendor.cairo` and add a `sellTokens(amount_tokens: u256)` function! + +๐Ÿ”จ Use the `Debug Contracts` tab to call the approve and sellTokens() at first but then... + +๐Ÿ” Look in the `packages/nextjs/app/token-vendor/page.tsx` for the extra approve/sell UI to uncomment! + +![VendorBuyBack](./packages/nextjs/public/ch2-VendorBuySell.png) + +### ๐Ÿฅ… Goal + +- [ ] Can you sell tokens back to the vendor? +- [ ] Do you receive the right amount of ETH for the tokens? + ### โš”๏ธ Side Quests -_To finish your README, can add these links_ +- [ ] Should we disable the `owner` withdraw to keep liquidity in the `Vendor`? +- [ ] It would be a good idea to display Sell Token Events. Create an **event** + `SellTokens {seller: ContractAddress, tokens_amount: u256, eth_amount: u256}` + and `emit` it in your `Vendor.cairo` and uncomment `SellTokens Events` section in your `packages/nextjs/app/events/page.tsx` to update your frontend. + + ![Events](./packages/nextjs/public/ch2-Events.png) + +### โš ๏ธ Test it + +- Now is a good time to run `yarn test` to run the automated testing function. It will test that you hit the core checkpoints. You are looking for all green checkmarks and passing tests! + +--- + +## Checkpoint 4: ๐Ÿ’พ Deploy your contracts! ๐Ÿ›ฐ + +๐Ÿ“ก Edit the `defaultNetwork` to your choice of Starknet networks in `packages/nextjs/scaffold.config.ts` + +๐Ÿ” In devnet you can choose a burner wallet auto-generated + +โ›ฝ๏ธ You will need to send ETH to your deployer address with your wallet if not in devnet, or get it from a public faucet of your chosen network. + +๐Ÿš€ Run `yarn deploy` to deploy your smart contract to a public network (selected in `scaffold.config.ts`) + +> ๐Ÿ’ฌ Hint: For faster loading of your _"Events"_ page, consider updating the `fromBlock` passed to `useScaffoldEventHistory` in [`packages/nextjs/app/events/page.tsx`](https://github.com/Quantum3-Labs/speedrunstark/blob/token-vendor/packages/nextjs/app/events/page.tsx) to `blocknumber - 10` at which your contract was deployed. Example: `fromBlock: 3750241n` (where `n` represents its a [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)). To find this blocknumber, search your contract's address on Starkscan and find the `Contract Creation` transaction line. + +--- + +## Checkpoint 5: ๐Ÿšข Ship your frontend! ๐Ÿš + +โœ๏ธ Edit your frontend config in `packages/nextjs/scaffold.config.ts` to change the `targetNetwork` to `chains.sepolia`. + +๐Ÿ’ป View your frontend at and verify you see the correct network. -> ๐Ÿƒ Head to your next challenge [here](https://speedrunstark.com/). +๐Ÿ“ก When you are ready to ship the frontend app... + +๐Ÿ“ฆ Run `yarn vercel` to package up your frontend and deploy. + +> Follow the steps to deploy to Vercel. Once you log in (email, github, etc), the default options should work. It'll give you a public URL. + +> If you want to redeploy to the same production URL you can run `yarn vercel --prod`. If you omit the `--prod` flag it will deploy it to a preview/test URL. + +> ๐ŸฆŠ Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default, ๐Ÿ”ฅ `burner wallets` are only available on `devnet` . You can enable them on every chain by setting `onlyLocalBurnerWallet: false` in your frontend config (`scaffold.config.ts` in `packages/nextjs/`) + +#### Configuration of Third-Party Services for Production-Grade Apps + +By default, ๐Ÿ— Scaffold-Stark provides predefined API keys for some services such as Infura. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services. +This is great to complete your **SpeedRunStark**. + +For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at: + +๐Ÿ”ท `RPC_URL_SEPOLIA` variable in `packages/snfoundry/.env` and `packages/nextjs/.env.local`. You can create API keys from the [Infura dashboard](https://www.infura.io/). + +> ๐Ÿ’ฌ Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing. + +--- -> ๐Ÿ’ฌ Problems, questions, comments on the stack? Post them to the [๐Ÿ— Scaffold-Stark developers chat](https://t.me/+wO3PtlRAreo4MDI9) +> ๐Ÿƒ Head to your next challenge [here](https://github.com/Quantum3-Labs/speedrunstark/tree/dice-game). +> ๏ฟฝ Problems, questions, comments on the stack? Post them to the [๐Ÿ— Scaffold-Stark developers chat](https://t.me/+wO3PtlRAreo4MDI9) \ No newline at end of file diff --git a/packages/nextjs/app/events/page.tsx b/packages/nextjs/app/events/page.tsx new file mode 100644 index 000000000..a32a021f3 --- /dev/null +++ b/packages/nextjs/app/events/page.tsx @@ -0,0 +1,120 @@ +"use client"; + +import type { NextPage } from "next"; +import { Address } from "~~/components/scaffold-stark/Address"; +import { useScaffoldEventHistory } from "~~/hooks/scaffold-stark/useScaffoldEventHistory"; +import { formatEther } from "ethers"; + +const Events: NextPage = () => { + const { data: buyTokenEvents, isLoading: isBuyEventsLoading } = + useScaffoldEventHistory({ + contractName: "Vendor", + eventName: "contracts::Vendor::Vendor::BuyTokens", + fromBlock: 0n, + }); + + const { data: sellTokenEvents, isLoading: isSellEventsLoading } = + useScaffoldEventHistory({ + contractName: "Vendor", + eventName: "contracts::Vendor::Vendor::SellTokens", + fromBlock: 0n, + }); + + return ( +
+
+
+ Buy Token Events +
+ {isBuyEventsLoading ? ( +
+ +
+ ) : ( +
+ + + + + + + + + + {!buyTokenEvents || buyTokenEvents.length === 0 ? ( + + + + ) : ( + buyTokenEvents?.map((event, index) => { + return ( + + + + + + ); + }) + )} + +
BuyerAmount of ETHAmount of Tokens
+ No events found +
+
+
{formatEther(event.args.eth_amount).toString()} + {formatEther(event.args.tokens_amount).toString()} +
+
+ )} +
+ +
+
+ Sell Token Events +
+ {isSellEventsLoading ? ( +
+ +
+ ) : ( +
+ + + + + + + + + + {!sellTokenEvents || sellTokenEvents.length === 0 ? ( + + + + ) : ( + sellTokenEvents?.map((event, index) => { + return ( + + + + + + ); + }) + )} + +
SellerAmount of ETHAmount of Tokens
+ No events found +
+
+
{formatEther(event.args.eth_amount).toString()} + {formatEther(event.args.tokens_amount).toString()} +
+
+ )} +
+
+ ); +}; + +export default Events; diff --git a/packages/nextjs/app/exampleView1/page.tsx b/packages/nextjs/app/exampleView1/page.tsx deleted file mode 100644 index 349cd12ae..000000000 --- a/packages/nextjs/app/exampleView1/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client"; - -import type { NextPage } from "next"; - -const ExampleView1: NextPage = () => { - return ( - <> -
-
-
- - ); -}; - -export default ExampleView1; diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 14a6f002a..13ec685f6 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -1,15 +1,56 @@ "use client"; import type { NextPage } from "next"; +import Image from "next/image"; import { useAccount } from "@starknet-react/core"; const Home: NextPage = () => { const connectedAddress = useAccount(); - return ( <>
-
+
+

+ SpeedRunStark + + Challenge #2: ๐Ÿต Token Vendor ๐Ÿค– + +

+
+ challenge banner +
+

+ ๐Ÿค– Smart contracts are kind of like "always on" + vending machines that anyone can access. Let's make a + decentralized, digital currency. Then, let's build an + unstoppable vending machine that will buy and sell the currency. + We'll learn about the "approve" pattern for + ERC20s and how contract to contract interactions work. +

+

+ ๐ŸŒŸ The final deliverable is an app that lets users purchase your + ERC20 token, transfer it, and sell it back to the vendor. Deploy + your contracts on your public chain of choice and then deploy + your app to a public webserver. Submit the url on{" "} + + SpeedrunStark.com + {" "} + ! +

+
+
+
); diff --git a/packages/nextjs/app/token-vendor/page.tsx b/packages/nextjs/app/token-vendor/page.tsx new file mode 100644 index 000000000..3a41a3274 --- /dev/null +++ b/packages/nextjs/app/token-vendor/page.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useState } from "react"; +import type { NextPage } from "next"; +import { useAccount } from "@starknet-react/core"; +import { AddressInput } from "~~/components/scaffold-stark/Input/AddressInput"; +import { IntegerInput } from "~~/components/scaffold-stark/Input/IntegerInput"; +import { useScaffoldReadContract } from "~~/hooks/scaffold-stark/useScaffoldReadContract"; +import { useScaffoldWriteContract } from "~~/hooks/scaffold-stark/useScaffoldWriteContract"; +import { useScaffoldMultiWriteContract } from "~~/hooks/scaffold-stark/useScaffoldMultiWriteContract"; +import { useDeployedContractInfo } from "~~/hooks/scaffold-stark"; +//import { useBalance } from "@starknet-react/core"; +import { formatEther } from "ethers"; +import { + getTokenPrice, + multiplyTo1e18, +} from "~~/utils/scaffold-stark/priceInWei"; +//import { BlockNumber,byteArray } from "starknet"; +import useScaffoldEthBalance from "~~/hooks/scaffold-stark/useScaffoldEthBalance"; + +const TokenVendor: NextPage = () => { + const [toAddress, setToAddress] = useState(""); + const [tokensToSend, setTokensToSend] = useState(""); + const [tokensToBuy, setTokensToBuy] = useState(""); + const [isApproved, setIsApproved] = useState(false); + const [tokensToSell, setTokensToSell] = useState(""); + + const { address: connectedAddress } = useAccount(); + const { data: yourTokenSymbol } = useScaffoldReadContract({ + contractName: "YourToken", + functionName: "symbol", + }); + + const { data: yourTokenBalance } = useScaffoldReadContract({ + contractName: "YourToken", + functionName: "balance_of", + args: [connectedAddress ?? ""], + }); + + // // Vendor Balances + const { data: vendorContractData } = useDeployedContractInfo("Vendor"); + + const { data: vendorTokenBalance } = useScaffoldReadContract({ + contractName: "YourToken", + functionName: "balance_of", + args: [vendorContractData?.address ?? ""], + }); + + const { data: tokensPerEth } = useScaffoldReadContract({ + contractName: "Vendor", + functionName: "tokens_per_eth", + }); + + const { writeAsync: transferTokens } = useScaffoldWriteContract({ + contractName: "YourToken", + functionName: "transfer", + args: [toAddress, multiplyTo1e18(tokensToSend)], + }); + + const { value: vendorContractBalance } = useScaffoldEthBalance({ + address: vendorContractData?.address, + // watch: true, + // blockIdentifier: "pending" as BlockNumber, + }); + + console.log("vendorContractBalance", vendorContractBalance); + + const eth_to_spent = getTokenPrice( + tokensToBuy, + tokensPerEth as unknown as bigint, + ); + + const { writeAsync: buy } = useScaffoldMultiWriteContract({ + calls: [ + { + contractName: "Eth", + functionName: "approve", + args: [vendorContractData?.address ?? "", eth_to_spent], + }, + { + contractName: "Vendor", + functionName: "buy_tokens", + args: [eth_to_spent], + }, + ], + }); + + const { writeAsync: sell } = useScaffoldMultiWriteContract({ + calls: [ + { + contractName: "YourToken", + functionName: "approve", + args: [vendorContractData?.address ?? "", multiplyTo1e18(tokensToSell)], + }, + { + contractName: "Vendor", + functionName: "sell_tokens", + args: [multiplyTo1e18(tokensToSell)], + }, + ], + }); + + // FixMe: Read symbol from contract + const ethSymbol = "ETH"; + + const wrapInTryCatch = + (fn: () => Promise, errorMessageFnDescription: string) => async () => { + try { + await fn(); + } catch (error) { + console.error( + `Error calling ${errorMessageFnDescription} function`, + error, + ); + } + }; + + // FixMe: This is a hack to get the symbol of the token. Propose a better way to do this. + const parsedSymbol = yourTokenSymbol as unknown as string; + + return ( + <> +
+
+
+ Your token balance:{" "} +
+ + {parseFloat(formatEther(yourTokenBalance?.toString() || 0n))} + + {parsedSymbol} +
+
+ {/* Vendor Balances */} + {/*
+
+ Vendor token balance:{" "} +
+ {parseFloat( + formatEther(vendorTokenBalance?.toString() || 0n), + ).toFixed(4)} + + {parsedSymbol} + +
+
+
+ Vendor eth balance: + {parseFloat(formatEther(vendorContractBalance?.toString() || 0n))} + + {ethSymbol} + +
*/} +
+ + {/* Buy Tokens */} + {/*
+
Buy tokens
+
{Number(tokensPerEth)} tokens per ETH
+
+ setTokensToBuy(value)} + disableMultiplyBy1e18 + /> +
+ +
*/} + + {!!yourTokenBalance && ( +
+
Transfer tokens
+
+ setToAddress(value)} + /> + setTokensToSend(value as string)} + disableMultiplyBy1e18 + /> +
+ +
+ )} + + {/* Sell Tokens */} + {/* {!!yourTokenBalance && ( +
+
Sell tokens
+
{Number(tokensPerEth)} tokens per ETH
+
+ setTokensToSell(value as string)} + disabled={isApproved} + disableMultiplyBy1e18 + /> +
+ +
+ +
+
+ )}*/} +
+ + ); +}; + +export default TokenVendor; diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 4683518f4..69d0e9c01 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -4,7 +4,13 @@ import React, { useCallback, useRef, useState, useEffect } from "react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Bars3Icon, BugAntIcon, PhotoIcon } from "@heroicons/react/24/outline"; +import { + Bars3Icon, + BoltIcon, + BugAntIcon, + CircleStackIcon, + PhotoIcon, +} from "@heroicons/react/24/outline"; import { useOutsideClick } from "~~/hooks/scaffold-stark"; import { CustomConnectButton } from "~~/components/scaffold-stark/CustomConnectButton"; import { useTheme } from "next-themes"; @@ -21,9 +27,18 @@ type HeaderMenuLink = { export const menuLinks: HeaderMenuLink[] = [ { - label: "Example View 1", - href: "/exampleView1", - icon: , + label: "Home", + href: "/", + }, + { + label: "Token Vendor", + href: "/token-vendor", + icon: , + }, + { + label: "Events", + href: "/events", + icon: , }, { label: "Debug Contracts", diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 25751cee0..a37889ae4 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -3,6 +3,586 @@ * You should not edit it manually or your changes might be overwritten. */ -const deployedContracts = {} as const; +const deployedContracts = { + devnet: { + YourToken: { + address: + "0xbf2e7617923f4c791a3bf45dc1c2873d2fdefae0bcdc0ff86402268e79b170", + abi: [ + { + type: "impl", + name: "IYourTokenImpl", + interface_name: "openzeppelin::token::erc20::interface::IERC20", + }, + { + type: "struct", + name: "core::integer::u256", + members: [ + { + name: "low", + type: "core::integer::u128", + }, + { + name: "high", + type: "core::integer::u128", + }, + ], + }, + { + type: "enum", + name: "core::bool", + variants: [ + { + name: "False", + type: "()", + }, + { + name: "True", + type: "()", + }, + ], + }, + { + type: "interface", + name: "openzeppelin::token::erc20::interface::IERC20", + items: [ + { + type: "function", + name: "total_supply", + inputs: [], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "balance_of", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "allowance", + inputs: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "transfer", + inputs: [ + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + { + type: "function", + name: "transfer_from", + inputs: [ + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + { + type: "function", + name: "approve", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "amount", + type: "core::integer::u256", + }, + ], + outputs: [ + { + type: "core::bool", + }, + ], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "ERC20MetadataImpl", + interface_name: + "openzeppelin::token::erc20::interface::IERC20Metadata", + }, + { + type: "struct", + name: "core::byte_array::ByteArray", + members: [ + { + name: "data", + type: "core::array::Array::", + }, + { + name: "pending_word", + type: "core::felt252", + }, + { + name: "pending_word_len", + type: "core::integer::u32", + }, + ], + }, + { + type: "interface", + name: "openzeppelin::token::erc20::interface::IERC20Metadata", + items: [ + { + type: "function", + name: "name", + inputs: [], + outputs: [ + { + type: "core::byte_array::ByteArray", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "symbol", + inputs: [], + outputs: [ + { + type: "core::byte_array::ByteArray", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "decimals", + inputs: [], + outputs: [ + { + type: "core::integer::u8", + }, + ], + state_mutability: "view", + }, + ], + }, + { + type: "constructor", + name: "constructor", + inputs: [ + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + }, + { + type: "event", + name: "openzeppelin::token::erc20::erc20::ERC20Component::Transfer", + kind: "struct", + members: [ + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "value", + type: "core::integer::u256", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin::token::erc20::erc20::ERC20Component::Approval", + kind: "struct", + members: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "value", + type: "core::integer::u256", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin::token::erc20::erc20::ERC20Component::Event", + kind: "enum", + variants: [ + { + name: "Transfer", + type: "openzeppelin::token::erc20::erc20::ERC20Component::Transfer", + kind: "nested", + }, + { + name: "Approval", + type: "openzeppelin::token::erc20::erc20::ERC20Component::Approval", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "contracts::YourToken::YourToken::Event", + kind: "enum", + variants: [ + { + name: "ERC20Event", + type: "openzeppelin::token::erc20::erc20::ERC20Component::Event", + kind: "flat", + }, + ], + }, + ], + classHash: + "0x940ba155d095e24cc410bf634d1702adddd3ec84ebc4b059b413145e5132cd", + }, + Vendor: { + address: + "0x462ef55b1b8b860916060a6f8b3664a9a413459ae87b3ef2e48ab55d5a0d134", + abi: [ + { + type: "impl", + name: "VendorImpl", + interface_name: "contracts::Vendor::IVendor", + }, + { + type: "struct", + name: "core::integer::u256", + members: [ + { + name: "low", + type: "core::integer::u128", + }, + { + name: "high", + type: "core::integer::u128", + }, + ], + }, + { + type: "interface", + name: "contracts::Vendor::IVendor", + items: [ + { + type: "function", + name: "buy_tokens", + inputs: [ + { + name: "eth_amount_wei", + type: "core::integer::u256", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "withdraw", + inputs: [], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "sell_tokens", + inputs: [ + { + name: "amount_tokens", + type: "core::integer::u256", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "tokens_per_eth", + inputs: [], + outputs: [ + { + type: "core::integer::u256", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "your_token", + inputs: [], + outputs: [ + { + type: "core::starknet::contract_address::ContractAddress", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "eth_token", + inputs: [], + outputs: [ + { + type: "core::starknet::contract_address::ContractAddress", + }, + ], + state_mutability: "view", + }, + ], + }, + { + type: "impl", + name: "OwnableImpl", + interface_name: "openzeppelin::access::ownable::interface::IOwnable", + }, + { + type: "interface", + name: "openzeppelin::access::ownable::interface::IOwnable", + items: [ + { + type: "function", + name: "owner", + inputs: [], + outputs: [ + { + type: "core::starknet::contract_address::ContractAddress", + }, + ], + state_mutability: "view", + }, + { + type: "function", + name: "transfer_ownership", + inputs: [ + { + name: "new_owner", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "renounce_ownership", + inputs: [], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "constructor", + name: "constructor", + inputs: [ + { + name: "eth_token_address", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "your_token_address", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + }, + { + type: "event", + name: "openzeppelin::access::ownable::ownable::OwnableComponent::OwnershipTransferred", + kind: "struct", + members: [ + { + name: "previous_owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "new_owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + ], + }, + { + type: "event", + name: "openzeppelin::access::ownable::ownable::OwnableComponent::OwnershipTransferStarted", + kind: "struct", + members: [ + { + name: "previous_owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "new_owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + ], + }, + { + type: "event", + name: "openzeppelin::access::ownable::ownable::OwnableComponent::Event", + kind: "enum", + variants: [ + { + name: "OwnershipTransferred", + type: "openzeppelin::access::ownable::ownable::OwnableComponent::OwnershipTransferred", + kind: "nested", + }, + { + name: "OwnershipTransferStarted", + type: "openzeppelin::access::ownable::ownable::OwnableComponent::OwnershipTransferStarted", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "contracts::Vendor::Vendor::BuyTokens", + kind: "struct", + members: [ + { + name: "buyer", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "eth_amount", + type: "core::integer::u256", + kind: "data", + }, + { + name: "tokens_amount", + type: "core::integer::u256", + kind: "data", + }, + ], + }, + { + type: "event", + name: "contracts::Vendor::Vendor::SellTokens", + kind: "struct", + members: [ + { + name: "seller", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "tokens_amount", + type: "core::integer::u256", + kind: "data", + }, + { + name: "eth_amount", + type: "core::integer::u256", + kind: "data", + }, + ], + }, + { + type: "event", + name: "contracts::Vendor::Vendor::Event", + kind: "enum", + variants: [ + { + name: "OwnableEvent", + type: "openzeppelin::access::ownable::ownable::OwnableComponent::Event", + kind: "flat", + }, + { + name: "BuyTokens", + type: "contracts::Vendor::Vendor::BuyTokens", + kind: "nested", + }, + { + name: "SellTokens", + type: "contracts::Vendor::Vendor::SellTokens", + kind: "nested", + }, + ], + }, + ], + classHash: + "0x3b85ef9ebc063c265893a17e02769e894787342cc83d530de0f6becb8cff2c", + }, + }, +} as const; export default deployedContracts; diff --git a/packages/nextjs/public/ch2-Events.png b/packages/nextjs/public/ch2-Events.png new file mode 100644 index 000000000..31526c95d Binary files /dev/null and b/packages/nextjs/public/ch2-Events.png differ diff --git a/packages/nextjs/public/ch2-TokenVendorBalance.png b/packages/nextjs/public/ch2-TokenVendorBalance.png new file mode 100644 index 000000000..2e2409d2c Binary files /dev/null and b/packages/nextjs/public/ch2-TokenVendorBalance.png differ diff --git a/packages/nextjs/public/ch2-VendorBuySell.png b/packages/nextjs/public/ch2-VendorBuySell.png new file mode 100644 index 000000000..e3b157ce4 Binary files /dev/null and b/packages/nextjs/public/ch2-VendorBuySell.png differ diff --git a/packages/nextjs/public/ch2-YourToken.png b/packages/nextjs/public/ch2-YourToken.png new file mode 100644 index 000000000..6b797dff2 Binary files /dev/null and b/packages/nextjs/public/ch2-YourToken.png differ diff --git a/packages/nextjs/public/hero2.png b/packages/nextjs/public/hero2.png new file mode 100644 index 000000000..754761154 Binary files /dev/null and b/packages/nextjs/public/hero2.png differ diff --git a/packages/nextjs/utils/scaffold-stark/priceInWei.ts b/packages/nextjs/utils/scaffold-stark/priceInWei.ts new file mode 100644 index 000000000..c0e9b9c7a --- /dev/null +++ b/packages/nextjs/utils/scaffold-stark/priceInWei.ts @@ -0,0 +1,16 @@ +import { formatEther, parseEther } from "ethers"; + +export function multiplyTo1e18(tokens: string | bigint) { + try { + return parseEther(tokens.toString()); + } catch (err) { + // wrong tokens value + return 0n; + } +} + +export function getTokenPrice(tokens: string | bigint, tokensPerEth?: bigint) { + const tokensMultiplied = multiplyTo1e18(tokens); + + return tokensPerEth ? tokensMultiplied / tokensPerEth : tokensMultiplied; +} diff --git a/packages/snfoundry/contracts/src/Vendor.cairo b/packages/snfoundry/contracts/src/Vendor.cairo new file mode 100644 index 000000000..275dc7235 --- /dev/null +++ b/packages/snfoundry/contracts/src/Vendor.cairo @@ -0,0 +1,101 @@ +use starknet::ContractAddress; +#[starknet::interface] +pub trait IVendor { + fn buy_tokens(ref self: T, eth_amount_wei: u256); + fn withdraw(ref self: T); + fn sell_tokens(ref self: T, amount_tokens: u256); + fn tokens_per_eth(self: @T) -> u256; + fn your_token(self: @T) -> ContractAddress; + fn eth_token(self: @T) -> ContractAddress; +} + +#[starknet::contract] +mod Vendor { + use contracts::YourToken::{IYourTokenDispatcher, IYourTokenDispatcherTrait}; + use core::traits::TryInto; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::access::ownable::interface::IOwnable; + use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; + use starknet::{get_caller_address, get_contract_address}; + use super::{ContractAddress, IVendor}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + eth_token: IERC20CamelDispatcher, + your_token: IYourTokenDispatcher, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + BuyTokens: BuyTokens, + SellTokens: SellTokens, + } + + #[derive(Drop, starknet::Event)] + struct BuyTokens { + buyer: ContractAddress, + eth_amount: u256, + tokens_amount: u256, + } + + #[derive(Drop, starknet::Event)] + struct SellTokens { + #[key] + seller: ContractAddress, + tokens_amount: u256, + eth_amount: u256, + } + + #[constructor] + fn constructor( + ref self: ContractState, + eth_token_address: ContractAddress, + your_token_address: ContractAddress, + ) { + self.eth_token.write(IERC20CamelDispatcher { contract_address: eth_token_address }); + self.your_token.write(IYourTokenDispatcher { contract_address: your_token_address }); + // Implement your constructor here. + // ToDo: Initialize the owner of the contract. + } + + #[abi(embed_v0)] + impl VendorImpl of IVendor { + fn buy_tokens( + ref self: ContractState, eth_amount_wei: u256 + ) { // Implement your function buy_tokens here. + } + + fn withdraw(ref self: ContractState) { // Implement your function withdraw here. + } + + fn sell_tokens( + ref self: ContractState, amount_tokens: u256 + ) { // Implement your function sell_tokens here. + } + + fn tokens_per_eth( + self: @ContractState + ) -> u256 { // Modify to return the amount of tokens per 1 ETH. + 0 + } + + fn your_token(self: @ContractState) -> ContractAddress { + self.your_token.read().contract_address + } + + fn eth_token(self: @ContractState) -> ContractAddress { + self.eth_token.read().contract_address + } + } +} diff --git a/packages/snfoundry/contracts/src/YourContract.cairo b/packages/snfoundry/contracts/src/YourContract.cairo deleted file mode 100644 index 21e185095..000000000 --- a/packages/snfoundry/contracts/src/YourContract.cairo +++ /dev/null @@ -1,103 +0,0 @@ -#[starknet::interface] -pub trait IYourContract { - fn gretting(self: @TContractState) -> ByteArray; - fn set_gretting(ref self: TContractState, new_greeting: ByteArray, amount_eth: u256); - fn withdraw(ref self: TContractState); - fn premium(self: @TContractState) -> bool; -} - -#[starknet::contract] -mod YourContract { - use openzeppelin::access::ownable::OwnableComponent; - use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; - use starknet::{ContractAddress, contract_address_const}; - use starknet::{get_caller_address, get_contract_address}; - use super::{IYourContract}; - - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); - - #[abi(embed_v0)] - impl OwnableImpl = OwnableComponent::OwnableImpl; - impl OwnableInternalImpl = OwnableComponent::InternalImpl; - - const ETH_CONTRACT_ADDRESS: felt252 = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - OwnableEvent: OwnableComponent::Event, - GreetingChanged: GreetingChanged - } - - #[derive(Drop, starknet::Event)] - struct GreetingChanged { - #[key] - greeting_setter: ContractAddress, - #[key] - new_greeting: ByteArray, - premium: bool, - value: u256, - } - - #[storage] - struct Storage { - eth_token: IERC20CamelDispatcher, - greeting: ByteArray, - premium: bool, - total_counter: u256, - user_gretting_counter: LegacyMap, - #[substorage(v0)] - ownable: OwnableComponent::Storage, - } - - #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress) { - let eth_contract_address = contract_address_const::(); - self.eth_token.write(IERC20CamelDispatcher { contract_address: eth_contract_address }); - self.greeting.write("Building Unstoppable Apps!!!"); - self.ownable.initializer(owner); - } - - #[abi(embed_v0)] - impl YourContractImpl of IYourContract { - fn gretting(self: @ContractState) -> ByteArray { - self.greeting.read() - } - fn set_gretting(ref self: ContractState, new_greeting: ByteArray, amount_eth: u256) { - self.greeting.write(new_greeting); - self.total_counter.write(self.total_counter.read() + 1); - let user_counter = self.user_gretting_counter.read(get_caller_address()); - self.user_gretting_counter.write(get_caller_address(), user_counter + 1); - - if amount_eth > 0 { - // In `Debug Contract` or UI implementation call `approve` on ETH contract before invoke fn set_gretting() - self - .eth_token - .read() - .transferFrom(get_caller_address(), get_contract_address(), amount_eth); - self.premium.write(true); - } else { - self.premium.write(false); - } - self - .emit( - GreetingChanged { - greeting_setter: get_caller_address(), - new_greeting: self.greeting.read(), - premium: true, - value: 100 - } - ); - } - fn withdraw(ref self: ContractState) { - self.ownable.assert_only_owner(); - let balance = self.eth_token.read().balanceOf(get_contract_address()); - self.eth_token.read().transfer(self.ownable.owner(), balance); - } - fn premium(self: @ContractState) -> bool { - self.premium.read() - } - } -} diff --git a/packages/snfoundry/contracts/src/YourToken.cairo b/packages/snfoundry/contracts/src/YourToken.cairo new file mode 100644 index 000000000..bac718cc1 --- /dev/null +++ b/packages/snfoundry/contracts/src/YourToken.cairo @@ -0,0 +1,82 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IYourToken { + fn balance_of(self: @T, account: ContractAddress) -> u256; + fn total_supply(self: @T) -> u256; + fn transfer(ref self: T, recipient: ContractAddress, amount: u256) -> bool; + fn approve(ref self: T, spender: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: T, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn allowance(self: @T, owner: ContractAddress, spender: ContractAddress) -> u256; +} + +#[starknet::contract] +mod YourToken { + use openzeppelin::token::erc20::interface::IERC20; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + + use super::{ContractAddress, IYourToken}; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + + #[constructor] + fn constructor( + ref self: ContractState, recipient: ContractAddress + ) { // Implement your constructor here. + let name = "Gold"; + let symbol = "GLD"; + self.erc20.initializer(name, symbol); + // Uncomment this line below + // let fixed_supply: u256 = 2_000_000_000_000_000_000_000; //2000 * 10^18 + // ToDo: Mint `fixed_supply` tokens to `recipient`. + } + + #[abi(embed_v0)] + impl IYourTokenImpl of IERC20 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + fn total_supply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self.erc20.transfer(recipient, amount) + } + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.erc20.approve(spender, amount) + } + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + } +} diff --git a/packages/snfoundry/contracts/src/lib.cairo b/packages/snfoundry/contracts/src/lib.cairo index 568f9f72a..81a54a3ad 100644 --- a/packages/snfoundry/contracts/src/lib.cairo +++ b/packages/snfoundry/contracts/src/lib.cairo @@ -1,4 +1,8 @@ -mod YourContract; +mod Vendor; +mod YourToken; +mod mock_contracts { + pub mod MockETHToken; +} #[cfg(test)] mod test { mod TestContract; diff --git a/packages/snfoundry/contracts/src/mock_contracts/MockETHToken.cairo b/packages/snfoundry/contracts/src/mock_contracts/MockETHToken.cairo new file mode 100644 index 000000000..070643118 --- /dev/null +++ b/packages/snfoundry/contracts/src/mock_contracts/MockETHToken.cairo @@ -0,0 +1,34 @@ +#[starknet::contract] +pub mod MockETHToken { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { + let name = "MockETH"; + let symbol = "ETH"; + + self.erc20.initializer(name, symbol); + let amount_to_mint = initial_supply / 10; + self.erc20.mint(recipient, amount_to_mint); + } +} diff --git a/packages/snfoundry/contracts/src/test/TestContract.cairo b/packages/snfoundry/contracts/src/test/TestContract.cairo index 8b1378917..93eb4eabb 100644 --- a/packages/snfoundry/contracts/src/test/TestContract.cairo +++ b/packages/snfoundry/contracts/src/test/TestContract.cairo @@ -1 +1,272 @@ +use contracts::Vendor::{IVendorDispatcher, IVendorDispatcherTrait}; +use contracts::YourToken::{IYourTokenDispatcher, IYourTokenDispatcherTrait}; +use contracts::mock_contracts::MockETHToken; +use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use snforge_std::{ + declare, ContractClassTrait, cheat_caller_address, cheat_block_timestamp, CheatSpan +}; +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; + +fn RECIPIENT() -> ContractAddress { + contract_address_const::<'RECIPIENT'>() +} + +fn OTHER() -> ContractAddress { + contract_address_const::<'OTHER'>() +} +// Should deploy the MockETHToken contract +fn deploy_mock_eth_token() -> ContractAddress { + let erc20_class_hash = declare("MockETHToken").unwrap(); + let INITIAL_SUPPLY: u256 = 100000000000000000000; // 100_ETH_IN_WEI + let mut calldata = array![]; + calldata.append_serde(INITIAL_SUPPLY); + calldata.append_serde(RECIPIENT()); + let (eth_token_address, _) = erc20_class_hash.deploy(@calldata).unwrap(); + eth_token_address +} + +// Should deploy the YourToken contract +fn deploy_your_token_token() -> ContractAddress { + let erc20_class_hash = declare("YourToken").unwrap(); + let mut calldata = array![]; + calldata.append_serde(RECIPIENT()); + let (your_token_address, _) = erc20_class_hash.deploy(@calldata).unwrap(); + println!("-- YourToken contract deployed on: {:?}", your_token_address); + your_token_address +} + +// Should deploy the Vendor contract +fn deploy_vendor_contract() -> ContractAddress { + let eth_token_address = deploy_mock_eth_token(); + let your_token_address = deploy_your_token_token(); + let vendor_class_hash = declare("Vendor").unwrap(); + let tester_address = RECIPIENT(); + let mut calldata = array![]; + calldata.append_serde(eth_token_address); + calldata.append_serde(your_token_address); + calldata.append_serde(tester_address); + let (vendor_contract_address, _) = vendor_class_hash.deploy(@calldata).unwrap(); + println!("-- Vendor contract deployed on: {:?}", vendor_contract_address); + + // send eth to vendor contract + // change the caller address of the eth_token_address to be tester_address + cheat_caller_address(eth_token_address, tester_address, CheatSpan::TargetCalls(1)); + let eth_amount_wei: u256 = 1000000000000000000; // 1_ETH_IN_WEI + let eth_token_dispatcher = IERC20CamelDispatcher { contract_address: eth_token_address }; + assert( + eth_token_dispatcher.transfer(vendor_contract_address, eth_amount_wei), 'Transfer failed' + ); + let vendor_eth_balance = eth_token_dispatcher.balanceOf(vendor_contract_address); + println!("-- Vendor eth balance: {:?} ETH in wei", vendor_eth_balance); + + // send GLD token to vendor contract + // Change the caller address of the your_token_address to be tester_address + cheat_caller_address(your_token_address, tester_address, CheatSpan::TargetCalls(1)); + let your_token_dispatcher = IYourTokenDispatcher { contract_address: your_token_address }; + let INITIAL_BALANCE: u256 = 1000000000000000000000; // 1000_GLD_IN_WEI + assert( + your_token_dispatcher.transfer(vendor_contract_address, INITIAL_BALANCE), 'Transfer failed' + ); + let vendor_token_balance = your_token_dispatcher.balance_of(vendor_contract_address); + println!("-- Vendor GLD token balance: {:?} GLD in wei", vendor_token_balance); + vendor_contract_address +} + +#[test] +fn test_deploy_mock_eth_token() { + let INITIAL_BALANCE: u256 = 10000000000000000000; // 10_ETH_IN_WEI + let contract_address = deploy_mock_eth_token(); + let eth_token_dispatcher = IERC20CamelDispatcher { contract_address }; + assert(eth_token_dispatcher.balanceOf(RECIPIENT()) == INITIAL_BALANCE, 'Balance should be > 0'); +} + +#[test] +fn test_deploy_your_token() { + let MINIMUN_SUPPLY: u256 = 1000000000000000000000; // 1000_GLD_IN_WEI + let contract_address = deploy_your_token_token(); + let your_token_dispatcher = IYourTokenDispatcher { contract_address }; + let total_supply = your_token_dispatcher.total_supply(); + println!("-- Total supply: {:?}", total_supply); + assert(total_supply >= MINIMUN_SUPPLY, 'supply should be at least 1000'); +} + +#[test] +fn test_deploy_vendor() { + deploy_vendor_contract(); +} + +//Should let us buy tokens and our balance should go up... +#[test] +fn test_buy_tokens() { + let vendor_contract_address = deploy_vendor_contract(); + let vendor_dispatcher = IVendorDispatcher { contract_address: vendor_contract_address }; + let your_token_address = vendor_dispatcher.your_token(); + let your_token_dispatcher = IYourTokenDispatcher { contract_address: your_token_address }; + let eth_token_address = vendor_dispatcher.eth_token(); + let eth_token_dispatcher = IERC20CamelDispatcher { contract_address: eth_token_address }; + + let tester_address = RECIPIENT(); + + println!("-- Tester address: {:?}", tester_address); + let starting_balance = your_token_dispatcher.balance_of(tester_address); // 1000 GLD_IN_WEI + println!("---- Starting token balance: {:?} GLD in wei", starting_balance); + + println!("-- Buying 0.001 ETH worth of tokens ..."); + let eth_amount_wei: u256 = 1000000000000000; // 0.001_ETH_IN_WEI + // Change the caller address of the ETH_token_contract to the tester_address + cheat_caller_address(eth_token_address, tester_address, CheatSpan::TargetCalls(1)); + eth_token_dispatcher.approve(vendor_contract_address, eth_amount_wei); + // check allowance + let allowance = eth_token_dispatcher.allowance(tester_address, vendor_contract_address); + assert_eq!(allowance, eth_amount_wei, "Allowance should be equal to the bought amount"); + + // Change the caller address of the your_token_address to the tester_address + cheat_caller_address(vendor_contract_address, tester_address, CheatSpan::TargetCalls(1)); + vendor_dispatcher.buy_tokens(eth_amount_wei); + println!("-- Bought 0.001 ETH worth of tokens"); + let tokens_per_eth: u256 = vendor_dispatcher.tokens_per_eth(); // 100 tokens per ETH + let expected_tokens = eth_amount_wei * tokens_per_eth; // 0.1_GLD_IN_WEI ; + println!("---- Expect to receive: {:?} GLD in wei", expected_tokens); + let expected_balance = starting_balance + expected_tokens; // 1000 + 0.1 = 1000.1_GLD_IN_WEI + let new_balance = your_token_dispatcher.balance_of(tester_address); + println!("---- New token balance: {:?} GLD in wei", new_balance); + assert_eq!(new_balance, expected_balance, "Balance should be increased by the bought amount"); +} + +// Should let us sell tokens and we should get the appropriate amount eth back... +#[test] +fn test_sell_tokens() { + let vendor_contract_address = deploy_vendor_contract(); + let vendor_dispatcher = IVendorDispatcher { contract_address: vendor_contract_address }; + let your_token_address = vendor_dispatcher.your_token(); + let your_token_dispatcher = IYourTokenDispatcher { contract_address: your_token_address }; + + let tester_address = RECIPIENT(); + + println!("-- Tester address: {:?}", tester_address); + let starting_balance = your_token_dispatcher.balance_of(tester_address); // 1000 GLD_IN_WEI + println!("---- Starting token balance: {:?} GLD in wei", starting_balance); + + println!("-- Selling back 0.1 GLD tokens ..."); + let gld_token_amount_wei: u256 = 100000000000000000; // 0.1_GLD_IN_WEI + // Change the caller address of the your_token_contract to the tester_address + cheat_caller_address(your_token_address, tester_address, CheatSpan::TargetCalls(1)); + your_token_dispatcher.approve(vendor_contract_address, gld_token_amount_wei); + // check allowance + let allowance = your_token_dispatcher.allowance(tester_address, vendor_contract_address); + assert_eq!(allowance, gld_token_amount_wei, "Allowance should be equal to the sold amount"); + + // Change the caller address of the your_token_address to the tester_address + cheat_caller_address(vendor_contract_address, tester_address, CheatSpan::TargetCalls(1)); + vendor_dispatcher.sell_tokens(gld_token_amount_wei); + println!("-- Sold 0.1 GLD tokens"); + let new_balance = your_token_dispatcher.balance_of(tester_address); + println!("---- New token balance: {:?} GLD in wei", new_balance); + let expected_balance = starting_balance + - gld_token_amount_wei; // 2000 - 0.1 = 1999.9_GLD_IN_WEI + assert_eq!(new_balance, expected_balance, "Balance should be decreased by the sold amount"); +} + +//Should let the owner (and nobody else) withdraw the eth from the contract... +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_failing_withdraw_tokens() { + let vendor_contract_address = deploy_vendor_contract(); + let vendor_dispatcher = IVendorDispatcher { contract_address: vendor_contract_address }; + let your_token_address = vendor_dispatcher.your_token(); + let your_token_dispatcher = IYourTokenDispatcher { contract_address: your_token_address }; + let eth_token_address = vendor_dispatcher.eth_token(); + let eth_token_dispatcher = IERC20CamelDispatcher { contract_address: eth_token_address }; + + let tester_address = RECIPIENT(); + + println!("-- Tester address: {:?}", tester_address); + let starting_balance = your_token_dispatcher.balance_of(tester_address); // 1000 GLD_IN_WEI + println!("---- Starting token balance: {:?} GLD in wei", starting_balance); + + println!("-- Buying 0.1 ETH worth of tokens ..."); + let eth_amount_wei: u256 = 100000000000000000; // 0.1_ETH_IN_WEI + // Change the caller address of the ETH_token_contract to the tester_address + cheat_caller_address(eth_token_address, tester_address, CheatSpan::TargetCalls(1)); + eth_token_dispatcher.approve(vendor_contract_address, eth_amount_wei); + // check allowance + let allowance = eth_token_dispatcher.allowance(tester_address, vendor_contract_address); + assert_eq!(allowance, eth_amount_wei, "Allowance should be equal to the bought amount"); + + // Change the caller address of the your_token_address to the tester_address + cheat_caller_address(vendor_contract_address, tester_address, CheatSpan::TargetCalls(1)); + vendor_dispatcher.buy_tokens(eth_amount_wei); + println!("-- Bought 0.1 ETH worth of tokens"); + let tokens_per_eth: u256 = vendor_dispatcher.tokens_per_eth(); // 100 tokens per ETH + let expected_tokens = eth_amount_wei * tokens_per_eth; // 10_GLD_IN_WEI ; + println!("---- Expect to receive: {:?} GLD in wei", expected_tokens); + let expected_balance = starting_balance + expected_tokens; // 1000 + 0.1 = 1000.1_GLD_IN_WEI + let new_balance = your_token_dispatcher.balance_of(tester_address); + println!("---- New token balance: {:?} GLD in wei", new_balance); + assert_eq!(new_balance, expected_balance, "Balance should be increased by the bought amount"); + + let vendor_eth_balance = eth_token_dispatcher.balanceOf(vendor_contract_address); + println!("---- Vendor contract eth balance: {:?} ETH in wei", vendor_eth_balance); + + let not_owner_address = OTHER(); + let not_owner_balance = eth_token_dispatcher.balanceOf(not_owner_address); + println!("---- Other address eth balance: {:?} ETH in wei", not_owner_balance); + // Change the caller address of the vendor_contract_address to the not_owner_address + cheat_caller_address(vendor_contract_address, not_owner_address, CheatSpan::TargetCalls(1)); + vendor_dispatcher.withdraw(); + + let balance_after_attemp_withdraw = eth_token_dispatcher.balanceOf(vendor_contract_address); + println!( + "---- Vendor contract eth balance after withdraw: {:?} ETH in wei", + balance_after_attemp_withdraw + ); + assert_eq!(not_owner_balance, balance_after_attemp_withdraw, "Balance should be the same"); +} + +#[test] +fn test_success_withdraw_tokens() { + let vendor_contract_address = deploy_vendor_contract(); + let vendor_dispatcher = IVendorDispatcher { contract_address: vendor_contract_address }; + let eth_token_address = vendor_dispatcher.eth_token(); + let eth_token_dispatcher = IERC20CamelDispatcher { contract_address: eth_token_address }; + + let owner_address = RECIPIENT(); + + println!("-- Tester address: {:?}", owner_address); + + println!("-- Buying 0.1 ETH worth of tokens ..."); + let eth_amount_wei: u256 = 100000000000000000; // 0.1_ETH_IN_WEI + // Change the caller address of the ETH_token_contract to the owner_address + cheat_caller_address(eth_token_address, owner_address, CheatSpan::TargetCalls(1)); + eth_token_dispatcher.approve(vendor_contract_address, eth_amount_wei); + + // Change the caller address of the your_token_address to the owner_address + cheat_caller_address(vendor_contract_address, owner_address, CheatSpan::TargetCalls(1)); + vendor_dispatcher.buy_tokens(eth_amount_wei); + println!("-- Bought 0.1 ETH worth of tokens"); + + let owner_eth_balance_before_withdraw = eth_token_dispatcher.balanceOf(owner_address); + println!( + "---- Owner token balance before withdraw: {:?} ETH in wei", + owner_eth_balance_before_withdraw + ); + + let vendor_eth_balance = eth_token_dispatcher.balanceOf(vendor_contract_address); + println!("---- Vendor contract eth balance: {:?} ETH in wei", vendor_eth_balance); + + println!("-- Withdrawing eth from Vendor contract ..."); + // Change the caller address of the vendor_contract_address to the owner_address + cheat_caller_address(vendor_contract_address, owner_address, CheatSpan::TargetCalls(1)); + vendor_dispatcher.withdraw(); + let eth_balance_after_withdraw = eth_token_dispatcher.balanceOf(owner_address); + println!( + "---- Owner token balance after withdraw: {:?} ETH in wei", eth_balance_after_withdraw + ); + assert_eq!( + owner_eth_balance_before_withdraw + vendor_eth_balance, + eth_balance_after_withdraw, + "Balance should be the same" + ); +} diff --git a/packages/snfoundry/scripts-ts/deploy.ts b/packages/snfoundry/scripts-ts/deploy.ts index 101aa7cfe..ba7186f11 100644 --- a/packages/snfoundry/scripts-ts/deploy.ts +++ b/packages/snfoundry/scripts-ts/deploy.ts @@ -1,60 +1,62 @@ import { deployContract, executeDeployCalls, - exportDeployments, deployer, + exportDeployments, } from "./deploy-contract"; import { green } from "./helpers/colorize-log"; -/** - * Deploy a contract using the specified parameters. - * - * @example (deploy contract with contructorArgs) - * const deployScript = async (): Promise => { - * await deployContract( - * { - * contract: "YourContract", - * contractName: "YourContractExportName", - * constructorArgs: { - * owner: deployer.address, - * }, - * options: { - * maxFee: BigInt(1000000000000) - * } - * } - * ); - * }; - * - * @example (deploy contract without contructorArgs) - * const deployScript = async (): Promise => { - * await deployContract( - * { - * contract: "YourContract", - * contractName: "YourContractExportName", - * options: { - * maxFee: BigInt(1000000000000) - * } - * } - * ); - * }; - * - * - * @returns {Promise} - */ +let your_token: any; +let vendor: any; const deployScript = async (): Promise => { - await deployContract({ - contract: "YourContract", + your_token = await deployContract({ + contract: "YourToken", constructorArgs: { - owner: deployer.address, + recipient: deployer.address, // ~~~YOUR FRONTEND ADDRESS HERE~~~~ }, }); }; -deployScript() - .then(async () => { - await executeDeployCalls(); - exportDeployments(); +// Todo: Uncomment Vendor deploy lines +// vendor = await deployContract({ +// contract: "Vendor", +// constructorArgs: { +// eth_token_address: +// "0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7", +// your_token_address: your_token.address, - console.log(green("All Setup Done")); +// Todo: Add owner address, should be the same as `deployer.address` +// }, +// }); + +// const transferScript = async (): Promise => { +//transfer 1000 GLD tokens to VendorContract +// await deployer.execute( +// [ +// { +// contractAddress: your_token.address, +// calldata: [ +// vendor.address, +// { +// low: 1_000_000_000_000_000_000_000n, //1000 * 10^18 +// high: 0, +// }, +// ], +// entrypoint: "transfer", +// }, +// ], +// { +// maxFee: 1e18, +// } +// ); +// }; + +deployScript() + .then(() => { + executeDeployCalls().then(() => { + exportDeployments(); + // transferScript(); + }); + console.log("All Setup Done"); }) .catch(console.error);