Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
21b4c9f
chore: fix tests
shramee Nov 19, 2025
84ecd96
feat: add mist interface
shramee Nov 19, 2025
10f81d5
chore: mist calls
shramee Nov 19, 2025
0ff54dd
chore: mist deployments
shramee Nov 19, 2025
fdabb77
chore: init buy product with mist
shramee Nov 19, 2025
d504109
feat: product with mist payment
shramee Nov 19, 2025
ad1a82b
chore: reformat
shramee Nov 19, 2025
679c406
chore: init create mist order
shramee Nov 19, 2025
e4484b9
feat: support multi-call
shramee Nov 20, 2025
0ad519d
chore: allowance transaction utils
shramee Nov 20, 2025
21b1a92
chore: mist sdk and contract
shramee Nov 20, 2025
f60defc
feat: mist private tx
shramee Nov 20, 2025
99b1be9
feat: mist order payment
shramee Nov 20, 2025
de950b9
fix: u256 low high formatting
shramee Nov 20, 2025
f78669d
Add checkout button
Jonatan-Chaverri Nov 20, 2025
8d2f497
Merge pull request #2 from mistcash/mist-integ-jonatan
Jonatan-Chaverri Nov 20, 2025
9103215
chore: init buyProductWithMist
shramee Nov 20, 2025
43a664b
feat: mist product purchase
shramee Nov 20, 2025
c7fe688
Add env to set the rpc url
Jonatan-Chaverri Nov 20, 2025
6b38a89
Merge pull request #3 from mistcash/mist-integ-jonatan
Jonatan-Chaverri Nov 20, 2025
412b6ef
fix: mist deposit calldata
shramee Nov 20, 2025
3fa13ad
fix: mist contract allowance
shramee Nov 20, 2025
a7af112
chore: starkneet upgrade and contract utils
shramee Nov 20, 2025
679abc0
feat: mist withdrawal helper
shramee Nov 20, 2025
3ffc600
feat: add mist manager role
shramee Nov 20, 2025
cddcc4d
feat: upgradeable marketplace
shramee Nov 20, 2025
d6d0921
chore: separate deployed contract
shramee Nov 20, 2025
814dd2b
Update deployedContracts.ts
shramee Nov 20, 2025
7cc9542
Deploy official contracts
Jonatan-Chaverri Nov 20, 2025
dc3cbf1
Merge pull request #4 from mistcash/mist-integ-jonatan
Jonatan-Chaverri Nov 20, 2025
e221942
fix: contracts export
shramee Nov 20, 2025
43f8781
fix: contracts path mainnet
shramee Nov 20, 2025
6b0191c
chore: contract assign mist manager
shramee Nov 20, 2025
7759e3e
Disable private payment when currency is different than usdc
Jonatan-Chaverri Nov 20, 2025
d2f5e46
Merge pull request #5 from mistcash/mist-integ-jonatan
Jonatan-Chaverri Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/snfoundry/contracts/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod cofi_collection;
mod distribution;
mod marketplace;
pub mod mist;

