Skip to content

Commit 43584ae

Browse files
authored
Merge pull request #332 from ChaoLing140/chore/close-78-81-84-89
Add contract events/upgrade hook and npm audit
2 parents adaa9da + f0a1584 commit 43584ae

2 files changed

Lines changed: 157 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,25 @@ jobs:
5858
- name: Run ESLint
5959
run: npm run lint
6060

61+
npm-audit:
62+
name: NPM Audit (High/Critical)
63+
runs-on: ubuntu-latest
64+
steps:
65+
- name: Checkout code
66+
uses: actions/checkout@v4
67+
68+
- name: Setup Node.js
69+
uses: actions/setup-node@v4
70+
with:
71+
node-version: ${{ env.NODE_VERSION }}
72+
cache: 'npm'
73+
74+
- name: Install dependencies
75+
run: npm ci --legacy-peer-deps
76+
77+
- name: Run NPM Audit
78+
run: npm audit --audit-level=high
79+
6180
typescript-typecheck:
6281
name: TypeScript Type Check
6382
runs-on: ubuntu-latest

contracts/src/lib.rs

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@
55
#![no_std]
66

77
use soroban_sdk::{
8-
contract, contractimpl, contracttype, Env, Address, String,
9-
Vec, Symbol, IntoVal, Val, TryFromVal,
8+
contract, contractimpl, contracttype, Address, BytesN, Env, IntoVal, String, Symbol, TryFromVal,
9+
Val, Vec,
1010
};
1111

1212
// ════════════════════════════════════════════════════════════════
1313
// DATA STRUCTURES
1414
// ════════════════════════════════════════════════════════════════
1515

16+
#[derive(Clone)]
17+
#[contracttype]
18+
enum DataKey {
19+
Admin,
20+
ContractVersion,
21+
}
22+
1623
/// Represents a single operation in a batch
1724
#[derive(Clone)]
1825
#[contracttype]
@@ -197,6 +204,62 @@ pub struct SubTrackrBatch;
197204

