diff --git a/apps/snfoundry/.env.example b/apps/snfoundry/.env.example index 464a702..169d21e 100644 --- a/apps/snfoundry/.env.example +++ b/apps/snfoundry/.env.example @@ -8,3 +8,6 @@ PRIVATE_KEY_SEPOLIA= RPC_URL_SEPOLIA=https://starknet-sepolia.public.blastapi.io/rpc/v0_7 ACCOUNT_ADDRESS_SEPOLIA= + +# IPFS +TOKEN_METADATA_URL="ipfs://test/{id}.json" diff --git a/apps/snfoundry/README.md b/apps/snfoundry/README.md new file mode 100644 index 0000000..3cdc571 --- /dev/null +++ b/apps/snfoundry/README.md @@ -0,0 +1,73 @@ +# CofiBlocks Contracts + +## Deployment Guide + +Follow these steps to deploy the CofiBlocks contracts on the StarkNet network. + +### 1. Configure the `.env` file + +Set the following environment variables in your `.env` file with the details of a prefunded wallet. This wallet will act as the admin address: + +- **`PRIVATE_KEY_SEPOLIA`** – The private key of the admin wallet. +- **`ACCOUNT_ADDRESS_SEPOLIA`** – The address of the admin wallet. +- **`TOKEN_METADATA_URL`** – The IPFS URL to serve as the token metadata. + + The URL should follow the format: `ipfs:///{id}.json`, where `{id}` will be dynamically replaced with the actual token ID by clients when fetching metadata. + + **Example:** + ``` + ipfs://bafybeihevtihdmcjkdh6sjdtkbdjnngbfdlr3tjk2dfmvd3demdm57o3va/{id}.json + ``` + For token ID `1`, the resulting URL will be: + ``` + ipfs://bafybeihevtihdmcjkdh6sjdtkbdjnngbfdlr3tjk2dfmvd3demdm57o3va/1.json + ``` + +### 2. Install dependencies + +Run the following command to install project dependencies: +```bash +bun i +``` + +### 3. Deploy the contracts + +To deploy the contracts on the Sepolia testnet, run: +```bash +bun deploy:sepolia +``` + +This command will: +- Deploy both the **CofiCollections** and **Marketplace** contracts. +- Set the **Marketplace** contract as the minter in the **CofiCollection** contract. +- Set the `base_uri` in the **CofiCollection** contract using the `TOKEN_METADATA_URL` value from the `.env` file. + +### 4. Retrieve deployed contract addresses + +Once the deployment is complete, the contract addresses will be available in: +- The terminal output. +- The file located at: `deployments/sepolia_latest.json`. + + +## Testing +To test the contracts, follow these steps. + +1. Go to contracts folder +```bash +cd contracts +``` + +2. Run test command +```bash +scarb test +``` + +## Note +When updating the contracts, you need to update them in the web app too. To do that follow this steps + +1. Go to page https://scaffold-stark-demo.vercel.app/configure +2. Copy/paste the contract address in the Address field. Use this valid names: CofiCollection, Marketplace. +3. Click on download Contract file. It will give you a json file with the contract abi and contract address. +4. Update the file https://github.com/Vagabonds-Labs/cofiblocks/blob/main/apps/web/src/contracts/configExternalContracts.ts +with the new data (just copy/paste the contract metadata in the corresponding field without affecting other contracts metadata). +5. Do the same for Marketplace contract. diff --git a/apps/snfoundry/contracts/src/marketplace.cairo b/apps/snfoundry/contracts/src/marketplace.cairo index 58b63fa..6956a16 100644 --- a/apps/snfoundry/contracts/src/marketplace.cairo +++ b/apps/snfoundry/contracts/src/marketplace.cairo @@ -13,9 +13,12 @@ pub trait IMarketplace { fn create_product( ref self: ContractState, initial_stock: u256, price: u256, data: Span ) -> u256; - fn create_products(ref self: ContractState, initial_stock: Span, price: Span) -> Span; + fn create_products( + ref self: ContractState, initial_stock: Span, price: Span + ) -> Span; fn delete_product(ref self: ContractState, token_id: u256); fn delete_products(ref self: ContractState, token_ids: Span); + fn claim_balance(self: @ContractState, producer: ContractAddress) -> u256; fn claim(ref self: ContractState); } @@ -197,19 +200,19 @@ mod Marketplace { let buyer = get_caller_address(); let contract_address = get_contract_address(); let strk_token_dispatcher = IERC20Dispatcher { - contract_address: contract_address_const::() + contract_address: contract_address_const::() }; // Get payment from buyer let mut producer_fee = self.listed_product_price.read(token_id) * token_amount; let mut total_price = producer_fee + self.calculate_fee(producer_fee, self.market_fee.read()); assert( - strk_token_dispatcher.balance_of(get_caller_address()) >= total_price, - 'insufficient funds' + strk_token_dispatcher.balance_of(get_caller_address()) >= total_price, + 'insufficient funds' ); assert( - strk_token_dispatcher.allowance(buyer, contract_address) >= total_price, - 'insufficient allowance' + strk_token_dispatcher.allowance(buyer, contract_address) >= total_price, + 'insufficient allowance' ); strk_token_dispatcher.transfer_from(buyer, contract_address, total_price); @@ -336,7 +339,9 @@ mod Marketplace { token_id } - fn create_products(ref self: ContractState, initial_stock: Span, price: Span) -> Span { + fn create_products( + ref self: ContractState, initial_stock: Span, price: Span + ) -> Span { assert(initial_stock.len() == price.len(), 'wrong len of arrays'); self.accesscontrol.assert_only_role(PRODUCER); let producer = get_caller_address(); @@ -427,6 +432,10 @@ mod Marketplace { }; } + fn claim_balance(self: @ContractState, producer: ContractAddress) -> u256 { + self.claim_balances.read(producer) + } + fn claim(ref self: ContractState) { self.accesscontrol.assert_only_role(PRODUCER); let producer = get_caller_address(); diff --git a/apps/snfoundry/contracts/src/test/test_marketplace.cairo b/apps/snfoundry/contracts/src/test/test_marketplace.cairo index a623863..272deab 100644 --- a/apps/snfoundry/contracts/src/test/test_marketplace.cairo +++ b/apps/snfoundry/contracts/src/test/test_marketplace.cairo @@ -1,62 +1,61 @@ mod test_marketplace { use contracts::cofi_collection::ICofiCollectionDispatcher; use contracts::cofi_collection::ICofiCollectionDispatcherTrait; - use contracts::marketplace::{ IMarketplaceDispatcher, IMarketplaceDispatcherTrait }; + use contracts::marketplace::{IMarketplaceDispatcher, IMarketplaceDispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - use openzeppelin::utils::serde::SerializedAppend; - use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address}; use starknet::ContractAddress; use starknet::syscalls::call_contract_syscall; - + fn OWNER() -> ContractAddress { starknet::contract_address_const::<'OWNER'>() } const STRK_TOKEN_ADDRESS: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; - + const STRK_TOKEN_MINTER_ADDRESS: felt252 = 0x0594c1582459ea03f77deaf9eb7e3917d6994a03c13405ba42867f83d85f085d; const ONE_E18: u256 = 1000000000000000000_u256; const MARKET_FEE: u256 = 250_u256; // 2.5% - + fn deploy_receiver() -> ContractAddress { let contract = declare("Receiver").unwrap().contract_class(); let calldata = array![]; let (contract_address, _) = contract.deploy(@calldata).unwrap(); contract_address } - + fn deploy_cofi_collection() -> ICofiCollectionDispatcher { let contract = declare("CofiCollection").unwrap().contract_class(); - + let mut calldata: Array = array![]; calldata.append_serde(OWNER()); // default_admin calldata.append_serde(OWNER()); // pauser calldata.append_serde(OWNER()); // minter calldata.append_serde(OWNER()); // uri_setter calldata.append_serde(OWNER()); // upgrader - + let (contract_address, _) = contract.deploy(@calldata).unwrap(); let cofi_collection = ICofiCollectionDispatcher { contract_address }; - + cofi_collection } - + fn deploy_marketplace(cofi_collection: ContractAddress) -> IMarketplaceDispatcher { let contract = declare("Marketplace").unwrap().contract_class(); - + let mut calldata: Array = array![]; calldata.append_serde(cofi_collection); // coffi_collection calldata.append_serde(OWNER()); // admin calldata.append_serde(MARKET_FEE); // market fee - + let (contract_address, _) = contract.deploy(@calldata).unwrap(); let marketplace = IMarketplaceDispatcher { contract_address }; - + marketplace } @@ -65,12 +64,12 @@ mod test_marketplace { let market_fee = total_price * MARKET_FEE / 10000; total_price + market_fee } - + #[test] fn test_assign_seller_role() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + start_cheat_caller_address(marketplace.contract_address, OWNER()); marketplace.assign_seller_role(OWNER()); } @@ -79,7 +78,7 @@ mod test_marketplace { fn test_assign_consumer_role() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + start_cheat_caller_address(marketplace.contract_address, OWNER()); marketplace.assign_consumer_role(OWNER()); } @@ -88,7 +87,7 @@ mod test_marketplace { fn test_assign_admin_role() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + start_cheat_caller_address(marketplace.contract_address, OWNER()); let ANYONE = starknet::contract_address_const::<'ANYONE'>(); marketplace.assign_admin_role(ANYONE); @@ -98,7 +97,7 @@ mod test_marketplace { fn test_create_product() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); let PRODUCER = starknet::contract_address_const::<'PRODUCER'>(); @@ -120,7 +119,7 @@ mod test_marketplace { fn test_create_products() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); let PRODUCER = starknet::contract_address_const::<'PRODUCER'>(); @@ -144,7 +143,7 @@ mod test_marketplace { let cofi_collection = deploy_cofi_collection(); let CONSUMER = deploy_receiver(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); let PRODUCER = starknet::contract_address_const::<'PRODUCER'>(); @@ -168,14 +167,15 @@ mod test_marketplace { let amount_to_buy = 2; let required_tokens = calculate_total_token_price(price, amount_to_buy); let minter_address = starknet::contract_address_const::(); - let token_address = starknet::contract_address_const::(); + let token_address = starknet::contract_address_const::(); let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; start_cheat_caller_address(token_address, minter_address); let mut calldata = array![]; calldata.append_serde(CONSUMER); calldata.append_serde(required_tokens); - call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); + call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()) + .unwrap(); assert(token_dispatcher.balance_of(CONSUMER) == required_tokens, 'invalid balance'); @@ -199,7 +199,7 @@ mod test_marketplace { let cofi_collection = deploy_cofi_collection(); let CONSUMER = deploy_receiver(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); let PRODUCER = starknet::contract_address_const::<'PRODUCER'>(); @@ -221,14 +221,15 @@ mod test_marketplace { // Fund buyer wallet let minter_address = starknet::contract_address_const::(); - let token_address = starknet::contract_address_const::(); + let token_address = starknet::contract_address_const::(); let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; start_cheat_caller_address(token_address, minter_address); let mut calldata = array![]; calldata.append_serde(CONSUMER); calldata.append_serde(50000000_u256); - call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); + call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()) + .unwrap(); assert(token_dispatcher.balance_of(CONSUMER) == 50000000_u256, 'invalid balance'); @@ -248,7 +249,7 @@ mod test_marketplace { fn test_delete_product() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); let PRODUCER = starknet::contract_address_const::<'PRODUCER'>(); @@ -279,7 +280,7 @@ mod test_marketplace { fn test_delete_products() { let cofi_collection = deploy_cofi_collection(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); let PRODUCER = starknet::contract_address_const::<'PRODUCER'>(); @@ -297,11 +298,13 @@ mod test_marketplace { // Delete the product start_cheat_caller_address(cofi_collection.contract_address, marketplace.contract_address); - let tokens_before = cofi_collection.balance_of(marketplace.contract_address, *token_ids.at(0)); + let tokens_before = cofi_collection + .balance_of(marketplace.contract_address, *token_ids.at(0)); assert(tokens_before == 10, 'invalid tokens before'); marketplace.delete_products(token_ids); - let tokens_after = cofi_collection.balance_of(marketplace.contract_address, *token_ids.at(0)); + let tokens_after = cofi_collection + .balance_of(marketplace.contract_address, *token_ids.at(0)); assert(tokens_after == 0, 'invalid tokens after'); } @@ -312,7 +315,7 @@ mod test_marketplace { let CONSUMER = deploy_receiver(); let PRODUCER = deploy_receiver(); let marketplace = deploy_marketplace(cofi_collection.contract_address); - + // Create a producer start_cheat_caller_address(marketplace.contract_address, OWNER()); marketplace.assign_seller_role(PRODUCER); @@ -335,14 +338,15 @@ mod test_marketplace { let amount_to_buy = 2; let required_tokens = calculate_total_token_price(price, amount_to_buy); let minter_address = starknet::contract_address_const::(); - let token_address = starknet::contract_address_const::(); + let token_address = starknet::contract_address_const::(); let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; start_cheat_caller_address(token_address, minter_address); let mut calldata = array![]; calldata.append_serde(CONSUMER); calldata.append_serde(required_tokens); - call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); + call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()) + .unwrap(); // Approve marketplace to spend buyer's tokens start_cheat_caller_address(token_address, CONSUMER); @@ -362,6 +366,9 @@ mod test_marketplace { start_cheat_caller_address(marketplace.contract_address, PRODUCER); let producer_starks_before = token_dispatcher.balance_of(PRODUCER); + let claim_balance = marketplace.claim_balance(PRODUCER); + assert(claim_balance > 0, 'invalid claim balance'); + marketplace.claim(); let producer_starks_after = token_dispatcher.balance_of(PRODUCER); assert(producer_starks_before < producer_starks_after, 'invalid producer starks'); diff --git a/apps/snfoundry/deployments/.gitignore b/apps/snfoundry/deployments/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/apps/snfoundry/deployments/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/apps/snfoundry/deployments/sepolia_latest.json b/apps/snfoundry/deployments/sepolia_latest.json new file mode 100644 index 0000000..f704a2a --- /dev/null +++ b/apps/snfoundry/deployments/sepolia_latest.json @@ -0,0 +1,12 @@ +{ + "CofiCollection": { + "classHash": "0x7a108607024be2054d1558cbfd5de3fe264afe9e025bc7653b1bc5fc76d1af0", + "address": "0x2beb2106100c5238830e2d0e23193f581fd69e53b9c5d0f7eabf11c7d85db14", + "contract": "cofi_collection.cairo" + }, + "Marketplace": { + "classHash": "0x44eb6074fa74a9dbddb65c52394cd1bab274e3bc22e1fc36c26c37686d1e440", + "address": "0x3b73e235a852f76cb01e014246c59db4623d7ecf5c68675f07df0e209cd0303", + "contract": "marketplace.cairo" + } +} diff --git a/apps/snfoundry/package.json b/apps/snfoundry/package.json index 4d34445..f512eee 100644 --- a/apps/snfoundry/package.json +++ b/apps/snfoundry/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "scripts": { "chain": "starknet-devnet --seed 0 --account-class cairo1", - "deploy": "ts-node scripts-ts/helpers/deploy-wrapper.ts", + "deploy:sepolia": "ts-node scripts-ts/helpers/deploy-wrapper.ts --network sepolia", "deploy:no-reset": "snfoundry deploy --no-reset", "test": "cd contracts && snforge test", "test-eslint": "node eslint-contract-name/eslint-plugin-contract-names.test.js", diff --git a/apps/snfoundry/scripts-cairo/README.md b/apps/snfoundry/scripts-cairo/README.md deleted file mode 100644 index 62270fa..0000000 --- a/apps/snfoundry/scripts-cairo/README.md +++ /dev/null @@ -1 +0,0 @@ -# COMING SOON diff --git a/apps/snfoundry/scripts-ts/deploy.ts b/apps/snfoundry/scripts-ts/deploy.ts index 0e93278..3eb1107 100644 --- a/apps/snfoundry/scripts-ts/deploy.ts +++ b/apps/snfoundry/scripts-ts/deploy.ts @@ -1,4 +1,4 @@ -import { addAddressPadding } from "starknet"; +import { addAddressPadding, byteArray } from "starknet"; import { deployContract, deployer, @@ -42,41 +42,75 @@ import { green } from "./helpers/colorize-log"; * * @returns {Promise} */ + +const string_to_byte_array = (str: string): string[] => { + const byte_array = byteArray.byteArrayFromString(str); + const result = [`0x${byte_array.data.length.toString(16)}`]; + for (let i = 0; i < byte_array.data.length; i++) { + result.push(byte_array.data[i].toString()); + } + if (byte_array.pending_word) { + result.push(byte_array.pending_word.toString()); + } + result.push(`0x${byte_array.pending_word_len.toString(16)}`); + return result; +}; + const deployScript = async (): Promise => { - console.log("πŸš€ Deploying with address:", green(deployer.address)); + console.log("πŸš€ Creating deployment calls..."); - // await deployContract({ - // contract: "cofi_collection.cairo", - // contractName: "CofiCollection", - // constructorArgs: { - // default_admin: deployer.address, - // pauser: deployer.address, - // minter: deployer.address, - // uri_setter: deployer.address, - // upgrader: deployer.address, - // }, - // }); - // Deploy Marketplace - await deployContract({ + const { address: cofiCollectionAddress } = await deployContract({ + contract: "cofi_collection.cairo", + contractName: "CofiCollection", + constructorArgs: { + default_admin: deployer.address, + pauser: deployer.address, + minter: deployer.address, + uri_setter: deployer.address, + upgrader: deployer.address, + }, + }); + + const { address: marketplaceAddress } = await deployContract({ contract: "marketplace.cairo", contractName: "Marketplace", - // TODO: incluide constructor args for deploy - // cofi_collection_address: ContractAddress - // cofi_vault_address: ContractAddress - // strk_contract: ContractAddress constructorArgs: { - cofi_collection_address: addAddressPadding( - "0x0448d8cc3403303a76d89a56b7e8ecf9aa9fcd41e7bb66d10f4be5b67b2f8aab", - ), + cofi_collection_address: cofiCollectionAddress, admin: deployer.address, market_fee: BigInt(300000), }, }); + + console.log( + "CofiCollection will be deployed at:", + green(cofiCollectionAddress), + ); + console.log("Marketplace will be deployed at:", green(marketplaceAddress)); + await executeDeployCalls(); + + console.log("πŸš€ Setting marketplace as minter and setting base uri..."); + const base_uri_txt = process.env.TOKEN_METADATA_URL || ""; + console.log("Base URI:", base_uri_txt); + const transactions = [ + { + contractAddress: cofiCollectionAddress, + entrypoint: "set_minter", + calldata: { + minter: marketplaceAddress, + }, + }, + { + contractAddress: cofiCollectionAddress, + entrypoint: "set_base_uri", + calldata: string_to_byte_array(base_uri_txt), + }, + ]; + const { transaction_hash } = await deployer.execute(transactions); + console.log("πŸš€ Final transactions hash", transaction_hash); }; deployScript() .then(async () => { - await executeDeployCalls(); exportDeployments(); console.log(green("All Setup Done")); diff --git a/apps/snfoundry/scripts-ts/helpers/deploy-wrapper.ts b/apps/snfoundry/scripts-ts/helpers/deploy-wrapper.ts index 84dbad4..c41176f 100644 --- a/apps/snfoundry/scripts-ts/helpers/deploy-wrapper.ts +++ b/apps/snfoundry/scripts-ts/helpers/deploy-wrapper.ts @@ -10,29 +10,40 @@ interface CommandLineOptions { fee?: string; } -const argv = yargs(process.argv.slice(2)) - .options({ - network: { type: "string" }, - fee: { type: "string", choices: ["eth", "strk"], default: "eth" }, - reset: { +function main() { + const argv = yargs(process.argv.slice(2)) + .option("network", { + type: "string", + choices: ["devnet", "sepolia", "mainnet"], + default: "sepolia", + }) + .option("fee", { type: "string", choices: ["eth", "strk"], default: "eth" }) + .option("reset", { type: "boolean", description: "Do not reset deployments (keep existing deployments)", default: true, - }, - }) - .parseSync() as CommandLineOptions; + }) + .demandOption(["network", "fee", "reset"]) + .parseSync() as CommandLineOptions; -// Set the NETWORK environment variable based on the --network argument -process.env.NETWORK = argv.network || "devnet"; -process.env.FEE_TOKEN = argv.fee || "eth"; -process.env.NO_RESET = !argv.reset ? "true" : "false"; + if (argv._.length > 0) { + console.error( + "❌ Invalid arguments, only --network, --fee, or --reset/--no-reset can be passed in", + ); + return; + } -// Execute the deploy script without the reset option -try { - execSync( - `cd contracts && scarb build && ts-node ../scripts-ts/deploy.ts --network ${process.env.NETWORK} --fee ${process.env.FEE_TOKEN} --no-reset ${process.env.NO_RESET} && ts-node ../scripts-ts/helpers/parse-deployments.ts && cd ../../..`, - { stdio: "inherit" }, - ); -} catch (error) { - console.error("Error during deployment:", error); + // Execute the deploy script without the reset option + try { + execSync( + `cd contracts && scarb build && ts-node ../scripts-ts/deploy.ts --network ${argv.network || "devnet"} --fee ${argv.fee || "eth"} ${!argv.reset && "--no-reset "} && ts-node ../scripts-ts/helpers/parse-deployments.ts && cdΒ·..`, + { stdio: "inherit" }, + ); + } catch (error) { + console.error("Error during deployment:", error); + } +} + +if (require.main === module) { + main(); } diff --git a/apps/web/.env.example b/apps/web/.env.example index 636a4eb..a3a1561 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -8,4 +8,6 @@ NEXT_PUBLIC_BASE_URL=/ NEXT_PUBLIC_SIGNER_ADDRESS=0x123 PINATA_JWT= -NEXT_PUBLIC_GATEWAY_URL= \ No newline at end of file +NEXT_PUBLIC_GATEWAY_URL=https://api.cartridge.gg/x/starknet/sepolia + +STARKNET_ENV=sepolia \ No newline at end of file diff --git a/apps/web/README.md b/apps/web/README.md index 67943c7..e60cae6 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,29 +1,54 @@ -# Create T3 App +# CofiBlocks Web -This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. +## Running the Application -## What's next? How do I make an app with this? +Follow the steps below to set up and run the CofiBlocks web application. -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. +### 1. Set Up Environment Variables -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. +Create a `.env` file by copying the example configuration: -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Prisma](https://prisma.io) -- [Drizzle](https://orm.drizzle.team) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) +```bash +cp .env.example .env +``` -## Learn More +Then, fill in the required values in the `.env` file: -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: +- **`DATABASE_URL`**: The database connection URL. + - Example for a local MySQL setup: + ``` + mysql://root:root@127.0.0.1:3306/web + ``` -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) β€” Check out these awesome tutorials +### 2. Start the Database -You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) β€” your feedback and contributions are welcome! +Ensure Docker is running and execute the script to start the database: -## How do I deploy this? +```bash +./start-database.sh +``` + +### 3. Generate Database Migrations + +Run the following command to generate the required database migrations: + +```bash +bun db:generate +``` + +### 4. Generate Prisma Client + +Execute the command to generate the Prisma client: + +```bash +bun postinstall +``` + +### 5. Start the Application + +To run the application in development mode, use: + +```bash +bun turbo dev +``` -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. diff --git a/apps/web/package.json b/apps/web/package.json index 7e42fe0..ef03efc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@auth/prisma-adapter": "^1.6.0", - "@prisma/client": "^5.14.0", + "@prisma/client": "^6.2.1", "@repo/ui": "*", "@starknet-react/chains": "^0.1.7", "@starknet-react/core": "^2.9.0", diff --git a/apps/web/prisma/migrations/20250120164024_add_token_id_to_product/migration.sql b/apps/web/prisma/migrations/20250120164024_add_token_id_to_product/migration.sql new file mode 100644 index 0000000..6a0b726 --- /dev/null +++ b/apps/web/prisma/migrations/20250120164024_add_token_id_to_product/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `tokenId` to the `Product` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `Product` ADD COLUMN `tokenId` INTEGER NOT NULL; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 2a31d2d..ba8efb7 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -35,6 +35,7 @@ model ShoppingCart { model Product { id Int @id @default(autoincrement()) + tokenId Int name String price Float nftMetadata Json diff --git a/apps/web/prisma/seed.js b/apps/web/prisma/seed.js index aab73d3..c7ce46b 100644 --- a/apps/web/prisma/seed.js +++ b/apps/web/prisma/seed.js @@ -107,6 +107,7 @@ async function main() { prisma.product.create({ data: { name: card.title, + tokenId: index + 1, price: 15.99 + index * 2, // Randomize price for realism nftMetadata: JSON.stringify({ description: card.description, diff --git a/apps/web/src/app/_components/features/ProductCatalog.tsx b/apps/web/src/app/_components/features/ProductCatalog.tsx index 110988d..a423ea8 100755 --- a/apps/web/src/app/_components/features/ProductCatalog.tsx +++ b/apps/web/src/app/_components/features/ProductCatalog.tsx @@ -39,7 +39,7 @@ export default function ProductCatalog() { const allProducts = data.pages.flatMap((page) => page.products.map((product) => ({ ...product, - process: product.process ?? "Natural", + process: "Natural", })), ); setProducts(allProducts); @@ -81,12 +81,12 @@ export default function ProductCatalog() { return ( accessProductDetails(product.id)} /> ); diff --git a/apps/web/src/app/_components/features/ProductDetails.tsx b/apps/web/src/app/_components/features/ProductDetails.tsx index 5c002cc..d27e6e2 100644 --- a/apps/web/src/app/_components/features/ProductDetails.tsx +++ b/apps/web/src/app/_components/features/ProductDetails.tsx @@ -16,6 +16,7 @@ import { SelectionTypeCard } from "./SelectionTypeCard"; interface ProductDetailsProps { product: { id: number; + tokenId: number; image: string; name: string; region: string; @@ -59,6 +60,7 @@ export default function ProductDetails({ product }: ProductDetailsProps) { setIsAddingToCart(true); addItem({ id: String(product.id), + tokenId: product.tokenId, name: product.name, quantity: quantity, price: product.price, diff --git a/apps/web/src/app/_components/features/ProductList.tsx b/apps/web/src/app/_components/features/ProductList.tsx index eb5ca11..19be17f 100644 --- a/apps/web/src/app/_components/features/ProductList.tsx +++ b/apps/web/src/app/_components/features/ProductList.tsx @@ -5,6 +5,7 @@ import { addItemAtom } from "~/store/cartAtom"; interface Product { id: number; + tokenId: number; name: string; price: number; description: string; @@ -20,6 +21,7 @@ export default function ProductList({ products }: ProductListProps) { const handleAddToCart = (product: Product) => { addItem({ id: String(product.id), + tokenId: product.tokenId, name: product.name, quantity: 1, price: product.price, diff --git a/apps/web/src/app/_components/features/types.ts b/apps/web/src/app/_components/features/types.ts index dc8755b..ef447ed 100644 --- a/apps/web/src/app/_components/features/types.ts +++ b/apps/web/src/app/_components/features/types.ts @@ -4,16 +4,17 @@ export type NftMetadata = { imageUrl: string; imageAlt: string; description: string; + strength: string; + farmName: string; + region: string; }; export type Product = { id: number; + tokenId: number; name: string; price: number; nftMetadata: Prisma.JsonValue | NftMetadata; - region: string; - farmName: string; - strength: string; process?: string; createdAt: Date; updatedAt: Date; diff --git a/apps/web/src/app/api/files/route.ts b/apps/web/src/app/api/files/route.ts index f933d5c..d64b8ad 100644 --- a/apps/web/src/app/api/files/route.ts +++ b/apps/web/src/app/api/files/route.ts @@ -1,18 +1,18 @@ -import { NextResponse, type NextRequest } from "next/server"; -import { pinata } from '../../../utils/pinata' +import { type NextRequest, NextResponse } from "next/server"; +import { pinata } from "../../../utils/pinata"; export async function POST(request: NextRequest) { - try { - const data = await request.formData(); - const file: File | null = data.get("file") as unknown as File; - const uploadData = await pinata.upload.file(file) - const url = await pinata.gateways.convert(uploadData.IpfsHash) - return NextResponse.json(url, { status: 200 }); - } catch (e) { - console.log(e); - return NextResponse.json( - { error: "Internal Server Error" }, - { status: 500 } - ); - } + try { + const data = await request.formData(); + const file: File | null = data.get("file") as unknown as File; + const uploadData = await pinata.upload.file(file); + const url = await pinata.gateways.convert(uploadData.IpfsHash); + return NextResponse.json(url, { status: 200 }); + } catch (e) { + console.log(e); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } } diff --git a/apps/web/src/app/api/product/[id]/route.ts b/apps/web/src/app/api/product/[id]/route.ts index 9727590..a190170 100644 --- a/apps/web/src/app/api/product/[id]/route.ts +++ b/apps/web/src/app/api/product/[id]/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from "next/server"; -import { mockedProducts } from "~/server/api/routers/mockProducts"; +import { api } from "~/trpc/server"; export async function GET( request: Request, { params }: { params: { id: string } }, ) { const id = Number.parseInt(params.id); - const product = mockedProducts.find((p) => p.id === id); + const product = await api.product.getProductById({ id }); if (!product) { return NextResponse.json({ error: "Product not found" }, { status: 404 }); diff --git a/apps/web/src/app/product/[id]/page.tsx b/apps/web/src/app/product/[id]/page.tsx index e22f958..908c18f 100644 --- a/apps/web/src/app/product/[id]/page.tsx +++ b/apps/web/src/app/product/[id]/page.tsx @@ -8,6 +8,7 @@ import ProductDetails from "~/app/_components/features/ProductDetails"; interface Product { id: number; + tokenId: number; image: string; name: string; region: string; @@ -21,19 +22,21 @@ interface Product { } interface ApiResponse { - nftMetadata: string; + id: number; + tokenId: number; + nftMetadata: NftMetadata; name: string; - region: string; - farmName: string; - strength: string; bagsAvailable: number; price: number; - process?: string; + //process?: string; } interface NftMetadata { imageUrl: string; description: string; + region: string; + farmName: string; + strength: string; } function ProductPage() { @@ -58,20 +61,24 @@ function ProductPage() { } const data = (await response.json()) as ApiResponse; - const parsedMetadata = JSON.parse(data.nftMetadata) as NftMetadata; + const parsedMetadata: NftMetadata = + typeof data.nftMetadata === "string" + ? (JSON.parse(data.nftMetadata) as NftMetadata) + : data.nftMetadata; const product: Product = { id: Number(id), + tokenId: data.tokenId, image: parsedMetadata.imageUrl, name: data.name, - region: data.region, - farmName: data.farmName, - roastLevel: data.strength, + region: parsedMetadata.region, + farmName: parsedMetadata.farmName, + roastLevel: parsedMetadata.strength, bagsAvailable: data.bagsAvailable ?? 10, price: data.price, description: parsedMetadata.description, type: "Buyer", - process: data.process ?? "Natural", + process: "Natural", }; setProduct(product); diff --git a/apps/web/src/app/shopping-cart/page.tsx b/apps/web/src/app/shopping-cart/page.tsx index d1dc0ec..5b167cd 100644 --- a/apps/web/src/app/shopping-cart/page.tsx +++ b/apps/web/src/app/shopping-cart/page.tsx @@ -1,6 +1,8 @@ "use client"; import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { useAccount } from "@starknet-react/core"; +import { useProvider } from "@starknet-react/core"; import { useAtom, useAtomValue } from "jotai"; import Image from "next/image"; import { useRouter } from "next/navigation"; @@ -8,6 +10,14 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { cartItemsAtom, removeItemAtom } from "~/store/cartAtom"; import type { CartItem } from "~/store/cartAtom"; +import { + ContractsError, + ContractsInterface, + useCofiCollectionContract, + useMarketplaceContract, + useStarkContract, +} from "../../services/contractsInterface"; +//import { api } from "~/trpc/server"; interface DeleteModalProps { isOpen: boolean; @@ -59,6 +69,14 @@ export default function ShoppingCart() { const items = useAtomValue(cartItemsAtom); const [, removeItem] = useAtom(removeItemAtom); const [itemToDelete, setItemToDelete] = useState(null); + const { provider } = useProvider(); + const contract = new ContractsInterface( + useAccount(), + useCofiCollectionContract(), + useMarketplaceContract(), + useStarkContract(), + provider, + ); const { t } = useTranslation(); const handleRemove = (item: CartItem) => { setItemToDelete(item); @@ -75,6 +93,28 @@ export default function ShoppingCart() { setItemToDelete(null); }; + const handleBuy = async () => { + const token_ids = items.map((item) => item.tokenId); + const token_amounts = items.map((item) => item.quantity); + console.log("buying items", token_ids, token_amounts, totalPrice); + try { + const tx_hash = await contract.buy_product( + token_ids, + token_amounts, + totalPrice, + ); + alert(`Items bought successfully tx hash: ${tx_hash}`); + for (const item of items) { + removeItem(item.id); + } + } catch (error) { + if (error instanceof ContractsError) { + alert(error.message); + } + console.error("Error buying items:", error); + } + }; + const totalPrice = items.reduce( (total, item) => total + item.price * item.quantity, 0, @@ -153,6 +193,7 @@ export default function ShoppingCart() {
diff --git a/apps/web/src/app/user/register-coffee/page.tsx b/apps/web/src/app/user/register-coffee/page.tsx index 88efa5f..192988a 100644 --- a/apps/web/src/app/user/register-coffee/page.tsx +++ b/apps/web/src/app/user/register-coffee/page.tsx @@ -8,11 +8,20 @@ import { ClockIcon } from "@heroicons/react/24/solid"; import { zodResolver } from "@hookform/resolvers/zod"; import Button from "@repo/ui/button"; import RadioButton from "@repo/ui/form/radioButton"; +import { useAccount, useProvider } from "@starknet-react/core"; import Image from "next/image"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { z } from "zod"; import { ProfileOptionLayout } from "~/app/_components/features/ProfileOptionLayout"; +import { + ContractsError, + ContractsInterface, + useCofiCollectionContract, + useMarketplaceContract, + useStarkContract, +} from "~/services/contractsInterface"; +import { api } from "~/trpc/react"; import { RoastLevel } from "~/types"; const schema = z.object({ @@ -33,6 +42,15 @@ type FormData = z.infer; export default function RegisterCoffee() { const { t } = useTranslation(); + const { provider } = useProvider(); + const contracts = new ContractsInterface( + useAccount(), + useCofiCollectionContract(), + useMarketplaceContract(), + useStarkContract(), + provider, + ); + const mutation = api.product.createProduct.useMutation(); const handleImageUpload = () => { alert(t("implement_image_upload")); }; @@ -53,6 +71,34 @@ export default function RegisterCoffee() { }; // TODO: Implement coffee registration logic console.log(submissionData); + try { + const token_id = await contracts.register_product( + submissionData.price, + submissionData.bagsAvailable, + ); + await mutation.mutateAsync({ + tokenId: token_id, + name: submissionData.variety, + price: submissionData.price, + description: submissionData.description, + image: submissionData.image ?? "", + strength: submissionData.roast, + region: "", + farmName: "", + }); + alert("Product registered successfully"); + } catch (error) { + if (error instanceof ContractsError) { + if (error.code === ContractsError.USER_MISSING_ROLE) { + alert("User is not registered as a seller"); + } else if (error.code === ContractsError.USER_NOT_CONNECTED) { + alert("User is disconnected"); + } + } else { + console.log("error registering", error); + alert("An error occurred while registering the product"); + } + } }; return ( diff --git a/apps/web/src/atoms/productAtom.ts b/apps/web/src/atoms/productAtom.ts index c65116c..a679c30 100644 --- a/apps/web/src/atoms/productAtom.ts +++ b/apps/web/src/atoms/productAtom.ts @@ -2,6 +2,7 @@ import { atom } from "jotai"; interface Product { id: number; + tokenId: number; name: string; price: number; region: string; diff --git a/apps/web/src/contracts/configExternalContracts.ts b/apps/web/src/contracts/configExternalContracts.ts index 0746fb6..80cedef 100644 --- a/apps/web/src/contracts/configExternalContracts.ts +++ b/apps/web/src/contracts/configExternalContracts.ts @@ -3,6 +3,2410 @@ * You should not edit it manually or your changes might be overwritten. */ -const configExternalContracts = {} as const; +const configExternalContracts = { + sepolia: { + Marketplace: { + address: + "0x3b73e235a852f76cb01e014246c59db4623d7ecf5c68675f07df0e209cd0303", + classHash: + "0x44eb6074fa74a9dbddb65c52394cd1bab274e3bc22e1fc36c26c37686d1e440", + abi: [ + { + type: "impl", + name: "MarketplaceImpl", + interface_name: "contracts::marketplace::IMarketplace", + }, + { + type: "struct", + name: "core::integer::u256", + members: [ + { name: "low", type: "core::integer::u128" }, + { name: "high", type: "core::integer::u128" }, + ], + }, + { + type: "struct", + name: "core::array::Span::", + members: [ + { + name: "snapshot", + type: "@core::array::Array::", + }, + ], + }, + { + type: "struct", + name: "core::array::Span::", + members: [ + { name: "snapshot", type: "@core::array::Array::" }, + ], + }, + { + type: "interface", + name: "contracts::marketplace::IMarketplace", + items: [ + { + type: "function", + name: "assign_seller_role", + inputs: [ + { + name: "assignee", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "assign_consumer_role", + inputs: [ + { + name: "assignee", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "assign_admin_role", + inputs: [ + { + name: "assignee", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "buy_product", + inputs: [ + { name: "token_id", type: "core::integer::u256" }, + { name: "token_amount", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "buy_products", + inputs: [ + { + name: "token_ids", + type: "core::array::Span::", + }, + { + name: "token_amount", + type: "core::array::Span::", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "create_product", + inputs: [ + { name: "initial_stock", type: "core::integer::u256" }, + { name: "price", type: "core::integer::u256" }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "external", + }, + { + type: "function", + name: "create_products", + inputs: [ + { + name: "initial_stock", + type: "core::array::Span::", + }, + { + name: "price", + type: "core::array::Span::", + }, + ], + outputs: [{ type: "core::array::Span::" }], + state_mutability: "external", + }, + { + type: "function", + name: "delete_product", + inputs: [{ name: "token_id", type: "core::integer::u256" }], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "delete_products", + inputs: [ + { + name: "token_ids", + type: "core::array::Span::", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "claim_balance", + inputs: [ + { + name: "producer", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "view", + }, + { + type: "function", + name: "claim", + inputs: [], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "ERC1155ReceiverImpl", + interface_name: + "openzeppelin_token::erc1155::interface::IERC1155Receiver", + }, + { + type: "interface", + name: "openzeppelin_token::erc1155::interface::IERC1155Receiver", + items: [ + { + type: "function", + name: "on_erc1155_received", + inputs: [ + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "token_id", type: "core::integer::u256" }, + { name: "value", type: "core::integer::u256" }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + { + type: "function", + name: "on_erc1155_batch_received", + inputs: [ + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "token_ids", + type: "core::array::Span::", + }, + { + name: "values", + type: "core::array::Span::", + }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + ], + }, + { + type: "impl", + name: "SRC5Impl", + interface_name: "openzeppelin_introspection::interface::ISRC5", + }, + { + type: "enum", + name: "core::bool", + variants: [ + { name: "False", type: "()" }, + { name: "True", type: "()" }, + ], + }, + { + type: "interface", + name: "openzeppelin_introspection::interface::ISRC5", + items: [ + { + type: "function", + name: "supports_interface", + inputs: [{ name: "interface_id", type: "core::felt252" }], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + ], + }, + { + type: "impl", + name: "AccessControlImpl", + interface_name: + "openzeppelin_access::accesscontrol::interface::IAccessControl", + }, + { + type: "interface", + name: "openzeppelin_access::accesscontrol::interface::IAccessControl", + items: [ + { + type: "function", + name: "has_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "get_role_admin", + inputs: [{ name: "role", type: "core::felt252" }], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + { + type: "function", + name: "grant_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "revoke_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "renounce_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "constructor", + name: "constructor", + inputs: [ + { + name: "cofi_collection_address", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "admin", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "market_fee", type: "core::integer::u256" }, + ], + }, + { + type: "event", + name: "openzeppelin_token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::Event", + kind: "enum", + variants: [], + }, + { + type: "event", + name: "openzeppelin_introspection::src5::SRC5Component::Event", + kind: "enum", + variants: [], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleGranted", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleRevoked", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleAdminChanged", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "previous_admin_role", + type: "core::felt252", + kind: "data", + }, + { name: "new_admin_role", type: "core::felt252", kind: "data" }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::Event", + kind: "enum", + variants: [ + { + name: "RoleGranted", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleGranted", + kind: "nested", + }, + { + name: "RoleRevoked", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleRevoked", + kind: "nested", + }, + { + name: "RoleAdminChanged", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleAdminChanged", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded", + kind: "struct", + members: [ + { + name: "class_hash", + type: "core::starknet::class_hash::ClassHash", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event", + kind: "enum", + variants: [ + { + name: "Upgraded", + type: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::DeleteProduct", + kind: "struct", + members: [ + { name: "token_id", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::CreateProduct", + kind: "struct", + members: [ + { name: "token_id", type: "core::integer::u256", kind: "data" }, + { + name: "initial_stock", + type: "core::integer::u256", + kind: "data", + }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::UpdateStock", + kind: "struct", + members: [ + { name: "token_id", type: "core::integer::u256", kind: "data" }, + { name: "new_stock", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::BuyProduct", + kind: "struct", + members: [ + { name: "token_id", type: "core::integer::u256", kind: "data" }, + { name: "amount", type: "core::integer::u256", kind: "data" }, + { name: "price", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::BuyBatchProducts", + kind: "struct", + members: [ + { + name: "token_ids", + type: "core::array::Span::", + kind: "data", + }, + { + name: "token_amount", + type: "core::array::Span::", + kind: "data", + }, + { name: "total_price", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::PaymentSeller", + kind: "struct", + members: [ + { + name: "token_ids", + type: "core::array::Span::", + kind: "data", + }, + { + name: "seller", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { name: "payment", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "contracts::marketplace::Marketplace::Event", + kind: "enum", + variants: [ + { + name: "ERC1155ReceiverEvent", + type: "openzeppelin_token::erc1155::erc1155_receiver::ERC1155ReceiverComponent::Event", + kind: "flat", + }, + { + name: "SRC5Event", + type: "openzeppelin_introspection::src5::SRC5Component::Event", + kind: "flat", + }, + { + name: "AccessControlEvent", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::Event", + kind: "flat", + }, + { + name: "UpgradeableEvent", + type: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event", + kind: "flat", + }, + { + name: "DeleteProduct", + type: "contracts::marketplace::Marketplace::DeleteProduct", + kind: "nested", + }, + { + name: "CreateProduct", + type: "contracts::marketplace::Marketplace::CreateProduct", + kind: "nested", + }, + { + name: "UpdateStock", + type: "contracts::marketplace::Marketplace::UpdateStock", + kind: "nested", + }, + { + name: "BuyProduct", + type: "contracts::marketplace::Marketplace::BuyProduct", + kind: "nested", + }, + { + name: "BuyBatchProducts", + type: "contracts::marketplace::Marketplace::BuyBatchProducts", + kind: "nested", + }, + { + name: "PaymentSeller", + type: "contracts::marketplace::Marketplace::PaymentSeller", + kind: "nested", + }, + ], + }, + ], + }, + CofiCollection: { + address: + "0x2beb2106100c5238830e2d0e23193f581fd69e53b9c5d0f7eabf11c7d85db14", + classHash: + "0x7a108607024be2054d1558cbfd5de3fe264afe9e025bc7653b1bc5fc76d1af0", + abi: [ + { + type: "impl", + name: "UpgradeableImpl", + interface_name: "openzeppelin_upgrades::interface::IUpgradeable", + }, + { + type: "interface", + name: "openzeppelin_upgrades::interface::IUpgradeable", + items: [ + { + type: "function", + name: "upgrade", + inputs: [ + { + name: "new_class_hash", + type: "core::starknet::class_hash::ClassHash", + }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "function", + name: "pause", + inputs: [], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "unpause", + inputs: [], + outputs: [], + state_mutability: "external", + }, + { + type: "struct", + name: "core::integer::u256", + members: [ + { name: "low", type: "core::integer::u128" }, + { name: "high", type: "core::integer::u128" }, + ], + }, + { + type: "function", + name: "burn", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "token_id", type: "core::integer::u256" }, + { name: "value", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "struct", + name: "core::array::Span::", + members: [ + { + name: "snapshot", + type: "@core::array::Array::", + }, + ], + }, + { + type: "function", + name: "batch_burn", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "token_ids", + type: "core::array::Span::", + }, + { + name: "values", + type: "core::array::Span::", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "struct", + name: "core::array::Span::", + members: [ + { name: "snapshot", type: "@core::array::Array::" }, + ], + }, + { + type: "function", + name: "mint", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "token_id", type: "core::integer::u256" }, + { name: "value", type: "core::integer::u256" }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [], + state_mutability: "external", + }, + { + 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: "function", + name: "mint_item", + inputs: [ + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "token_id", type: "core::integer::u256" }, + { name: "value", type: "core::integer::u256" }, + { name: "data", type: "core::array::Span::" }, + { name: "uri", type: "core::byte_array::ByteArray" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "batch_mint", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "token_ids", + type: "core::array::Span::", + }, + { + name: "values", + type: "core::array::Span::", + }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "set_base_uri", + inputs: [{ name: "base_uri", type: "core::byte_array::ByteArray" }], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "set_minter", + inputs: [ + { + name: "minter", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "impl", + name: "ERC1155MixinImpl", + interface_name: "openzeppelin_token::erc1155::interface::ERC1155ABI", + }, + { + type: "struct", + name: "core::array::Span::", + members: [ + { + name: "snapshot", + type: "@core::array::Array::", + }, + ], + }, + { + type: "enum", + name: "core::bool", + variants: [ + { name: "False", type: "()" }, + { name: "True", type: "()" }, + ], + }, + { + type: "interface", + name: "openzeppelin_token::erc1155::interface::ERC1155ABI", + items: [ + { + type: "function", + name: "balance_of", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "token_id", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "view", + }, + { + type: "function", + name: "balance_of_batch", + inputs: [ + { + name: "accounts", + type: "core::array::Span::", + }, + { + name: "token_ids", + type: "core::array::Span::", + }, + ], + outputs: [{ type: "core::array::Span::" }], + state_mutability: "view", + }, + { + type: "function", + name: "safe_transfer_from", + inputs: [ + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "token_id", type: "core::integer::u256" }, + { name: "value", type: "core::integer::u256" }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "safe_batch_transfer_from", + inputs: [ + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "token_ids", + type: "core::array::Span::", + }, + { + name: "values", + type: "core::array::Span::", + }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "is_approved_for_all", + inputs: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "set_approval_for_all", + inputs: [ + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "approved", type: "core::bool" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "supports_interface", + inputs: [{ name: "interface_id", type: "core::felt252" }], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "uri", + inputs: [{ name: "token_id", type: "core::integer::u256" }], + outputs: [{ type: "core::byte_array::ByteArray" }], + state_mutability: "view", + }, + { + type: "function", + name: "balanceOf", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "tokenId", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "view", + }, + { + type: "function", + name: "balanceOfBatch", + inputs: [ + { + name: "accounts", + type: "core::array::Span::", + }, + { + name: "tokenIds", + type: "core::array::Span::", + }, + ], + outputs: [{ type: "core::array::Span::" }], + state_mutability: "view", + }, + { + type: "function", + name: "safeTransferFrom", + inputs: [ + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "tokenId", type: "core::integer::u256" }, + { name: "value", type: "core::integer::u256" }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "safeBatchTransferFrom", + inputs: [ + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "tokenIds", + type: "core::array::Span::", + }, + { + name: "values", + type: "core::array::Span::", + }, + { name: "data", type: "core::array::Span::" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "isApprovedForAll", + inputs: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "setApprovalForAll", + inputs: [ + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "approved", type: "core::bool" }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "PausableImpl", + interface_name: "openzeppelin_security::interface::IPausable", + }, + { + type: "interface", + name: "openzeppelin_security::interface::IPausable", + items: [ + { + type: "function", + name: "is_paused", + inputs: [], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + ], + }, + { + type: "impl", + name: "AccessControlImpl", + interface_name: + "openzeppelin_access::accesscontrol::interface::IAccessControl", + }, + { + type: "interface", + name: "openzeppelin_access::accesscontrol::interface::IAccessControl", + items: [ + { + type: "function", + name: "has_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "get_role_admin", + inputs: [{ name: "role", type: "core::felt252" }], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + { + type: "function", + name: "grant_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "revoke_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "renounce_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "AccessControlCamelImpl", + interface_name: + "openzeppelin_access::accesscontrol::interface::IAccessControlCamel", + }, + { + type: "interface", + name: "openzeppelin_access::accesscontrol::interface::IAccessControlCamel", + items: [ + { + type: "function", + name: "hasRole", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "getRoleAdmin", + inputs: [{ name: "role", type: "core::felt252" }], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + { + type: "function", + name: "grantRole", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "revokeRole", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "renounceRole", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "constructor", + name: "constructor", + inputs: [ + { + name: "default_admin", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "pauser", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "minter", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "uri_setter", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "upgrader", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + }, + { + type: "event", + name: "openzeppelin_token::erc1155::erc1155::ERC1155Component::TransferSingle", + kind: "struct", + members: [ + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { name: "id", type: "core::integer::u256", kind: "data" }, + { name: "value", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "openzeppelin_token::erc1155::erc1155::ERC1155Component::TransferBatch", + kind: "struct", + members: [ + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "ids", + type: "core::array::Span::", + kind: "data", + }, + { + name: "values", + type: "core::array::Span::", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_token::erc1155::erc1155::ERC1155Component::ApprovalForAll", + kind: "struct", + members: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { + name: "operator", + type: "core::starknet::contract_address::ContractAddress", + kind: "key", + }, + { name: "approved", type: "core::bool", kind: "data" }, + ], + }, + { + type: "event", + name: "openzeppelin_token::erc1155::erc1155::ERC1155Component::URI", + kind: "struct", + members: [ + { + name: "value", + type: "core::byte_array::ByteArray", + kind: "data", + }, + { name: "id", type: "core::integer::u256", kind: "key" }, + ], + }, + { + type: "event", + name: "openzeppelin_token::erc1155::erc1155::ERC1155Component::Event", + kind: "enum", + variants: [ + { + name: "TransferSingle", + type: "openzeppelin_token::erc1155::erc1155::ERC1155Component::TransferSingle", + kind: "nested", + }, + { + name: "TransferBatch", + type: "openzeppelin_token::erc1155::erc1155::ERC1155Component::TransferBatch", + kind: "nested", + }, + { + name: "ApprovalForAll", + type: "openzeppelin_token::erc1155::erc1155::ERC1155Component::ApprovalForAll", + kind: "nested", + }, + { + name: "URI", + type: "openzeppelin_token::erc1155::erc1155::ERC1155Component::URI", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "openzeppelin_introspection::src5::SRC5Component::Event", + kind: "enum", + variants: [], + }, + { + type: "event", + name: "openzeppelin_security::pausable::PausableComponent::Paused", + kind: "struct", + members: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_security::pausable::PausableComponent::Unpaused", + kind: "struct", + members: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_security::pausable::PausableComponent::Event", + kind: "enum", + variants: [ + { + name: "Paused", + type: "openzeppelin_security::pausable::PausableComponent::Paused", + kind: "nested", + }, + { + name: "Unpaused", + type: "openzeppelin_security::pausable::PausableComponent::Unpaused", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleGranted", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleRevoked", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleAdminChanged", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "previous_admin_role", + type: "core::felt252", + kind: "data", + }, + { name: "new_admin_role", type: "core::felt252", kind: "data" }, + ], + }, + { + type: "event", + name: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::Event", + kind: "enum", + variants: [ + { + name: "RoleGranted", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleGranted", + kind: "nested", + }, + { + name: "RoleRevoked", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleRevoked", + kind: "nested", + }, + { + name: "RoleAdminChanged", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleAdminChanged", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded", + kind: "struct", + members: [ + { + name: "class_hash", + type: "core::starknet::class_hash::ClassHash", + kind: "data", + }, + ], + }, + { + type: "event", + name: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event", + kind: "enum", + variants: [ + { + name: "Upgraded", + type: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded", + kind: "nested", + }, + ], + }, + { + type: "event", + name: "contracts::cofi_collection::CofiCollection::Event", + kind: "enum", + variants: [ + { + name: "ERC1155Event", + type: "openzeppelin_token::erc1155::erc1155::ERC1155Component::Event", + kind: "flat", + }, + { + name: "SRC5Event", + type: "openzeppelin_introspection::src5::SRC5Component::Event", + kind: "flat", + }, + { + name: "PausableEvent", + type: "openzeppelin_security::pausable::PausableComponent::Event", + kind: "flat", + }, + { + name: "AccessControlEvent", + type: "openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::Event", + kind: "flat", + }, + { + name: "UpgradeableEvent", + type: "openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event", + kind: "flat", + }, + ], + }, + ], + }, + stark: { + address: + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + classHash: + "0x4ad3c1dc8413453db314497945b6903e1c766495a1e60492d44da9c2a986e4b", + abi: [ + { + type: "impl", + name: "LockingContract", + interface_name: "src::mintable_lock_interface::ILockingContract", + }, + { + type: "interface", + name: "src::mintable_lock_interface::ILockingContract", + items: [ + { + type: "function", + name: "set_locking_contract", + inputs: [ + { + name: "locking_contract", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "get_locking_contract", + inputs: [], + outputs: [ + { type: "core::starknet::contract_address::ContractAddress" }, + ], + state_mutability: "view", + }, + ], + }, + { + type: "impl", + name: "LockAndDelegate", + interface_name: "src::mintable_lock_interface::ILockAndDelegate", + }, + { + type: "struct", + name: "core::integer::u256", + members: [ + { name: "low", type: "core::integer::u128" }, + { name: "high", type: "core::integer::u128" }, + ], + }, + { + type: "interface", + name: "src::mintable_lock_interface::ILockAndDelegate", + items: [ + { + type: "function", + name: "lock_and_delegate", + inputs: [ + { + name: "delegatee", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "lock_and_delegate_by_sig", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "delegatee", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + { name: "nonce", type: "core::felt252" }, + { name: "expiry", type: "core::integer::u64" }, + { + name: "signature", + type: "core::array::Array::", + }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "MintableToken", + interface_name: "src::mintable_token_interface::IMintableToken", + }, + { + type: "interface", + name: "src::mintable_token_interface::IMintableToken", + items: [ + { + type: "function", + name: "permissioned_mint", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "permissioned_burn", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "MintableTokenCamelImpl", + interface_name: "src::mintable_token_interface::IMintableTokenCamel", + }, + { + type: "interface", + name: "src::mintable_token_interface::IMintableTokenCamel", + items: [ + { + type: "function", + name: "permissionedMint", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "permissionedBurn", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "Replaceable", + interface_name: "src::replaceability_interface::IReplaceable", + }, + { + type: "struct", + name: "core::array::Span::", + members: [ + { name: "snapshot", type: "@core::array::Array::" }, + ], + }, + { + type: "struct", + name: "src::replaceability_interface::EICData", + members: [ + { name: "eic_hash", type: "core::starknet::class_hash::ClassHash" }, + { + name: "eic_init_data", + type: "core::array::Span::", + }, + ], + }, + { + type: "enum", + name: "core::option::Option::", + variants: [ + { name: "Some", type: "src::replaceability_interface::EICData" }, + { name: "None", type: "()" }, + ], + }, + { + type: "enum", + name: "core::bool", + variants: [ + { name: "False", type: "()" }, + { name: "True", type: "()" }, + ], + }, + { + type: "struct", + name: "src::replaceability_interface::ImplementationData", + members: [ + { + name: "impl_hash", + type: "core::starknet::class_hash::ClassHash", + }, + { + name: "eic_data", + type: "core::option::Option::", + }, + { name: "final", type: "core::bool" }, + ], + }, + { + type: "interface", + name: "src::replaceability_interface::IReplaceable", + items: [ + { + type: "function", + name: "get_upgrade_delay", + inputs: [], + outputs: [{ type: "core::integer::u64" }], + state_mutability: "view", + }, + { + type: "function", + name: "get_impl_activation_time", + inputs: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + }, + ], + outputs: [{ type: "core::integer::u64" }], + state_mutability: "view", + }, + { + type: "function", + name: "add_new_implementation", + inputs: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "remove_implementation", + inputs: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "replace_to", + inputs: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + }, + ], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "AccessControlImplExternal", + interface_name: "src::access_control_interface::IAccessControl", + }, + { + type: "interface", + name: "src::access_control_interface::IAccessControl", + items: [ + { + type: "function", + name: "has_role", + inputs: [ + { name: "role", type: "core::felt252" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "get_role_admin", + inputs: [{ name: "role", type: "core::felt252" }], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + ], + }, + { + type: "impl", + name: "RolesImpl", + interface_name: "src::roles_interface::IMinimalRoles", + }, + { + type: "interface", + name: "src::roles_interface::IMinimalRoles", + items: [ + { + type: "function", + name: "is_governance_admin", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "is_upgrade_governor", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "view", + }, + { + type: "function", + name: "register_governance_admin", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "remove_governance_admin", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "register_upgrade_governor", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "remove_upgrade_governor", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [], + state_mutability: "external", + }, + { + type: "function", + name: "renounce", + inputs: [{ name: "role", type: "core::felt252" }], + outputs: [], + state_mutability: "external", + }, + ], + }, + { + type: "impl", + name: "ERC20Impl", + interface_name: "openzeppelin::token::erc20::interface::IERC20", + }, + { + type: "interface", + name: "openzeppelin::token::erc20::interface::IERC20", + items: [ + { + type: "function", + name: "name", + inputs: [], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + { + type: "function", + name: "symbol", + inputs: [], + outputs: [{ type: "core::felt252" }], + state_mutability: "view", + }, + { + type: "function", + name: "decimals", + inputs: [], + outputs: [{ type: "core::integer::u8" }], + state_mutability: "view", + }, + { + 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: "ERC20CamelOnlyImpl", + interface_name: + "openzeppelin::token::erc20::interface::IERC20CamelOnly", + }, + { + type: "interface", + name: "openzeppelin::token::erc20::interface::IERC20CamelOnly", + items: [ + { + type: "function", + name: "totalSupply", + inputs: [], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "view", + }, + { + type: "function", + name: "balanceOf", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "view", + }, + { + type: "function", + name: "transferFrom", + 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: "constructor", + name: "constructor", + inputs: [ + { name: "name", type: "core::felt252" }, + { name: "symbol", type: "core::felt252" }, + { name: "decimals", type: "core::integer::u8" }, + { name: "initial_supply", type: "core::integer::u256" }, + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "permitted_minter", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "provisional_governance_admin", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "upgrade_delay", type: "core::integer::u64" }, + ], + }, + { + type: "function", + name: "increase_allowance", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "added_value", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "external", + }, + { + type: "function", + name: "decrease_allowance", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "subtracted_value", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "external", + }, + { + type: "function", + name: "increaseAllowance", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "addedValue", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "external", + }, + { + type: "function", + name: "decreaseAllowance", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "subtractedValue", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "external", + }, + { + type: "event", + name: "src::strk::erc20_lockable::ERC20Lockable::Transfer", + kind: "struct", + members: [ + { + name: "from", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "to", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { name: "value", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "src::strk::erc20_lockable::ERC20Lockable::Approval", + kind: "struct", + members: [ + { + name: "owner", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { name: "value", type: "core::integer::u256", kind: "data" }, + ], + }, + { + type: "event", + name: "src::replaceability_interface::ImplementationAdded", + kind: "struct", + members: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::replaceability_interface::ImplementationRemoved", + kind: "struct", + members: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::replaceability_interface::ImplementationReplaced", + kind: "struct", + members: [ + { + name: "implementation_data", + type: "src::replaceability_interface::ImplementationData", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::replaceability_interface::ImplementationFinalized", + kind: "struct", + members: [ + { + name: "impl_hash", + type: "core::starknet::class_hash::ClassHash", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::access_control_interface::RoleGranted", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::access_control_interface::RoleRevoked", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "sender", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::access_control_interface::RoleAdminChanged", + kind: "struct", + members: [ + { name: "role", type: "core::felt252", kind: "data" }, + { + name: "previous_admin_role", + type: "core::felt252", + kind: "data", + }, + { name: "new_admin_role", type: "core::felt252", kind: "data" }, + ], + }, + { + type: "event", + name: "src::roles_interface::GovernanceAdminAdded", + kind: "struct", + members: [ + { + name: "added_account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "added_by", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::roles_interface::GovernanceAdminRemoved", + kind: "struct", + members: [ + { + name: "removed_account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "removed_by", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::roles_interface::UpgradeGovernorAdded", + kind: "struct", + members: [ + { + name: "added_account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "added_by", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::roles_interface::UpgradeGovernorRemoved", + kind: "struct", + members: [ + { + name: "removed_account", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + { + name: "removed_by", + type: "core::starknet::contract_address::ContractAddress", + kind: "data", + }, + ], + }, + { + type: "event", + name: "src::strk::erc20_lockable::ERC20Lockable::Event", + kind: "enum", + variants: [ + { + name: "Transfer", + type: "src::strk::erc20_lockable::ERC20Lockable::Transfer", + kind: "nested", + }, + { + name: "Approval", + type: "src::strk::erc20_lockable::ERC20Lockable::Approval", + kind: "nested", + }, + { + name: "ImplementationAdded", + type: "src::replaceability_interface::ImplementationAdded", + kind: "nested", + }, + { + name: "ImplementationRemoved", + type: "src::replaceability_interface::ImplementationRemoved", + kind: "nested", + }, + { + name: "ImplementationReplaced", + type: "src::replaceability_interface::ImplementationReplaced", + kind: "nested", + }, + { + name: "ImplementationFinalized", + type: "src::replaceability_interface::ImplementationFinalized", + kind: "nested", + }, + { + name: "RoleGranted", + type: "src::access_control_interface::RoleGranted", + kind: "nested", + }, + { + name: "RoleRevoked", + type: "src::access_control_interface::RoleRevoked", + kind: "nested", + }, + { + name: "RoleAdminChanged", + type: "src::access_control_interface::RoleAdminChanged", + kind: "nested", + }, + { + name: "GovernanceAdminAdded", + type: "src::roles_interface::GovernanceAdminAdded", + kind: "nested", + }, + { + name: "GovernanceAdminRemoved", + type: "src::roles_interface::GovernanceAdminRemoved", + kind: "nested", + }, + { + name: "UpgradeGovernorAdded", + type: "src::roles_interface::UpgradeGovernorAdded", + kind: "nested", + }, + { + name: "UpgradeGovernorRemoved", + type: "src::roles_interface::UpgradeGovernorRemoved", + kind: "nested", + }, + ], + }, + ], + }, + }, +} as const; export default configExternalContracts; diff --git a/apps/web/src/server/api/routers/mockProducts.ts b/apps/web/src/server/api/routers/mockProducts.ts index 372e5af..14b67d1 100644 --- a/apps/web/src/server/api/routers/mockProducts.ts +++ b/apps/web/src/server/api/routers/mockProducts.ts @@ -5,6 +5,7 @@ export const mockedProducts = [ { id: 1, + tokenId: 1, name: "product_name_1", price: 15.99, region: "Alajuela", @@ -21,6 +22,7 @@ export const mockedProducts = [ }, { id: 2, + tokenId: 2, name: "product_name_2", price: 17.99, region: "Cartago", @@ -37,6 +39,7 @@ export const mockedProducts = [ }, { id: 3, + tokenId: 3, name: "product_name_3", price: 19.99, region: "Heredia", @@ -53,6 +56,7 @@ export const mockedProducts = [ }, { id: 4, + tokenId: 4, name: "product_name_4", price: 16.99, region: "Guanacaste", @@ -68,6 +72,7 @@ export const mockedProducts = [ }, { id: 5, + tokenId: 5, name: "product_name_5", price: 18.99, region: "Puntarenas", @@ -83,6 +88,7 @@ export const mockedProducts = [ }, { id: 6, + tokenId: 6, name: "product_name_6", price: 20.99, region: "San JosΓ©", @@ -98,6 +104,7 @@ export const mockedProducts = [ }, { id: 7, + tokenId: 7, name: "product_name_7", price: 14.99, region: "LimΓ³n", diff --git a/apps/web/src/server/api/routers/product.ts b/apps/web/src/server/api/routers/product.ts index 4ea0544..55123d4 100755 --- a/apps/web/src/server/api/routers/product.ts +++ b/apps/web/src/server/api/routers/product.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { db } from "~/server/db"; import { mockedProducts } from "./mockProducts"; // TODO: Replace mockedProducts with real data fetched from the blockchain in the near future. @@ -18,27 +19,116 @@ export const productRouter = createTRPCRouter({ cursor: z.number().optional(), }), ) - .query(({ input }) => { + .query(async ({ input }) => { const { limit, cursor } = input; - let startIndex = 0; - if (cursor) { - const index = mockedProducts.findIndex( - (product) => product.id === cursor, - ); - startIndex = index >= 0 ? index + 1 : 0; - } + // Fetch products from the database using Prisma + const products = await db.product.findMany({ + take: limit, // Limit the number of products + skip: cursor ? 1 : 0, // Skip if cursor is provided + cursor: cursor ? { id: cursor } : undefined, // Cursor-based pagination + orderBy: { id: "asc" }, // Order products by ID ascending + }); - const products = mockedProducts.slice(startIndex, startIndex + limit); + // Determine next cursor for pagination const nextCursor = products.length === limit ? products[products.length - 1]?.id : null; return { - products, + products: products, nextCursor, }; }), + getProductById: publicProcedure + .input( + z.object({ + id: z.number().min(1), + }), + ) + .query(async ({ input }) => { + try { + const product = await db.product.findUnique({ + where: { id: input.id }, + }); + + if (!product) { + throw new Error("Product not found"); + } + + return product; + } catch (error) { + console.error("Error fetching product:", error); + throw new Error("Failed to fetch product"); + } + }), + + createProduct: publicProcedure + .input( + z.object({ + tokenId: z.number(), + name: z.string().min(1), + price: z.number().min(0), + description: z.string().min(1), + image: z + .string() + .optional() + .refine( + (val) => val === "" || z.string().url().safeParse(val).success, + { message: "Invalid URL" }, + ), + strength: z.string().min(1), + region: z.string().optional(), + farmName: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + try { + const newProduct = await db.product.create({ + data: { + tokenId: input.tokenId, + name: input.name, + price: input.price, + nftMetadata: JSON.stringify({ + description: input.description, + imageUrl: input.image, + imageAlt: input.image, + region: input.region ?? "", + farmName: input.farmName ?? "", + strength: input.strength, + }), + }, + }); + return { success: true, product: newProduct }; + } catch (error) { + console.error("Error creating product:", error); + throw new Error("Failed to create product"); + } + }), + + // updateProductStock: publicProcedure + // .input( + // z.object({ + // id: z.number().min(1), + // buy_amount: z.number().min(0), + // }), + // ) + // .mutation(async ({ input }) => { + // try { + // const updatedProduct = await db.product.update({ + // where: { id: input.id }, + // data: { + // orderItems: {order} + // }, + // }); + + // return { success: true, product: updatedProduct }; + // } catch (error) { + // console.error("Error updating product stock:", error); + // throw new Error("Failed to update product stock"); + // } + // }), + searchProductCatalog: publicProcedure .input( z.object({ diff --git a/apps/web/src/server/db.ts b/apps/web/src/server/db.ts index 8b2507b..f3d23c7 100644 --- a/apps/web/src/server/db.ts +++ b/apps/web/src/server/db.ts @@ -1,5 +1,4 @@ import { PrismaClient } from "@prisma/client"; - import { env } from "~/env"; const createPrismaClient = () => @@ -15,3 +14,35 @@ const globalForPrisma = globalThis as unknown as { export const db = globalForPrisma.prisma ?? createPrismaClient(); if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; + +export const productService = { + async createProduct( + tokenId: number, + name: string, + price: number, + description: string, + image: string, + strength: string, + ) { + try { + return await db.product.create({ + data: { + name: name, + tokenId: tokenId, + price: price, + nftMetadata: JSON.stringify({ + description: description, + imageUrl: image, + imageAlt: image, + region: "", + farmName: "", + strength: strength, + }), + }, + }); + } catch (error) { + console.error("Error creating product:", error); + throw new Error("Database error occurred"); + } + }, +}; diff --git a/apps/web/src/services/contractsInterface.ts b/apps/web/src/services/contractsInterface.ts new file mode 100644 index 0000000..18bb7a6 --- /dev/null +++ b/apps/web/src/services/contractsInterface.ts @@ -0,0 +1,325 @@ +import { useContract } from "@starknet-react/core"; +import type { UseAccountResult } from "@starknet-react/core"; +import type { Abi, Contract, ProviderInterface } from "starknet"; +import { CallData, LibraryError } from "starknet"; +import configExternalContracts from "../contracts/configExternalContracts"; +import { parseEvents } from "../utils/parseEvents"; + +class ContractsError extends Error { + static USER_MISSING_ROLE = 1; + static USER_NOT_CONNECTED = 2; + static UNABLE_TO_BUY = 3; + code: number; + constructor(message: string, code: number) { + super(message); + this.name = "ContractsError"; + this.code = code; + Error.captureStackTrace(this, ContractsError); + } +} + +type CoinGeckoResponse = { + starknet: { + usd: number; + }; +}; + +const useCofiCollectionContract = () => { + const env = (process.env.STARKNET_ENV ?? + "sepolia") as keyof typeof configExternalContracts; + const { contract } = useContract({ + abi: configExternalContracts[env].CofiCollection.abi as Abi, + address: configExternalContracts[env].CofiCollection.address, + }); + return contract; +}; + +const useMarketplaceContract = () => { + const env = (process.env.STARKNET_ENV ?? + "sepolia") as keyof typeof configExternalContracts; + const { contract } = useContract({ + abi: configExternalContracts[env].Marketplace.abi as Abi, + address: configExternalContracts[env].Marketplace.address, + }); + + return contract; +}; +const useStarkContract = () => { + const env = (process.env.STARKNET_ENV ?? + "sepolia") as keyof typeof configExternalContracts; + const { contract } = useContract({ + abi: configExternalContracts[env].stark.abi as Abi, + address: configExternalContracts[env].stark.address, + }); + + return contract; +}; + +class ContractsInterface { + cofiCollectionContract: Contract | null; + marketplaceContract: Contract | null; + starkContract: Contract | null; + account: UseAccountResult; + provider: ProviderInterface; + + constructor( + account: UseAccountResult, + cofiCollection: Contract | undefined, + marketplace: Contract | undefined, + stark: Contract | undefined, + provider: ProviderInterface, + ) { + if (!account) { + throw new ContractsError( + "User is not connected", + ContractsError.USER_NOT_CONNECTED, + ); + } + this.cofiCollectionContract = cofiCollection ?? null; + this.marketplaceContract = marketplace ?? null; + this.starkContract = stark ?? null; + this.account = account; + this.provider = provider; + } + + get_user_address() { + if (this.account.address === undefined) { + throw new ContractsError( + "User is not connected", + ContractsError.USER_NOT_CONNECTED, + ); + } + return this.account.address; + } + + connect_account() { + if (!this.account.account) { + throw new ContractsError( + "User is not connected", + ContractsError.USER_NOT_CONNECTED, + ); + } + this.cofiCollectionContract?.connect(this.account.account); + this.marketplaceContract?.connect(this.account.account); + this.starkContract?.connect(this.account.account); + } + + async getStarkPrice() { + try { + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=starknet&vs_currencies=usd", + ); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = (await response.json()) as CoinGeckoResponse; + return data.starknet.usd; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Error fetching starknet price data: ${error.message}`); + } + throw new Error("Error fetching starknet price data"); + } + } + + async get_balance_of(token_id: string) { + const address = this.get_user_address(); + if (!this.cofiCollectionContract) { + throw new Error("Cofi collection contract is not loaded"); + } + const balance = await this.cofiCollectionContract.call( + "balanceOf", + CallData.compile([address, token_id, "0x0"]), + ); + return balance; + } + + async register_product(price_usd: number, initial_stock: number) { + // connect user account to contracts + this.connect_account(); + + // get the current stark price in usd to convert the price to starks + const stark_price_usd = await this.getStarkPrice(); + const price = Math.floor(price_usd / stark_price_usd) * 1000000000000000000; + + // Call the contract to register the product + if (!this.marketplaceContract) { + throw new Error("Marketplace contract is not loaded"); + } + try { + // each u256 requires two words, so we append 0x0 at the end of each u256 + const tx = await this.marketplaceContract.invoke( + "create_product", + CallData.compile([initial_stock, "0x0", price, "0x0", "0x1", "0x0"]), + { maxFee: "10000000000000000000000" }, + ); + console.log("tx hash is", tx.transaction_hash); + const txReceipt = await this.provider.waitForTransaction( + tx.transaction_hash, + { + retryInterval: 100, + }, + ); + let token_id = 0; + if (txReceipt?.isSuccess()) { + const events = parseEvents(txReceipt.events); + const createProductEvents = events.createProductEvents; + if (createProductEvents.length > 0) { + token_id = createProductEvents[0]?.token_id ?? 0; + } + } + console.log("token_id is", token_id); + return token_id; + } catch (error) { + if ( + error instanceof LibraryError && + error.message.includes("Caller is missing rol") + ) { + throw new ContractsError( + "User is not registered as a seller", + ContractsError.USER_MISSING_ROLE, + ); + } + throw error; + } + } + + _get_formatted_u256_list(input: number[]) { + const calldata = [`0x${input.length.toString(16)}`]; + for (const value of input) { + calldata.push(`0x${value.toString(16)}`); + calldata.push("0x0"); // add padding since its a u256 + } + return calldata; + } + + async get_user_allowance_on_marketplace(token_amount: number) { + if (!this.starkContract) { + throw new Error("Stark contract is not loaded"); + } + if (!this.marketplaceContract) { + throw new Error("Marketplace contract is not loaded"); + } + const tx = await this.starkContract.invoke( + "approve", + CallData.compile([ + this.marketplaceContract.address, + `0x${token_amount.toString(16)}`, + "0x0", + ]), + ); + return tx.transaction_hash; + } + + async buy_product( + token_ids: number[], + token_amounts: number[], + price_usd: number, + ) { + // connect user account to contracts + this.connect_account(); + + // Call the contract to register the product + if (!this.marketplaceContract) { + throw new Error("Marketplace contract is not loaded"); + } + + // Get allowance to spend this amount of tokens + const stark_price_usd = await this.getStarkPrice(); + console.log("stark price is", stark_price_usd); + // adding some more starks to pay for the market fee and gas fee + const price = + Math.floor(price_usd / stark_price_usd) * 1000000000000000000 + + 10000000000000000000; + const approve_tx_hash = await this.get_user_allowance_on_marketplace(price); + console.log("approve_tx_hash is", approve_tx_hash); + try { + // each u256 requires two words, so we append 0x0 at the end of each u256 + const calldata = [ + ...this._get_formatted_u256_list(token_ids), + ...this._get_formatted_u256_list(token_amounts), + ]; + const tx = await this.marketplaceContract.invoke( + "buy_products", + CallData.compile(calldata), + { maxFee: "10000000000000000000000" }, + ); + console.log("tx hash is", tx.transaction_hash); + const txReceipt = await this.provider.waitForTransaction( + tx.transaction_hash, + { + retryInterval: 100, + }, + ); + if (txReceipt?.isSuccess()) { + const events = parseEvents(txReceipt.events); + if (events.buyProductsEvents.length === 0) { + throw new ContractsError( + "Buy did not finish successfully", + ContractsError.UNABLE_TO_BUY, + ); + } + } + return tx.transaction_hash; + } catch (error) { + if ( + error instanceof LibraryError && + error.message.includes("Caller is missing rol") + ) { + throw new ContractsError( + "User is not registered as a buyer", + ContractsError.USER_MISSING_ROLE, + ); + } + throw error; + } + } + + async claim() { + // connect user account to contracts + this.connect_account(); + + // Call the contract to register the product + if (!this.marketplaceContract) { + throw new Error("Marketplace contract is not loaded"); + } + + try { + const tx = await this.marketplaceContract.invoke( + "claim", + CallData.compile([]), + { maxFee: "10000000000000000000000" }, + ); + console.log("tx hash is", tx.transaction_hash); + const txReceipt = await this.provider.waitForTransaction( + tx.transaction_hash, + { + retryInterval: 100, + }, + ); + if (txReceipt?.isSuccess()) { + console.log("claim success", txReceipt.events); + } + return tx.transaction_hash; + } catch (error) { + if ( + error instanceof LibraryError && + error.message.includes("Caller is missing rol") + ) { + throw new ContractsError( + "User is not registered as a producer", + ContractsError.USER_MISSING_ROLE, + ); + } + throw error; + } + } +} + +export { + ContractsInterface, + ContractsError, + useCofiCollectionContract, + useMarketplaceContract, + useStarkContract, +}; diff --git a/apps/web/src/store/cartAtom.ts b/apps/web/src/store/cartAtom.ts index 22fd05c..1711891 100644 --- a/apps/web/src/store/cartAtom.ts +++ b/apps/web/src/store/cartAtom.ts @@ -3,6 +3,7 @@ import { atomWithStorage } from "jotai/utils"; export interface CartItem { id: string; + tokenId: number; name: string; quantity: number; price: number; diff --git a/apps/web/src/stories/NftCard.stories.tsx b/apps/web/src/stories/NftCard.stories.tsx index bb77f30..3f2d359 100644 --- a/apps/web/src/stories/NftCard.stories.tsx +++ b/apps/web/src/stories/NftCard.stories.tsx @@ -1,5 +1,5 @@ import NFTCard from "@repo/ui/nftCard"; -import { type Meta, StoryFn, type StoryObj } from "@storybook/react"; +import type { Meta, StoryObj } from "@storybook/react"; import type React from "react"; export default { diff --git a/apps/web/src/utils/parseEvents.ts b/apps/web/src/utils/parseEvents.ts new file mode 100644 index 0000000..f8dbd33 --- /dev/null +++ b/apps/web/src/utils/parseEvents.ts @@ -0,0 +1,99 @@ +const CREATE_PRODUCT = + "0x3445cfe36cb1f05fc6459c6435ec98cafeb71c3d835b976a5e24260b1a82f3"; +const BUY_BATCH_PRODUCTS = + "0xd0aa475e7a5c79fefd7165ca354e8195bdeaa90a6704c74ac1bb971bf5d02c"; + +type CreateProductEvent = { + token_id: number; + initial_stock: number; +}; + +type BuyProductsEvent = { + token_ids: number[]; + token_amounts: number[]; + total_price: number; +}; + +class ParsedEvents { + createProductEvents: CreateProductEvent[] = []; + buyProductsEvents: BuyProductsEvent[] = []; + + constructor() { + this.createProductEvents = []; + } + + add_create_product_events(event: CreateProductEvent) { + this.createProductEvents.push(event); + } + + add_buy_products_events(event: BuyProductsEvent) { + this.buyProductsEvents.push(event); + } +} + +type EventData = { + keys: string[]; + data: string[]; +}; + +const parseEvents = (events: EventData[]) => { + const result = new ParsedEvents(); + for (const event of events) { + if (event.keys[0] === CREATE_PRODUCT) { + result.add_create_product_events(parseCreateProduct(event)); + } else if (event.keys[0] === BUY_BATCH_PRODUCTS) { + result.add_buy_products_events(parseBuyProducts(event)); + } + } + return result; +}; + +const parseCreateProduct = (event: EventData) => { + try { + return { + token_id: event.data[0] ? Number.parseInt(event.data[0], 16) : 0, + initial_stock: event.data[2] ? Number.parseInt(event.data[2], 16) : 0, + }; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Error parsing create product event: ${error}`); + } + throw new Error("Error parsing create product event"); + } +}; + +const parseBuyProducts = (event: EventData) => { + const token_ids = []; + const token_amounts = []; + let price = 0; + try { + const array_len = event.data[0] ? Number.parseInt(event.data[0], 16) : 0; + for (let i = 1; i < event.data.length; i += 2) { + let data = event.data[i]; + if (!data) { + data = "0"; + } + if (token_ids.length < array_len) { + const token_id = Number.parseInt(data, 16); + token_ids.push(token_id); + } else if (token_amounts.length < array_len) { + const token_amount = Number.parseInt(data, 16); + token_amounts.push(token_amount); + } else { + price = price + Number.parseInt(data, 16); + } + } + } catch (error) { + if (error instanceof Error) { + throw new Error(`Error parsing buy product event: ${error}`); + } + throw new Error("Error parsing create product event"); + } + return { + token_ids: token_ids, + token_amounts: token_amounts, + total_price: price, + }; +}; + +export { parseEvents }; diff --git a/apps/web/src/utils/pinata.ts b/apps/web/src/utils/pinata.ts index 91dd38f..2339e8b 100644 --- a/apps/web/src/utils/pinata.ts +++ b/apps/web/src/utils/pinata.ts @@ -1,11 +1,11 @@ -"server only" +"server only"; -import { PinataSDK } from 'pinata-web3' +import { PinataSDK } from "pinata-web3"; export const pinata = new PinataSDK({ - pinataJwt: `${process.env.PINATA_JWT}`, - pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}` -}) + pinataJwt: `${process.env.PINATA_JWT}`, + pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`, +}); // To get the client side form implementation of file uploads to pinata: // Visit the url: https://docs.pinata.cloud/frameworks/next-js-ipfs#create-client-side-form diff --git a/bun.lockb b/bun.lockb index b397e86..4e2a0c6 100755 Binary files a/bun.lockb and b/bun.lockb differ