Skip to content

Commit bea20da

Browse files
authored
Merge pull request #882 from sommy92/somzilla-issues
Implement issues #722, #724, #725, #723 from somzilla.md
2 parents 7fed558 + 4637817 commit bea20da

9 files changed

Lines changed: 512 additions & 75 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ jspm_packages/
7171
# Yarn Integrity file
7272
.yarn-integrity
7373
.env
74+
somzilla.md

app/bounties/[id]/page.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { notFound } from 'next/navigation'
2-
import { bounties } from '@/lib/creators-data'
2+
import type { Metadata } from 'next'
3+
import { bounties, formatBudget } from '@/lib/creators-data'
34
import { Header } from '@/components/header'
45
import { Footer } from '@/components/footer'
56
import { BountyMetaRow } from './bounty-meta-client'
@@ -9,6 +10,31 @@ import { BountyMilestoneProgress } from './bounty-milestone-progress'
910
import { Badge } from '@/components/ui/badge'
1011
import { BountyShareButton } from './bounty-share-client'
1112

13+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
14+
const { id } = await params
15+
const bounty = bounties.find((b) => b.id === id)
16+
17+
if (!bounty) {
18+
return { title: 'Bounty Not Found' }
19+
}
20+
21+
const budgetStr = formatBudget(bounty.budget, bounty.currency)
22+
return {
23+
title: `${bounty.title}${budgetStr}`,
24+
description: `${bounty.description.substring(0, 160)} Budget: ${budgetStr}. Category: ${bounty.category}.`,
25+
openGraph: {
26+
title: `${bounty.title}${budgetStr} | Stellar Creator Portfolio`,
27+
description: bounty.description.substring(0, 200),
28+
29+
},
30+
twitter: {
31+
card: 'summary_large_image',
32+
title: `${bounty.title}${budgetStr} | Stellar Creator Portfolio`,
33+
description: bounty.description.substring(0, 200),
34+
},
35+
}
36+
}
37+
1238
interface Props {
1339
params: Promise<{ id: string }>
1440
}

app/bounties/layout.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Metadata } from 'next';
2+
3+
export const metadata: Metadata = {
4+
title: 'Bounty Directory',
5+
description:
6+
'Browse available bounties across design, content creation, development, and marketing. Find exciting paid opportunities and collaborate with top talent.',
7+
openGraph: {
8+
title: 'Bounty Directory — Stellar Creator Portfolio',
9+
description:
10+
'Browse available bounties across design, content creation, development, and marketing.',
11+
},
12+
twitter: {
13+
card: 'summary_large_image',
14+
title: 'Bounty Directory — Stellar Creator Portfolio',
15+
description:
16+
'Browse available bounties across design, content creation, development, and marketing.',
17+
},
18+
};
19+
20+
export default function BountiesLayout({
21+
children,
22+
}: {
23+
children: React.ReactNode;
24+
}) {
25+
return children;
26+
}

app/creators/[id]/page.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Suspense } from 'react';
22
import { notFound } from 'next/navigation';
3+
import type { Metadata } from 'next';
34
import { Header } from '@/components/header';
45
import { Footer } from '@/components/footer';
56
import { CreatorProfileSkeleton } from '@/components/ui/skeleton-group';
@@ -8,6 +9,33 @@ import { CreatorHeroSection, CreatorCtaSection } from '@/components/streaming/cr
89
import { CreatorBioSection } from '@/components/streaming/creator-bio-section';
910
import { CreatorProjectsSection } from '@/components/streaming/creator-projects-section';
1011
import { fetchCreatorCore } from '@/lib/streaming/chunk-data';
12+
import { getCreatorById } from '@/lib/creators-data';
13+
14+
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
15+
const { id } = await params;
16+
const creator = getCreatorById(id);
17+
18+
if (!creator) {
19+
return { title: 'Creator Not Found' };
20+
}
21+
22+
return {
23+
title: `${creator.name}${creator.title}`,
24+
description: creator.bio || `View ${creator.name}'s portfolio, projects, and skills on Stellar Creator Portfolio.`,
25+
openGraph: {
26+
title: `${creator.name}${creator.title} | Stellar Creator Portfolio`,
27+
description: creator.bio,
28+
images: creator.avatar
29+
? [{ url: creator.avatar, width: 400, height: 400, alt: creator.name }]
30+
: [],
31+
},
32+
twitter: {
33+
card: 'summary_large_image',
34+
title: `${creator.name}${creator.title} | Stellar Creator Portfolio`,
35+
description: creator.bio,
36+
},
37+
};
38+
}
1139