198205
#[contractimpl]
199206
impl SubTrackrBatch {
207+
/// Initialize the contract with an admin.
208+
///
209+
/// The admin is required for upgrade operations.
210+
pub fn init(env: Env, admin: Address) {
211+
if env.storage().instance().has(&DataKey::Admin) {
212+
panic!("already initialized");
213+
}
214+
215+
admin.require_auth();
216+
env.storage().instance().set(&DataKey::Admin, &admin);
217+
env.storage().instance().set(&DataKey::ContractVersion, &1u32);
218+
219+
env.events()
220+
.publish(Symbol::new(&env, "admin_initialized"), admin);
221+
}
222+
223+
/// Upgrade the contract WASM (admin-only).
224+
///
225+
/// Note: state is preserved because Soroban upgrades keep instance storage.
226+
/// If you need migrations, upgrade first, then call `migrate`.
227+
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
228+
let admin: Address = env
229+
.storage()
230+
.instance()
231+
.get(&DataKey::Admin)
232+
.unwrap_or_else(|| panic!("contract not initialized"));
233+
admin.require_auth();
234+
235+
env.events().publish(
236+
Symbol::new(&env, "contract_upgrade_requested"),
237+
new_wasm_hash.clone(),
238+
);
239+
240+
env.deployer().update_current_contract_wasm(new_wasm_hash);
241+
242+
env.events()
243+
.publish(Symbol::new(&env, "contract_upgraded"), admin);
244+
}
245+
246+
/// Post-upgrade migration hook (admin-only).
247+
pub fn migrate(env: Env, new_version: u32) {
248+
let admin: Address = env
249+
.storage()
250+
.instance()
251+
.get(&DataKey::Admin)
252+
.unwrap_or_else(|| panic!("contract not initialized"));
253+
admin.require_auth();
254+
255+
env.storage()
256+
.instance()
257+
.set(&DataKey::ContractVersion, &new_version);
258+
259+
env.events()
260+
.publish(Symbol::new(&env, "contract_migrated"), new_version);
261+
}
262+
200263
/// Execute a batch of subscription operations
201264
///
202265
/// # Arguments
@@ -225,6 +288,18 @@ impl SubTrackrBatch {
225288
for (index, operation) in operations.iter().enumerate() {
226289
let op_index = index as u32;
227290

291+
// Emit intent event so indexers can follow what was requested, even if it fails later.
292+
env.events().publish(
293+
(Symbol::new(&env, "operation_requested"), batch_id, op_index),
294+
(
295+
user.clone(),
296+
proxy.clone(),
297+
operation.function_name.clone(),
298+
operation.required,
299+
operation.depends_on,
300+
),
301+
);
302+
228303
// CHECK: Can we execute this operation?
229304
if should_fail && atomic {
230305
// In atomic mode, stop if previous failed
@@ -236,6 +311,11 @@ impl SubTrackrBatch {
236311
};
237312
results.push_back(result);
238313
failed_count += 1;
314+
315+
env.events().publish(
316+
(Symbol::new(&env, "operation_failed"), batch_id, op_index),
317+
String::from_str(&env, "Skipped due to atomic failure"),
318+
);
239319
continue;
240320
}
241321

@@ -254,6 +334,11 @@ impl SubTrackrBatch {
254334
results.push_back(result);
255335
failed_count += 1;
256336

337+
env.events().publish(
338+
(Symbol::new(&env, "operation_failed"), batch_id, op_index),
339+
String::from_str(&env, "Dependency failed"),
340+
);
341+
257342
if operation.required {
258343
should_fail = true;
259344
}
@@ -276,11 +361,14 @@ impl SubTrackrBatch {
276361

277362
successful_count += 1;
278363

279-
// Emit event for operation completion
364+
// Emit generic success event
280365
env.events().publish(
281-
(Symbol::new(&env, "operation_success"), batch_id),
282-
op_index,
366+
(Symbol::new(&env, "operation_success"), batch_id, op_index),
367+
operation.function_name.clone(),
283368
);
369+
370+
// Emit domain-level events for off-chain indexers.
371+
Self::emit_domain_event(&env, batch_id, op_index, &user, &proxy, operation);
284372
}
285373

286374
// Create batch result
@@ -346,6 +434,50 @@ impl SubTrackrBatch {
346434
(seq << 32) | (timestamp & 0xFFFFFFFF)
347435
}
348436

437+
fn emit_domain_event(
438+
env: &Env,
439+
batch_id: u64,
440+
op_index: u32,
441+
user: &Address,
442+
proxy: &Address,
443+
operation: &BatchOperation,
444+
) {
445+
let fn_name = operation.function_name.clone();
446+
447+
// Best-effort mapping based on operation name conventions.
448+
// Indexers can also rely on `operation_requested` / `operation_success` for full coverage.
449+
let topic = if fn_name == String::from_str(env, "create_plan")
450+
|| fn_name == String::from_str(env, "plan_create")
451+
|| fn_name == String::from_str(env, "createPlan")
452+
{
453+
Some(Symbol::new(env, "plan_created"))
454+
} else if fn_name == String::from_str(env, "subscribe")
455+
|| fn_name == String::from_str(env, "start_subscription")
456+
|| fn_name == String::from_str(env, "subscription_start")
457+
{
458+
Some(Symbol::new(env, "subscription_started"))
459+
} else if fn_name == String::from_str(env, "process_payment")
460+
|| fn_name == String::from_str(env, "payment_process")
461+
|| fn_name == String::from_str(env, "pay")
462+
{
463+
Some(Symbol::new(env, "payment_processed"))
464+
} else if fn_name == String::from_str(env, "cancel_subscription")
465+
|| fn_name == String::from_str(env, "subscription_cancel")
466+
|| fn_name == String::from_str(env, "cancel")
467+
{
468+
Some(Symbol::new(env, "subscription_cancelled"))
469+
} else {
470+
None
471+
};
472+
473+
if let Some(topic) = topic {
474+
env.events().publish(
475+
(topic, batch_id, op_index),
476+
(user.clone(), proxy.clone(), operation.params.clone()),
477+
);
478+
}
479+
}
480+
349481
/// Get batch status
350482
pub fn get_batch_status(env: Env, batch_id: u64) -> BatchStatus {
351483
let storage_key = Symbol::new(&env, &format!("batch_status_{}", batch_id));
@@ -450,4 +582,4 @@ mod tests {
450582

451583
assert!(builder.validate().is_err());
452584
}
453-
}
585+
}

0 commit comments

Comments
 (0)