Skip to content

Commit 273666f

Browse files
authored
Merge pull request #1237 from richardiyamura/fix/subscription-895-896-897-898
fix(subscription): validate error codes, optimize gas, integration tests, wasm CI verify
2 parents 3d044da + 0ab1336 commit 273666f

6 files changed

Lines changed: 446 additions & 26 deletions

File tree

.github/workflows/contract-ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,29 @@ jobs:
3939
- name: Build wasm target
4040
run: cargo build --release --target wasm32-unknown-unknown --manifest-path Cargo.toml
4141
working-directory: contract
42+
43+
- name: Verify wasm artifacts
44+
run: |
45+
WASM_DIR="target/wasm32-unknown-unknown/release"
46+
EXPECTED=(
47+
subscription
48+
myfans_token
49+
content_access
50+
creator_registry
51+
earnings
52+
)
53+
MISSING=0
54+
for pkg in "${EXPECTED[@]}"; do
55+
WASM_FILE="${WASM_DIR}/${pkg}.wasm"
56+
if [[ -f "$WASM_FILE" && -s "$WASM_FILE" ]]; then
57+
echo "✅ ${pkg}.wasm ($(du -sh "$WASM_FILE" | cut -f1))"
58+
else
59+
echo "❌ ${pkg}.wasm: missing or empty"
60+
MISSING=1
61+
fi
62+
done
63+
if [[ "$MISSING" -eq 1 ]]; then
64+
echo "::error::One or more expected wasm artifacts are missing or empty after build."
65+
exit 1
66+
fi
67+
working-directory: contract

contract/contracts/myfans-lib/src/error_codes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub mod subscription {
2727
pub const INVALID_FEE_BPS: u32 = 7;
2828
pub const INVALID_TOKEN_ADDRESS: u32 = 8;
2929
pub const INVALID_PRICE: u32 = 9;
30+
/// Plan ID does not exist; never created or out of range.
31+
pub const PLAN_NOT_FOUND: u32 = 10;
3032
}
3133

3234
/// Error codes for the **content-access** contract.

