Skip to content

Commit

Permalink
Merge pull request #12 from horuslabsio/feat/follow
Browse files Browse the repository at this point in the history
feat: Implemented the Follow NFT, added some workflows
  • Loading branch information
codeWhizperer authored May 22, 2024
2 parents 1b69c64 + 00141a4 commit 78a8e60
Show file tree
Hide file tree
Showing 27 changed files with 605 additions and 109 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/build_contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Build

on: [push, pull_request]

permissions: read-all

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
- name: Check cairo format
run: scarb fmt --check
- name: Build cairo programs
run: scarb build
16 changes: 16 additions & 0 deletions .github/workflows/test_contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Test

on: [push, pull_request]
permissions: read-all

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
- uses: foundry-rs/setup-snfoundry@v3
with:
starknet-foundry-version: 0.22.0
- name: Run cairo tests
run: snforge test
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
scarb 2.6.3
starknet-foundry 0.22.0
8 changes: 6 additions & 2 deletions Scarb.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
[package]
name = "karst"
version = "0.1.0"
edition = "2023_11"
edition = "2023_10"
authors = ["Horus Labs <[email protected]>"]
description = "Decentralized Social Graph on Starknet"
repository = "https://github.com/horuslabsio/karst-core"
keywords = ["Karst", "SocialFi", "tokenbound", "cairo", "contracts", "starknet"]

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet = "2.6.3"
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.11.0" }
token_bound_accounts= { git = "https://github.com/Starknet-Africa-Edu/TBA", tag = "v0.3.0" }