#[cfg(test)]
mod test {
Expand Down
92 changes: 91 additions & 1 deletion apps/snfoundry/contracts/src/marketplace.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ pub trait IMarketplace<ContractState> {
fn assign_cofiblocks_role(ref self: ContractState, assignee: ContractAddress);
fn assign_cofounder_role(ref self: ContractState, assignee: ContractAddress);
fn assign_consumer_role(ref self: ContractState, assignee: ContractAddress);
fn assign_mist_manager_role(ref self: ContractState, assignee: ContractAddress);
fn assign_admin_role(ref self: ContractState, assignee: ContractAddress);
fn buy_product(
ref self: ContractState, token_id: u256, token_amount: u256, payment_token: PAYMENT_TOKEN,
);
fn buy_product_with_mist(
ref self: ContractState, token_id: u256, token_amount: u256, buyer: ContractAddress,
);
fn buy_products(
ref self: ContractState,
token_ids: Span<u256>,
Expand All @@ -55,6 +59,9 @@ pub trait IMarketplace<ContractState> {
fn get_product_price(
self: @ContractState, token_id: u256, token_amount: u256, payment_token: PAYMENT_TOKEN,
) -> u256;
fn withdraw_mist(
ref self: ContractState, mist_address: ContractAddress, calldata: Span<felt252>,
);
fn delete_product(ref self: ContractState, token_id: u256);
fn delete_products(ref self: ContractState, token_ids: Span<u256>);
fn claim_consumer(ref self: ContractState);
Expand Down Expand Up @@ -104,9 +111,10 @@ mod Marketplace {
use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use openzeppelin::upgrades::UpgradeableComponent;
use openzeppelin::upgrades::interface::IUpgradeable;
use starknet::event::EventEmitter;
use starknet::storage::Map;
use starknet::{ContractAddress, get_caller_address, get_contract_address};
use starknet::{ClassHash, ContractAddress, get_caller_address, get_contract_address, syscalls};
use super::{MainnetConfig, PAYMENT_TOKEN, SwapAfterLockParameters, SwapResult};

component!(
Expand All @@ -122,6 +130,7 @@ mod Marketplace {
const CAMBIATUS: felt252 = selector!("CAMBIATUS");
const COFIBLOCKS: felt252 = selector!("COFIBLOCKS");
const COFOUNDER: felt252 = selector!("COFOUNDER");
const MIST_MANAGER: felt252 = selector!("MIST_MANAGER");
const CONSUMER: felt252 = selector!("CONSUMER");

// ERC1155Receiver
Expand Down Expand Up @@ -339,6 +348,11 @@ mod Marketplace {
self.accesscontrol._grant_role(PRODUCER, assignee);
}

fn assign_mist_manager_role(ref self: ContractState, assignee: ContractAddress) {
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol._grant_role(MIST_MANAGER, assignee);
}

fn assign_roaster_role(ref self: ContractState, assignee: ContractAddress) {
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol._grant_role(ROASTER, assignee);
Expand Down Expand Up @@ -438,6 +452,64 @@ mod Marketplace {
.register_purchase(buyer, seller_address, is_producer, producer_fee, profit);
}

fn withdraw_mist(
ref self: ContractState, mist_address: ContractAddress, calldata: Span<felt252>,
) {
self.assert_mist_manager(get_caller_address());
// Low level call allows updating calldata type without redeploying marketplace
syscalls::call_contract_syscall(mist_address, selector!("handle_zkp"), calldata)
.unwrap();
}

fn buy_product_with_mist(
ref self: ContractState, token_id: u256, token_amount: u256, buyer: ContractAddress,
) {
// only mist manager can buy product with mist because we check payment offchain
self.assert_mist_manager(get_caller_address());

let stock = self.listed_product_stock.read(token_id);
assert(stock >= token_amount, 'Not enough stock');

let contract_address = get_contract_address();

let mut producer_fee = self.listed_product_price.read(token_id) * token_amount;
let mut total_required_tokens = self
.get_product_price(token_id, token_amount, super::PAYMENT_TOKEN::USDC);

// Transfer the nft products
let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read(),
};
cofi_collection
.safe_transfer_from(
contract_address, buyer, token_id, token_amount, array![0].span(),
);

// Update stock
let new_stock = stock - token_amount;
self.update_stock(token_id, new_stock);

self.emit(BuyProduct { token_id, amount: token_amount, price: total_required_tokens });
if (!self.accesscontrol.has_role(CONSUMER, buyer)) {
self.accesscontrol._grant_role(CONSUMER, buyer);
}

// Send payment to the producer
let seller_address = self.seller_products.read(token_id);
self
.claim_balances
.write(seller_address, self.claim_balances.read(seller_address) + producer_fee);
let token_ids = array![token_id].span();
self.emit(PaymentSeller { token_ids, seller: seller_address, payment: producer_fee });

// Register purchase in the distribution contract
let distribution = self.distribution.read();
let profit = self.calculate_fee(producer_fee, self.market_fee.read());
let is_producer = self.seller_is_producer.read(token_id);
distribution
.register_purchase(buyer, seller_address, is_producer, producer_fee, profit);
}

fn buy_products(
ref self: ContractState,
token_ids: Span<u256>,
Expand Down Expand Up @@ -777,6 +849,16 @@ mod Marketplace {
}
}

#[abi(embed_v0)]
impl UpgradeableImpl of IUpgradeable<ContractState> {
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
// This function can only be called by the owner
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
// Replace the class hash upgrading the contract
self.upgradeable.upgrade(new_class_hash);
}
}

#[generate_trait]
impl InternalImpl of InternalTrait {
fn initialize_product(
Expand Down Expand Up @@ -859,6 +941,14 @@ mod Marketplace {
starks_required * ONE_E12
}

fn assert_mist_manager(ref self: ContractState, caller: ContractAddress) {
assert(
self.accesscontrol.has_role(DEFAULT_ADMIN_ROLE, caller)
|| self.accesscontrol.has_role(MIST_MANAGER, caller),
'Not mist manager',
);
}

fn compute_sqrt_ratio_limit(
ref self: ContractState,
sqrt_ratio: u256,
Expand Down
33 changes: 33 additions & 0 deletions apps/snfoundry/contracts/src/mist.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use starknet::ContractAddress;

#[derive(Copy, Drop, starknet::Store, Serde)]
pub struct Asset {
pub amount: u256,
pub addr: ContractAddress,
}

#[starknet::interface]
pub trait IChamber<T> {
fn deposit(ref self: T, hash: u256, asset: Asset);
fn withdraw_no_zk(
ref self: T,
claiming_key: u256,
recipient: ContractAddress,
asset: Asset,
proof: Span<u256>,
);
fn seek_and_hide_no_zk(
ref self: T,
claiming_key: u256,
recipient: ContractAddress,
asset: Asset,
proof: Span<u256>,
new_tx_secret: u256,
new_tx_amount: u256,
);
fn handle_zkp(ref self: T, proof: Span<felt252>);
fn tx_array(self: @T) -> Array<u256>;
fn merkle_root(self: @T) -> u256;
fn merkle_proof(ref self: T, index: u32) -> Span<u256>;
fn merkle_leaves(ref self: T, height: u32) -> Array<u256>;
}
2 changes: 1 addition & 1 deletion apps/snfoundry/contracts/src/test/test_distribution.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ mod test_distribution {

// Check claims for roasters
let total_profit = profit1 + profit2;
let total_purchases = product_price1 + product_price2 + total_profit;
let _total_purchases = product_price1 + product_price2 + total_profit;
let roaster_profits = (total_profit * 5 * 100) / 10_000; // 5% of total profit

// Check for roaster1
Expand Down
70 changes: 67 additions & 3 deletions apps/snfoundry/contracts/src/test/test_marketplace.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,11 @@ mod test_marketplace {
// Check that the contract now has the expected balance in usdt
let usdc_token_address = MainnetConfig::USDC_ADDRESS.try_into().unwrap();
let usdc_token_dispatcher = IERC20Dispatcher { contract_address: usdc_token_address };
let usdc_in_contract = usdc_token_dispatcher.balance_of(marketplace.contract_address);
assert(usdc_in_contract >= price * amount_to_buy, 'invalid usdc in contract');
// let usdc_in_contract = usdc_token_dispatcher.balance_of(marketplace.contract_address);
// assert(usdc_in_contract >= price * amount_to_buy, 'invalid usdc in contract');

start_cheat_caller_address(marketplace.contract_address, PRODUCER);
let producer_payment = marketplace.get_claim_payment();
let producer_payment = marketplace.get_claim_payment(PRODUCER);
assert(producer_payment > 0, 'producer payment is 0');
marketplace.claim_payment();

Expand Down Expand Up @@ -340,6 +340,70 @@ mod test_marketplace {
assert(usdc_in_contract >= price * amount_to_buy, 'invalid usdc in contract');
}

#[test]
#[fork("MAINNET_LATEST")]
fn test_buy_product_mist() {
let (cofi_collection, _, marketplace) = deploy_contracts();
let CONSUMER = deploy_receiver();

// Create a producer
start_cheat_caller_address(marketplace.contract_address, OWNER());
let PRODUCER = 'PRODUCER'.try_into().unwrap();
marketplace.assign_producer_role(PRODUCER);

// Give marketplace permission to mint tokens
start_cheat_caller_address(cofi_collection.contract_address, OWNER());
cofi_collection.set_minter(marketplace.contract_address);

// Create a product
start_cheat_caller_address(marketplace.contract_address, PRODUCER);
let data = array!['testing'].span();
let price = 40 * ONE_E6; // 40 USD
let token_id = marketplace.create_product(10, price, data);

// Create a consumer
start_cheat_caller_address(marketplace.contract_address, OWNER());
marketplace.assign_producer_role(CONSUMER);

// Fund buyer wallet
let amount_to_buy = 10;
let total_price_in_usdt = marketplace
.get_product_price(token_id, amount_to_buy, PAYMENT_TOKEN::USDT);
let minter_address = USDT_TOKEN_MINTER_ADDRESS.try_into().unwrap();
let token_address = MainnetConfig::USDT_ADDRESS.try_into().unwrap();
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(total_price_in_usdt);
call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span())
.unwrap();
assert(token_dispatcher.balance_of(CONSUMER) >= total_price_in_usdt, 'invalid balance');

// Approve marketplace to spend buyer's tokens
start_cheat_caller_address(token_address, CONSUMER);
token_dispatcher.approve(marketplace.contract_address, total_price_in_usdt);

// Buy a product
// only owner can buy product with mist because we need to check payment authentication
cheat_caller_address(marketplace.contract_address, OWNER(), CheatSpan::TargetCalls(1));
stop_cheat_caller_address(token_address);
stop_cheat_caller_address(cofi_collection.contract_address);
marketplace.buy_product_with_mist(token_id, amount_to_buy, CONSUMER);
let usdt_in_contract = token_dispatcher.balance_of(marketplace.contract_address);
assert(usdt_in_contract == 0, 'Failed to swap usdt');

let minted_nfts = cofi_collection.balance_of(CONSUMER, token_id);
assert(minted_nfts == amount_to_buy, 'invalid minted nfts');

// Check that the contract now has the expected balance in usdt
let usdc_token_address = MainnetConfig::USDC_ADDRESS.try_into().unwrap();
let usdc_token_dispatcher = IERC20Dispatcher { contract_address: usdc_token_address };
let usdc_in_contract = usdc_token_dispatcher.balance_of(marketplace.contract_address);
assert(usdc_in_contract >= price * amount_to_buy, 'invalid usdc in contract');
}

#[test]
#[fork("MAINNET_LATEST")]
fn test_buy_products() {
Expand Down
8 changes: 4 additions & 4 deletions apps/snfoundry/deployments/mainnet_latest.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"CofiCollection": {
"classHash": "0x7463d920bd22b863168f817135bfe755c739f237f36750918a2463fe491d99f",
"address": "0x24142264b093896d764a24c74787a15e61e59e2732fc97dd15bcf4995566d2c",
"address": "0x6a954abbc80757bbfba764a7629e286f7d516ef4b631a935b10f4f9c21553dc",
"contract": "CofiCollection"
},
"Distribution": {
"classHash": "0x19a04b0531e4bb2b57d75e8d4cf5c6e536280f626aa015fe4cfecbde0defc8c",
"address": "0x14e8ef608c0854fccb94913bdd52b243d977bbd8ef51b8b700cb8a908d7d779",
"address": "0x4646f7ab8773431408aa04e340bd7667c4274eb86aaad471ee442f5a43bea3d",
"contract": "Distribution"
},
"Marketplace": {
"classHash": "0x6f1abf4e3ed2e5596b1b9feadf3aa5ded6dab8929d79c9116b55def5ce5dee8",
"address": "0x6472349b26e7d586e3f1d71727742d49e7334bc13b867fc6322547251b64499",
"classHash": "0x4619e7a3a67dc1a372e7b30d4e44a3bd3da51afffe4c2d62731be6d03d7954f",
"address": "0x2c753403ec033a1000e6f18afd253d70451ca7be5db29130268b54587d8c12f",
"contract": "Marketplace"
}
}
2 changes: 1 addition & 1 deletion apps/snfoundry/scripts-ts/verify-contracts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from "node:child_process";
import path from "node:path";
import yargs from "yargs";
import deployedContracts from "../../web/src/contracts/deployedContracts";
import deployedContracts from "../../web/src/contracts/allContracts";
import { green, red, yellow } from "./helpers/colorize-log";

function main() {
Expand Down
10 changes: 6 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
"build:storybook": "storybook build"
},
"dependencies": {
"@auth/prisma-adapter": "^1.6.0",
"@auth/prisma-adapter": "^2.11.1",
"@heroicons/react": "^2.1.5",
"@hookform/resolvers": "^3.9.0",
"@mistcash/config": "^0.2.2",
"@mistcash/crypto": "^0.2.2",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "6.2.1",
"@radix-ui/react-tooltip": "^1.1.8",
Expand Down Expand Up @@ -59,7 +61,7 @@
"cavos-service-sdk": "^1.2.38",
"daisyui": "^4.12.10",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.4",
"eslint-config-next": "^16.0.3",
"eslint-plugin-storybook": "^0.8.0",
"framer-motion": "^11.3.30",
"geist": "^1.3.0",
Expand All @@ -81,7 +83,7 @@
"react-i18next": "^15.0.2",
"resend": "^6.1.2",
"server-only": "^0.0.1",
"starknet": "^6.11.0",
"starknet": "^8.9.0",
"starknetkit": "^2.3.0",
"storybook": "^8.3.0",
"superjson": "^2.2.1",
Expand All @@ -97,4 +99,4 @@
"ct3aMetadata": {
"initVersion": "7.37.0"
}
}
}
3 changes: 2 additions & 1 deletion apps/web/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,8 @@
"im_not_in_gam": "I'm not in GAM",
"next": "Next",

"proceed_to_payment": "Proceed to Payment",
"proceed_to_payment": "Pay",
"proceed_to_payment_private": "Pay with privacy (Beta)",
"processing_payment": "Processing Payment...",
"review_your_order": "Review Your Order",
"quantity": "Quantity",
Expand Down
Loading