1240
async function CreatorProfileShell({ id }: { id: string }) {
1341
const creator = await fetchCreatorCore(id);

app/creators/layout.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Metadata } from 'next';
2+
3+
export const metadata: Metadata = {
4+
title: 'Creator Directory',
5+
description:
6+
'Explore our community of world-class creators. Filter by discipline to find the perfect talent for your project in UI/UX design, writing, marketing, and more.',
7+
openGraph: {
8+
title: 'Creator Directory — Stellar Creator Portfolio',
9+
description:
10+
'Explore our community of world-class creators. Filter by discipline to find the perfect talent for your project.',
11+
},
12+
twitter: {
13+
card: 'summary_large_image',
14+
title: 'Creator Directory — Stellar Creator Portfolio',
15+
description:
16+
'Explore our community of world-class creators. Filter by discipline to find the perfect talent for your project.',
17+
},
18+
};
19+
20+
export default function CreatorsLayout({
21+
children,
22+
}: {
23+
children: React.ReactNode;
24+
}) {
25+
return children;
26+
}

app/layout.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ export const viewport: Viewport = {
2020
};
2121

2222
export const metadata: Metadata = {
23-
title: "Stellar Creators | Portfolio & Bounty Platform",
23+
title: {
24+
default: "Stellar Creator Portfolio — Discover Top Creative Talent",
25+
template: "%s — Stellar Creator Portfolio",
26+
},
2427
description:
25-
"Discover world-class creators across UI/UX design, writing, and content creation. Showcase your work, collaborate, and participate in exciting bounties.",
28+
"Discover, hire, and collaborate with exceptional creators across design, writing, marketing, product management, and 10+ more disciplines. Post bounties, find freelancers, and build amazing projects.",
2629
keywords: [
2730
"creators",
2831
"portfolio",
@@ -32,14 +35,24 @@ export const metadata: Metadata = {
3235
"content creation",
3336
"freelance",
3437
"showcase",
38+
"stellar",
3539
],
3640
generator: "v0.app",
3741
openGraph: {
38-
title: "Stellar Creators | Portfolio & Bounty Platform",
42+
title: "Stellar Creator Portfolio — Discover Top Creative Talent",
3943
description:
40-
"Discover and collaborate with world-class creators across multiple disciplines.",
44+
"Discover, hire, and collaborate with exceptional creators across design, writing, marketing, product management, and 10+ more disciplines.",
4145
type: "website",
46+
siteName: "Stellar Creator Portfolio",
47+
locale: "en_US",
48+
},
49+
twitter: {
50+
card: "summary_large_image",
51+
title: "Stellar Creator Portfolio — Discover Top Creative Talent",
52+
description:
53+
"Discover, hire, and collaborate with exceptional creators across design, writing, marketing, product management, and 10+ more disciplines.",
4254
},
55+
robots: { index: true, follow: true },
4356
icons: {
4457
icon: [
4558
{

backend/contracts/escrow/src/lib.rs

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,49 @@ pub enum ReleaseCondition {
2828
Timelock(u64),
2929
}
3030

31-
// ── Fee constants (#344) ─────────────────────────────────────────────────────
31+
// ---- Fee constants (#344/#722) --------------------------------------------------
3232

33-
/// Platform fee in basis points (2.5 %).
34-
pub const PLATFORM_FEE_BPS: i128 = 250;
33+
/// Default platform fee in basis points (2.5 %).
34+
pub const DEFAULT_PLATFORM_FEE_BPS: i128 = 250;
3535

36-
/// Maximum platform fee in token units (500 USDC-equivalent).
37-
pub const PLATFORM_FEE_CAP: i128 = 500;
36+
/// Default maximum platform fee in token units (500 USDC-equivalent).
37+
pub const DEFAULT_PLATFORM_FEE_CAP: i128 = 500;
3838

3939
/// Dispute timeout: 30 days in ledger seconds (#731).
4040
/// After this window anyone can call `resolve_expired_dispute` for a 50/50 split.
4141
pub const DISPUTE_TIMEOUT_SECS: u64 = 30 * 24 * 3600;
4242

43-
/// Compute the platform fee for a given gross amount.
44-
/// Fee = min(amount * 250 / 10_000, 500)
43+
/// Governance-configurable fee configuration stored on-chain (#722).
44+
#[contracttype]
45+
#[derive(Clone, Debug)]
46+
pub struct FeeConfig {
47+
pub fee_bps: i128,
48+
pub fee_cap: i128,
49+
}
50+
51+
/// Read the current fee config from storage, falling back to defaults.
52+
pub fn get_fee_config(env: &Env) -> FeeConfig {
53+
env.storage()
54+
.persistent()
55+
.get::<DataKey, FeeConfig>(&DataKey::FeeConfig)
56+
.unwrap_or(FeeConfig {
57+
fee_bps: DEFAULT_PLATFORM_FEE_BPS,
58+
fee_cap: DEFAULT_PLATFORM_FEE_CAP,
59+
})
60+
}
61+
62+
/// Compute the platform fee for a given gross amount using the current config.
63+
/// Fee = min(amount * fee_bps / 10_000, fee_cap)
4564
pub fn platform_fee(amount: i128) -> i128 {
46-
let raw = amount * PLATFORM_FEE_BPS / 10_000;
47-
if raw > PLATFORM_FEE_CAP { PLATFORM_FEE_CAP } else { raw }
65+
// When called from storage-aware code use platform_fee_with_config instead.
66+
let raw = amount * DEFAULT_PLATFORM_FEE_BPS / 10_000;
67+
if raw > DEFAULT_PLATFORM_FEE_CAP { DEFAULT_PLATFORM_FEE_CAP } else { raw }
68+
}
69+
70+
/// Compute platform fee using a specific FeeConfig (snapshot on deposit).
71+
pub fn platform_fee_with_config(amount: i128, config: &FeeConfig) -> i128 {
72+
let raw = amount * config.fee_bps / 10_000;
73+
if raw > config.fee_cap { config.fee_cap } else { raw }
4874
}
4975

5076
/// Escrow Account
@@ -87,6 +113,9 @@ pub enum DataKey {
87113
MilestoneReleasedAmount(u64),
88114
DisputeExpiry(u64),
89115
DisputeSplit,
116+
FeeConfig,
117+
OracleStalenessSecs,
118+
OracleAddress,
90119
}
91120

92121
// ── Issue #631: Yield Farming for Unwithdrawn Escrow Funds ───────────────────
@@ -146,7 +175,8 @@ impl EscrowContract {
146175
let mut counter: u64 = env.storage().persistent().get::<Symbol, u64>(&counter_key).unwrap_or(0);
147176
counter += 1;
148177

149-
let fee = platform_fee(amount);
178+
let fee_cfg = get_fee_config(&env);
179+
let fee = platform_fee_with_config(amount, &fee_cfg);
150180
let net_amount = amount - fee;
151181

152182
// Collect platform fee to admin if set, otherwise hold in contract
@@ -229,6 +259,26 @@ impl EscrowContract {
229259
"Release condition not met"
230260
);
231261

262+
263+
// Issue #725: Oracle price freshness check before release
264+
if let Some(oracle_addr) = env.storage().persistent().get::<DataKey, Address>(&DataKey::OracleAddress) {
265+
let max_staleness = Self::get_oracle_staleness_secs(&env);
266+
let price_data: soroban_sdk::Vec<soroban_sdk::Val> = env
267+
.invoke_contract(
268+
&oracle_addr,
269+
&Symbol::new(&env, "get_price"),
270+
soroban_sdk::Vec::new(&env),
271+
);
272+
let timestamp: u64 = price_data
273+
.get(1)
274+
.expect("Oracle returned invalid price data")
275+
.try_into()
276+
.unwrap_or(0);
277+
let age_secs = env.ledger().timestamp().saturating_sub(timestamp);
278+
if age_secs > max_staleness {
279+
panic!("Oracle price feed is stale");
280+
}
281+
}
232282
TokenClient::new(&env, &escrow.token)
233283
.transfer(&env.current_contract_address(), &escrow.payee, &escrow.amount);
234284

@@ -962,6 +1012,67 @@ impl EscrowContract {
9621012
}
9631013
}
9641014

1015+
1016+
// ---- Issue #722: Governance-controlled platform fee update ----
1017+
1018+
/// Update the platform fee configuration. Only the governance multisig
1019+
/// (platform admin) may call this.
1020+
/// Emits a fee_updated event on successful change.
1021+
pub fn update_fee(env: Env, admin: Address, new_bps: i128, new_cap: i128) {
1022+
admin.require_auth();
1023+
let stored_admin: Address = env
1024+
.storage()
1025+
.persistent()
1026+
.get::<Symbol, Address>(&Symbol::new(&env, "platform_admin"))
1027+
.expect("Platform admin not set");
1028+
assert_eq!(admin, stored_admin, "Only governance multisig can update fee");
1029+
assert!(new_bps >= 0, "Fee BPS must be non-negative");
1030+
assert!(new_bps <= 10_000, "Fee BPS must not exceed 10_000");
1031+
assert!(new_cap >= 0, "Fee cap must be non-negative");
1032+
env.storage().persistent().set(&DataKey::FeeConfig, &FeeConfig {
1033+
fee_bps: new_bps,
1034+
fee_cap: new_cap,
1035+
});
1036+
env.events().publish(
1037+
(symbol_short!("escrow"), symbol_short!("fee_upd")),
1038+
(new_bps, new_cap),
1039+
);
1040+
}
1041+
1042+
// ---- Issue #725: Oracle price freshness check ----
1043+
1044+
/// Return the oracle staleness threshold in seconds (governance-configurable).
1045+
/// Defaults to 3600 (1 hour).
1046+
pub fn get_oracle_staleness_secs(env: &Env) -> u64 {
1047+
env.storage()
1048+
.persistent()
1049+
.get::<DataKey, u64>(&DataKey::OracleStalenessSecs)
1050+
.unwrap_or(3600)
1051+
}
1052+
1053+
/// Set the oracle staleness threshold. Only governance multisig may call this.
1054+
pub fn set_oracle_staleness_secs(env: Env, admin: Address, staleness_secs: u64) {
1055+
admin.require_auth();
1056+
let stored_admin: Address = env
1057+
.storage()
1058+
.persistent()
1059+
.get::<Symbol, Address>(&Symbol::new(&env, "platform_admin"))
1060+
.expect("Platform admin not set");
1061+
assert_eq!(admin, stored_admin, "Only governance multisig can set staleness");
1062+
env.storage().persistent().set(&DataKey::OracleStalenessSecs, &staleness_secs);
1063+
}
1064+
1065+
/// Set the oracle contract address. Only governance multisig may call this.
1066+
pub fn set_oracle_address(env: Env, admin: Address, oracle: Address) {
1067+
admin.require_auth();
1068+
let stored_admin: Address = env
1069+
.storage()
1070+
.persistent()
1071+
.get::<Symbol, Address>(&Symbol::new(&env, "platform_admin"))
1072+
.expect("Platform admin not set");
1073+
assert_eq!(admin, stored_admin, "Only governance multisig can set oracle");
1074+
env.storage().persistent().set(&DataKey::OracleAddress, &oracle);
1075+
}
9651076
#[cfg(test)]
9661077
mod tests {
9671078
use super::*;

0 commit comments

Comments
 (0)