contract/contracts/subscription/src/lib.rs

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ impl DataKey {
6868
/// | 7 | `InvalidFeeBps` |
6969
/// | 8 | `InvalidTokenAddress` |
7070
/// | 9 | `InvalidPrice` |
71+
/// | 10 | `PlanNotFound` |
7172
#[contracterror]
7273
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7374
pub enum Error {
@@ -89,6 +90,8 @@ pub enum Error {
8990
InvalidTokenAddress = 8,
9091
/// Code 9 – subscription price must be strictly positive.
9192
InvalidPrice = 9,
93+
/// Code 10 – plan ID does not exist; never created or out of range.
94+
PlanNotFound = 10,
9295
}
9396

9497
/// Stellar "null" account (GAAA...WHF) — not a valid fee recipient.
@@ -214,20 +217,20 @@ impl MyfansContract {
214217
.storage()
215218
.instance()
216219
.get(&DataKey::Plan(plan_id))
217-
.unwrap();
220+
.unwrap_or_else(|| panic_with_error!(&env, Error::PlanNotFound));
218221
let fee_bps: u32 = env.storage().instance().get(&DataKey::FeeBps).unwrap_or(0);
219-
let fee_recipient: Address = env
220-
.storage()
221-
.instance()
222-
.get(&DataKey::FeeRecipient)
223-
.unwrap();
224-
225222
let fee = (plan.amount * fee_bps as i128) / 10000;
226223
let creator_amount = plan.amount - fee;
227224

228225
let token_client = token::Client::new(&env, &plan.asset);
229226
token_client.transfer(&fan, &plan.creator, &creator_amount);
230227
if fee > 0 {
228+
// Deferred read: only fetch fee_recipient when a fee is actually owed.
229+
let fee_recipient: Address = env
230+
.storage()
231+
.instance()
232+
.get(&DataKey::FeeRecipient)
233+
.unwrap();
231234
token_client.transfer(&fan, &fee_recipient, &fee);
232235
}
233236

@@ -284,7 +287,9 @@ impl MyfansContract {
284287
.instance()
285288
.get(&DataKey::Paused)
286289
.unwrap_or(false);
287-
assert!(!paused, "contract is paused");
290+
if paused {
291+
panic_with_error!(&env, Error::Paused);
292+
}
288293

289294
let sub: Subscription = env
290295
.storage()
@@ -300,21 +305,21 @@ impl MyfansContract {
300305
.storage()
301306
.instance()
302307
.get(&DataKey::Plan(sub.plan_id))
303-
.unwrap();
308+
.unwrap_or_else(|| panic_with_error!(&env, Error::PlanNotFound));
304309

305310
let fee_bps: u32 = env.storage().instance().get(&DataKey::FeeBps).unwrap_or(0);
306-
let fee_recipient: Address = env
307-
.storage()
308-
.instance()
309-
.get(&DataKey::FeeRecipient)
310-
.unwrap();
311-
312311
let fee = (plan.amount * fee_bps as i128) / 10000;
313312
let creator_amount = plan.amount - fee;
314313

315314
let token_client = token::Client::new(&env, &token);
316315
token_client.transfer(&fan, &creator, &creator_amount);
317316
if fee > 0 {
317+
// Deferred read: only fetch fee_recipient when a fee is actually owed.
318+
let fee_recipient: Address = env
319+
.storage()
320+
.instance()
321+
.get(&DataKey::FeeRecipient)
322+
.unwrap();
318323
token_client.transfer(&fan, &fee_recipient, &fee);
319324
}
320325

@@ -375,7 +380,9 @@ impl MyfansContract {
375380
.instance()
376381
.get(&DataKey::Paused)
377382
.unwrap_or(false);
378-
assert!(!paused, "contract is paused");
383+
if paused {
384+
panic_with_error!(&env, Error::Paused);
385+
}
379386

380387
let token: Address = env
381388
.storage()
@@ -384,18 +391,18 @@ impl MyfansContract {
384391
.unwrap();
385392
let price: i128 = env.storage().instance().get(&DataKey::Price).unwrap();
386393
let fee_bps: u32 = env.storage().instance().get(&DataKey::FeeBps).unwrap_or(0);
387-
let fee_recipient: Address = env
388-
.storage()
389-
.instance()
390-
.get(&DataKey::FeeRecipient)
391-
.unwrap();
392-
393394
let fee = (price * fee_bps as i128) / 10000;
394395
let creator_amount = price - fee;
395396

396397
let token_client = token::Client::new(&env, &token);
397398
token_client.transfer(&fan, &creator, &creator_amount);
398399
if fee > 0 {
400+
// Deferred read: only fetch fee_recipient when a fee is actually owed.
401+
let fee_recipient: Address = env
402+
.storage()
403+
.instance()
404+
.get(&DataKey::FeeRecipient)
405+
.unwrap();
399406
token_client.transfer(&fan, &fee_recipient, &fee);
400407
}
401408

contract/contracts/subscription/src/test.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,6 @@ fn test_subscribe_fails_when_paused() {
10461046
}
10471047

10481048
#[test]
1049-
#[should_panic(expected = "contract is paused")]
10501049
fn test_extend_subscription_fails_when_paused() {
10511050
let (env, client, admin, token, token_admin) = setup_test();
10521051
let fee_recipient = Address::generate(&env);
@@ -1057,7 +1056,11 @@ fn test_extend_subscription_fails_when_paused() {
10571056
let plan_id = client.create_plan(&creator, &token.address, &1000, &30);
10581057
client.subscribe(&fan, &plan_id, &token.address);
10591058
client.pause();
1060-
client.extend_subscription(&fan, &creator, &17280, &token.address);
1059+
let result = client.try_extend_subscription(&fan, &creator, &17280, &token.address);
1060+
assert_eq!(
1061+
result,
1062+
Err(Ok(SorobanError::from_contract_error(Error::Paused as u32)))
1063+
);
10611064
}
10621065

10631066
#[test]
@@ -1076,10 +1079,13 @@ fn test_cancel_fails_when_paused() {
10761079
}
10771080

10781081
#[test]
1079-
#[should_panic]
10801082
fn test_create_subscription_fails_when_paused() {
10811083
let (_env, client, _admin, creator, fan, _token, _token_admin) = setup_paused();
1082-
client.create_subscription(&fan, &creator, &518400);
1084+
let result = client.try_create_subscription(&fan, &creator, &518400);
1085+
assert_eq!(
1086+
result,
1087+
Err(Ok(SorobanError::from_contract_error(Error::Paused as u32)))
1088+
);
10831089
}
10841090

10851091
/// Views must remain available while paused.
@@ -1479,3 +1485,70 @@ fn test_ping_works_on_uninitialized_contract() {
14791485
let seq = client.ping();
14801486
assert_eq!(seq, env.ledger().sequence());
14811487
}
1488+
1489+
// ── #895 – error code validation ─────────────────────────────────────────────
1490+
1491+
/// subscribe with a plan_id that was never created returns Error::PlanNotFound (code 10).
1492+
#[test]
1493+
fn test_subscribe_nonexistent_plan_returns_plan_not_found() {
1494+
let (env, client, admin, token, _token_admin) = setup_test();
1495+
let fee_recipient = Address::generate(&env);
1496+
client.init(&admin, &500, &fee_recipient, &token.address, &1000);
1497+
let fan = Address::generate(&env);
1498+
1499+
let result = client.try_subscribe(&fan, &9999u32, &token.address);
1500+
assert_eq!(
1501+
result,
1502+
Err(Ok(SorobanError::from_contract_error(
1503+
Error::PlanNotFound as u32
1504+
)))
1505+
);
1506+
}
1507+
1508+
/// create_plan when paused returns Error::Paused (code 2).
1509+
#[test]
1510+
fn test_create_plan_paused_returns_typed_error() {
1511+
let (_env, client, _admin, creator, _fan, token, _token_admin) = setup_paused();
1512+
let result = client.try_create_plan(&creator, &token.address, &1000, &30);
1513+
assert_eq!(
1514+
result,
1515+
Err(Ok(SorobanError::from_contract_error(Error::Paused as u32)))
1516+
);
1517+
}
1518+
1519+
/// subscribe when paused returns Error::Paused (code 2).
1520+
#[test]
1521+
fn test_subscribe_paused_returns_typed_error() {
1522+
let (env, client, admin, token, token_admin) = setup_test();
1523+
let fee_recipient = Address::generate(&env);
1524+
let creator = Address::generate(&env);
1525+
let fan = Address::generate(&env);
1526+
client.init(&admin, &500, &fee_recipient, &token.address, &1000);
1527+
token_admin.mint(&fan, &50000);
1528+
let plan_id = client.create_plan(&creator, &token.address, &1000, &30);
1529+
client.pause();
1530+
let result = client.try_subscribe(&fan, &plan_id, &token.address);
1531+
assert_eq!(
1532+
result,
1533+
Err(Ok(SorobanError::from_contract_error(Error::Paused as u32)))
1534+
);
1535+
}
1536+
1537+
/// cancel when paused returns Error::Paused (code 2).
1538+
#[test]
1539+
fn test_cancel_paused_returns_typed_error() {
1540+
let (env, client, admin, token, token_admin) = setup_test();
1541+
let fee_recipient = Address::generate(&env);
1542+
let creator = Address::generate(&env);
1543+
let fan = Address::generate(&env);
1544+
client.init(&admin, &0, &fee_recipient, &token.address, &1000);
1545+
token_admin.mint(&fan, &50000);
1546+
let plan_id = client.create_plan(&creator, &token.address, &1000, &30);
1547+
client.subscribe(&fan, &plan_id, &token.address);
1548+
client.pause();
1549+
let result = client.try_cancel(&fan, &creator, &0);
1550+
assert_eq!(
1551+
result,
1552+
Err(Ok(SorobanError::from_contract_error(Error::Paused as u32)))
1553+
);
1554+
}

0 commit comments

Comments
 (0)