[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.22.0" }


[[target.starknet-contract]]
casm = true
build-external-contracts = ["token_bound_accounts::presets::account::Account"]
3 changes: 3 additions & 0 deletions src/base.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod hubrestricted;
pub mod errors;
pub mod types;
10 changes: 10 additions & 0 deletions src/base/errors.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// *************************************************************************
// ERRORS
// *************************************************************************
pub mod Errors {
pub const NOT_PROFILE_OWNER: felt252 = 'NOT_PROFILE_OWNER';
pub const INITIALIZED: felt252 = 'ALREADY_INITIALIZED';
pub const HUB_RESTRICTED: felt252 = 'CALLER_IS_NOT_HUB';
pub const FOLLOWING: felt252 = 'USER_ALREADY_FOLLOWING';
pub const NOT_FOLLOWING: felt252 = 'USER_NOT_FOLLOWING';
}
12 changes: 12 additions & 0 deletions src/base/hubrestricted.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// *************************************************************************
// HUB RESTRICTION
// *************************************************************************
pub mod HubRestricted {
use starknet::{ContractAddress, get_caller_address};
use karst::base::errors::Errors;

pub fn hub_only(hub: ContractAddress) {
let caller = get_caller_address();
assert(caller == hub, Errors::HUB_RESTRICTED);
}
}
10 changes: 10 additions & 0 deletions src/base/types.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// *************************************************************************
// TYPES
// *************************************************************************
use starknet::ContractAddress;

#[derive(Drop, Serde, starknet::Store)]
pub struct FollowData {
follower_profile_address: ContractAddress,
follow_timestamp: u64
}
1 change: 0 additions & 1 deletion src/errors.cairo

This file was deleted.

6 changes: 0 additions & 6 deletions src/errors/error.cairo

This file was deleted.

1 change: 1 addition & 0 deletions src/follownft.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod follownft;
215 changes: 215 additions & 0 deletions src/follownft/follownft.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#[starknet::contract]
mod Follow {
// *************************************************************************
// IMPORT
// *************************************************************************
use core::traits::TryInto;
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use core::num::traits::zero::Zero;
use karst::interfaces::{IFollowNFT::IFollowNFT};
use karst::base::{errors::Errors, hubrestricted::HubRestricted::hub_only};
use karst::base::types::FollowData;

// *************************************************************************
// STORAGE
// *************************************************************************
#[storage]
struct Storage {
followed_profile_address: ContractAddress,
follower_count: u256,
follow_id_by_follower_profile_address: LegacyMap<ContractAddress, u256>,
follow_data_by_follow_id: LegacyMap<u256, FollowData>,
initialized: bool,
karst_hub: ContractAddress,
}

// *************************************************************************
// EVENTS
// *************************************************************************
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Followed: Followed,
Unfollowed: Unfollowed,
FollowerBlocked: FollowerBlocked,
}

#[derive(Drop, starknet::Event)]
struct Followed {
followed_address: ContractAddress,
follower_address: ContractAddress,
follow_id: u256,
timestamp: u64,
}

#[derive(Drop, starknet::Event)]
struct Unfollowed {
unfollowed_address: ContractAddress,
unfollower_address: ContractAddress,
follow_id: u256,
timestamp: u64,
}

#[derive(Drop, starknet::Event)]
struct FollowerBlocked {
followed_address: ContractAddress,
blocked_follower: ContractAddress,
follow_id: u256,
timestamp: u64,
}

// *************************************************************************
// CONSTRUCTOR
// *************************************************************************
#[constructor]
fn constructor(ref self: ContractState, hub: ContractAddress) {
self.karst_hub.write(hub);
}

// *************************************************************************
// EXTERNAL FUNCTIONS
// *************************************************************************
#[abi(embed_v0)]
impl FollowImpl of IFollowNFT<ContractState> {
fn initialize(ref self: ContractState, profile_address: ContractAddress) {
assert(!self.initialized.read(), Errors::INITIALIZED);
self.initialized.write(true);
self.followed_profile_address.write(profile_address);
}

fn follow(ref self: ContractState, follower_profile_address: ContractAddress) -> u256 {
hub_only(self.karst_hub.read());
let follow_id = self
.follow_id_by_follower_profile_address
.read(follower_profile_address);
assert(follow_id.is_zero(), Errors::FOLLOWING);
self._follow(follower_profile_address)
}

fn unfollow(ref self: ContractState, unfollower_profile_address: ContractAddress) {
hub_only(self.karst_hub.read());
let follow_id = self
.follow_id_by_follower_profile_address
.read(unfollower_profile_address);
assert(follow_id.is_non_zero(), Errors::NOT_FOLLOWING);
self._unfollow(unfollower_profile_address, follow_id);
}

fn process_block(
ref self: ContractState, follower_profile_address: ContractAddress
) -> bool {
hub_only(self.karst_hub.read());
let follow_id = self
.follow_id_by_follower_profile_address
.read(follower_profile_address);
assert(follow_id.is_non_zero(), Errors::NOT_FOLLOWING);
self._unfollow(follower_profile_address, follow_id);
self
.emit(
FollowerBlocked {
followed_address: self.followed_profile_address.read(),
blocked_follower: follower_profile_address,
follow_id: follow_id,
timestamp: get_block_timestamp()
}
);
return true;
}

// *************************************************************************
// GETTERS
// *************************************************************************
fn get_follower_profile_address(self: @ContractState, follow_id: u256) -> ContractAddress {
let follow_data = self.follow_data_by_follow_id.read(follow_id);
follow_data.follower_profile_address
}

fn get_follow_timestamp(self: @ContractState, follow_id: u256) -> u64 {
let follow_data = self.follow_data_by_follow_id.read(follow_id);
follow_data.follow_timestamp
}

fn get_follow_data(self: @ContractState, follow_id: u256) -> FollowData {
self.follow_data_by_follow_id.read(follow_id)
}

fn is_following(self: @ContractState, follower_profile_address: ContractAddress) -> bool {
self.follow_id_by_follower_profile_address.read(follower_profile_address) != 0
}

fn get_follow_id(self: @ContractState, follower_profile_address: ContractAddress) -> u256 {
self.follow_id_by_follower_profile_address.read(follower_profile_address)
}

fn get_follower_count(self: @ContractState) -> u256 {
self.follower_count.read()
}

// *************************************************************************
// METADATA
// *************************************************************************
fn name(self: @ContractState) -> ByteArray {
return "KARST:FOLLOWER";
}
fn symbol(self: @ContractState) -> ByteArray {
return "KFL";
}
fn token_uri(self: @ContractState, follow_id: u256) -> ByteArray {
// TODO: return token uri for follower contract
return "TODO";
}
}

// *************************************************************************
// PRIVATE FUNCTIONS
// *************************************************************************
#[generate_trait]
impl Private of PrivateTrait {
fn _follow(ref self: ContractState, follower_profile_address: ContractAddress) -> u256 {
let new_follower_id = self.follower_count.read() + 1;
let follow_timestamp: u64 = get_block_timestamp();
let follow_data = FollowData {
follower_profile_address: follower_profile_address,
follow_timestamp: follow_timestamp
};

self
.follow_id_by_follower_profile_address
.write(follower_profile_address, new_follower_id);
self.follow_data_by_follow_id.write(new_follower_id, follow_data);
self.follower_count.write(new_follower_id);
self
.emit(
Followed {
followed_address: self.followed_profile_address.read(),
follower_address: follower_profile_address,
follow_id: new_follower_id,
timestamp: get_block_timestamp()
}
);
return (new_follower_id);
}

fn _unfollow(ref self: ContractState, unfollower: ContractAddress, follow_id: u256) {
self.follow_id_by_follower_profile_address.write(unfollower, 0);
self
.follow_data_by_follow_id
.write(
follow_id,
FollowData {
follower_profile_address: 0.try_into().unwrap(), follow_timestamp: 0
}
);
self.follower_count.write(self.follower_count.read() - 1);
self
.emit(
Unfollowed {
unfollowed_address: self.followed_profile_address.read(),
unfollower_address: unfollower,
follow_id,
timestamp: get_block_timestamp()
}
);
}
}
}
4 changes: 0 additions & 4 deletions src/interface.cairo

This file was deleted.

5 changes: 5 additions & 0 deletions src/interfaces.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod IKarstNFT;
pub mod IERC721;
pub mod IRegistry;
pub mod IProfile;
pub mod IFollowNFT;
8 changes: 6 additions & 2 deletions src/interface/IERC721.cairo → src/interfaces/IERC721.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ pub trait IERC721<TState> {
self: @TState, owner: ContractAddress, operator: ContractAddress
) -> bool;

// ISRC5
// *************************************************************************
// ISRC5
// *************************************************************************
fn supports_interface(self: @TState, interface_id: felt252) -> bool;

// IERC721Metadata
// *************************************************************************
// ERC721 METADATA
// *************************************************************************
fn name(self: @TState) -> ByteArray;
fn symbol(self: @TState) -> ByteArray;
fn token_uri(self: @TState, token_id: u256) -> ByteArray;
Expand Down
31 changes: 31 additions & 0 deletions src/interfaces/IFollowNFT.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use starknet::ContractAddress;
use karst::base::types::FollowData;

// *************************************************************************
// INTERFACE of FollowNFT
// *************************************************************************
#[starknet::interface]
pub trait IFollowNFT<TState> {
// *************************************************************************
// EXTERNALS
// *************************************************************************
fn initialize(ref self: TState, profile_address: ContractAddress);
fn follow(ref self: TState, follower_profile_address: ContractAddress) -> u256;
fn unfollow(ref self: TState, unfollower_profile_address: ContractAddress);
fn process_block(ref self: TState, follower_profile_address: ContractAddress) -> bool;
// *************************************************************************
// GETTERS
// *************************************************************************
fn get_follower_profile_address(self: @TState, follow_id: u256) -> ContractAddress;
fn get_follow_timestamp(self: @TState, follow_id: u256) -> u64;
fn get_follow_data(self: @TState, follow_id: u256) -> FollowData;
fn is_following(self: @TState, follower_profile_address: ContractAddress) -> bool;
fn get_follow_id(self: @TState, follower_profile_address: ContractAddress) -> u256;
fn get_follower_count(self: @TState) -> u256;
// *************************************************************************
// METADATA
// *************************************************************************
fn name(self: @TState) -> ByteArray;
fn symbol(self: @TState) -> ByteArray;
fn token_uri(self: @TState, follow_id: u256) -> ByteArray;
}
Loading

0 comments on commit 78a8e60

Please sign in to comment.