Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions backend/src/services/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,22 @@ function processEvent(db: any, event: rpc.Api.EventResponse): void {
event.ledger,
);
break;

case "StartUp":
recordEventWithDb(
db,
value.stream_id.toString(),
"start_time_updated",
timestamp,
value.sender,
undefined,
{
oldStartTime: Number(value.old_start_time),
newStartTime: Number(value.new_start_time),
},
event.ledger,
);
break;
Comment on lines +300 to +315

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate indexer and schemas =="
fd -a 'indexer\.ts|schemas\.ts$' . | sed 's#^./##'

echo "== relevant indexer section =="
idx="$(fd 'indexer\.ts$' . | head -n1)"
if [ -n "${idx:-}" ]; then
  wc -l "$idx"
  sed -n '1,70p;230,340p' "$idx" | cat -n
fi

echo "== event record implementation =="
rg -n "recordEventWithDb|function recordEventWithDb|record.*Event|event.*metadata|JSON\.stringify|oldStartTime|newStartTime|start_time_updated" -S .

echo "== probe u64 Number.MAX_SAFE_INTEGER issue =="
node - <<'JS'
const values = [
  BigInt(String(Number.MAX_SAFE_INTEGER)),
  BigInt(String(Number.MAX_SAFE_INTEGER + 1)),
  18446744073709551615n
];
for (const value of values) {
  const num = Number(value);
  const back = BigInt.asIntN(64, BigInt(num));
  console.log(JSON.stringify({
    bigint: value.toString(),
    number: num,
    backToBigInt: back.toString(),
    exact: value.toString() === back.toString(),
    aboveSafe: value > BigInt(Number.MAX_SAFE_INTEGER),
  }));
}
JS

Repository: ritik4ever/stellar-stream

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'indexer\.ts$' .
fd -a 'schemas\.ts$' .

echo "== target file lines =="
idx="$(fd 'indexer\.ts$' . | head -n1)"
if [ -n "${idx:-}" ]; then
  echo "INDEXER=$idx"
  wc -l "$idx"
  sed -n '1,90p;240,330p' "$idx" | nl -ba
fi

echo "== focused event/event metadata functions and schema usages =="
rg -n "recordEventWithDb|function recordEventWithDb|const recordEventWithDb|StartUp|start_time_updated|oldStartTime|newStartTime|value\.old_start_time|value\.new_start_time|JSON\.stringify|RecordEvent" -S "$idx" $(fd 'schemas\.ts$' . | head -n5 || true)

echo "== Node u64 Number precision probe =="
node - <<'JS'
const values = [
  BigInt(String(Number.MAX_SAFE_INTEGER)),
  BigInt(String(Number.MAX_SAFE_INTEGER) + "+1"),
  18446744073709551615n
];
for (const value of values) {
  const num = Number(value);
  const back = BigInt(num) & ((1n << 64n) - 1n);
  console.log(JSON.stringify({
    bigint: value.toString(),
    number: num,
    backThroughBigIntNumber: back.toString(),
    exact: value.toString() === back.toString(),
    aboveSafe: value > BigInt(Number.MAX_SAFE_INTEGER),
  }));
}
JS

Repository: ritik4ever/stellar-stream

Length of output: 488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file lines =="
sed -n '1,90p;240,330pM' backend/src/services/indexer.ts | awk '{printf "%6d\t%s\n", NR+0, $0}'

echo "== focused event/event metadata functions and schema usages =="
rg -n "recordEventWithDb|function recordEventWithDb|const recordEventWithDb|StartUp|start_time_updated|oldStartTime|newStartTime|value\.old_start_time|value\.new_start_time|JSON\.stringify|RecordEvent" -S backend/src/services/indexer.ts backend/src/validation/schemas.ts

echo "== Node u64 Number precision probe =="
node - <<'JS'
const values = [
  BigInt(String(Number.MAX_SAFE_INTEGER)),
  BigInt(String(Number.MAX_SAFE_INTEGER) + "+1"),
  18446744073709551615n
];
for (const value of values) {
  const num = Number(value);
  const back = BigInt(num) & ((1n << 64n) - 1n);
  console.log(JSON.stringify({
    bigint: value.toString(),
    number: num,
    backThroughBigIntNumber: back.toString(),
    exact: value.toString() === back.toString(),
    aboveSafe: value > BigInt(Number.MAX_SAFE_INTEGER),
  }));
}
JS

Repository: ritik4ever/stellar-stream

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file lines =="
sed -n '1,90p;240,330p' backend/src/services/indexer.ts | awk '{printf "%6d\t%s\n", NR+0, $0}'

echo "== focused event/event metadata functions and schema usages =="
rg -n "recordEventWithDb|function recordEventWithDb|const recordEventWithDb|StartUp|start_time_updated|oldStartTime|newStartTime|value\.old_start_time|value\.new_start_time|JSON\.stringify|RecordEvent" -S backend/src/services/indexer.ts backend/src/validation/schemas.ts

echo "== Node u64 Number precision probe =="
node - <<'JS'
const values = [
  BigInt(String(Number.MAX_SAFE_INTEGER)),
  BigInt(String(Number.MAX_SAFE_INTEGER) + "+1"),
  18446744073709551615n
];
for (const value of values) {
  const num = Number(value);
  const back = BigInt(num) & ((1n << 64n) - 1n);
  console.log(JSON.stringify({
    bigint: value.toString(),
    number: num,
    backThroughBigIntNumber: back.toString(),
    exact: value.toString() === back.toString(),
    aboveSafe: value > BigInt(Number.MAX_SAFE_INTEGER),
  }));
}
JS

Repository: ritik4ever/stellar-stream

Length of output: 7429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== eventHistory related files =="
fd -a 'eventHistory\.ts$' backend/src/services

echo "== eventHistory implementation =="
evt="$(fd 'eventHistory\.ts$' backend/src/services | head -n1)"
if [ -n "${evt:-}" ]; then
  wc -l "$evt"
  sed -n '1,220p' "$evt" | awk '{printf "%6d\t%s\n", NR+0, $0}'
fi

echo "== backend validation schema event rows =="
rg -n "event_history|start_time_updated|oldStartTime|newStartTime|JSON|metadata" -S backend/src backend/src/services backend/src/validation || true

echo "== Node u64 Number precision probe =="
node - <<'JS'
const values = [
  BigInt(String(Number.MAX_SAFE_INTEGER)),
  BigInt(number = Number.MAX_SAFE_INTEGER + 1),
  18446744073709551615n
];
for (const value of values) {
  const num = Number(value);
  const back = BigInt(num) & ((1n << 64n) - 1n);
  console.log(JSON.stringify({
    bigint: value.toString(),
    number: num,
    backThroughBigIntNumber: back.toString(),
    exact: value.toString() === back.toString(),
    aboveSafe: value > BigInt(Number.MAX_SAFE_INTEGER),
  }));
}
JS

Repository: ritik4ever/stellar-stream

Length of output: 27723


Do not coerce on-chain u64 timestamps to Number.

Valid timestamps above Number.MAX_SAFE_INTEGER lose precision when passed through Number(...) before JSON.stringify, so oldStartTime/newStartTime can be corrupted in persisted event metadata. Store these values as decimal strings or another exact representation and update callers accordingly.

Preserve exact timestamp values
-            oldStartTime: Number(value.old_start_time),
-            newStartTime: Number(value.new_start_time),
+            oldStartTime: String(value.old_start_time),
+            newStartTime: String(value.new_start_time),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "StartUp":
recordEventWithDb(
db,
value.stream_id.toString(),
"start_time_updated",
timestamp,
value.sender,
undefined,
{
oldStartTime: Number(value.old_start_time),
newStartTime: Number(value.new_start_time),
},
event.ledger,
);
break;
case "StartUp":
recordEventWithDb(
db,
value.stream_id.toString(),
"start_time_updated",
timestamp,
value.sender,
undefined,
{
oldStartTime: String(value.old_start_time),
newStartTime: String(value.new_start_time),
},
event.ledger,
);
break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/indexer.ts` around lines 300 - 315, Update the StartUp
handling in recordEventWithDb so value.old_start_time and value.new_start_time
are preserved exactly instead of being coerced with Number(...). Store them as
decimal strings or the repository’s established exact u64 representation, and
update any affected consumers or callers to handle that representation without
precision loss.

}
} catch (err) {
logger.error({ err }, "failed to process event");
Expand Down
59 changes: 59 additions & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ pub struct StreamResumed {

}

#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StreamStartTimeUpdated {
pub stream_id: u64,
pub sender: Address,
pub old_start_time: u64,
pub new_start_time: u64,
pub updated_at: u64,
}

#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClawbackExecuted {
Expand Down Expand Up @@ -589,6 +599,55 @@ impl StellarStreamContract {
);
}

// ---------------------------------------------------------------------------
// Update start time
// ---------------------------------------------------------------------------

/// Update the start time of a stream.
///
/// Only the original sender may invoke this function. The new start time
/// must be strictly in the future relative to the current ledger time,
/// strictly less than the stream's end time, and the stream must not be
/// canceled. Emits a `Stream/StartUp` event so off-chain listeners can
/// reconcile.
pub fn update_start_time(env: Env, stream_id: u64, sender: Address, new_start_time: u64) {
let mut stream = read_stream(&env, stream_id);
if stream.sender != sender {
panic!("sender mismatch");
}
sender.require_auth();

if stream.canceled {
panic!("stream canceled");
}

let now = env.ledger().timestamp();
if new_start_time <= now {
panic!("new_start_time must be in the future");
}
if new_start_time >= stream.end_time {
panic!("new_start_time must be before stream end_time");
}

let old_start_time = stream.start_time;
stream.start_time = new_start_time;

env.storage()
.persistent()
.set(&DataKey::Stream(stream_id), &stream);

env.events().publish(
(symbol_short!("Stream"), symbol_short!("StartUp")),
StreamStartTimeUpdated {
stream_id,
sender,
old_start_time,
new_start_time,
updated_at: now,
},
);
}

// -----------------------------------------------------------------------
// Clawback
// -----------------------------------------------------------------------
Expand Down
170 changes: 170 additions & 0 deletions contracts/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2332,3 +2332,173 @@ fn test_cancel_after_partial_claim_full_lifecycle() {
let recipient_balance = token_client.balance(&recipient);
assert_eq!(sender_refund + recipient_balance, 100);
}

// ============================================================================
// #589 \u2014 update_start_time tests
// ============================================================================

#[test]
fn test_update_start_time_success() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

// Create a stream scheduled to start in the future.
let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
let stream_before = client.get_stream(&stream_id);
assert_eq!(stream_before.start_time, 2000);

// Move the ledger forward so new_start_time (2500) is in the future.
env.ledger().with_mut(|l| l.timestamp = 1000);

// Update the start time to a new future time before end_time (3000).
client.update_start_time(&stream_id, &sender, &2500);

let stream_after = client.get_stream(&stream_id);
assert_eq!(stream_after.start_time, 2500);
}

#[test]
fn test_update_start_time_emits_event() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
env.ledger().with_mut(|l| l.timestamp = 1000);
client.update_start_time(&stream_id, &sender, &2500);

let last_event = env.events().all().last().unwrap();
assert_eq!(last_event.0, contract_id);
assert_eq!(
last_event.1,
(symbol_short!("Stream"), symbol_short!("StartUp")).into_val(&env)
);

let event_data: StreamStartTimeUpdated = last_event.2.into_val(&env);
assert_eq!(event_data.stream_id, stream_id);
assert_eq!(event_data.sender, sender);
assert_eq!(event_data.old_start_time, 2000);
assert_eq!(event_data.new_start_time, 2500);
assert_eq!(event_data.updated_at, 1000);
}

#[test]
#[should_panic(expected = "sender mismatch")]
fn test_update_start_time_fails_with_wrong_sender() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let wrong_sender = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
env.ledger().with_mut(|l| l.timestamp = 1000);
client.update_start_time(&stream_id, &wrong_sender, &2500);
}

#[test]
#[should_panic(expected = "new_start_time must be in the future")]
fn test_update_start_time_rejects_past_timestamp() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
env.ledger().with_mut(|l| l.timestamp = 2500);
// 2000 is not strictly greater than now (2500)
client.update_start_time(&stream_id, &sender, &2000);
}

#[test]
#[should_panic(expected = "new_start_time must be before stream end_time")]
fn test_update_start_time_rejects_at_end_time() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
env.ledger().with_mut(|l| l.timestamp = 1000);
// Setting new_start_time equal to end_time (3000) must be rejected.
client.update_start_time(&stream_id, &sender, &3000);
}

#[test]
#[should_panic(expected = "new_start_time must be before stream end_time")]
fn test_update_start_time_rejects_after_end_time() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
env.ledger().with_mut(|l| l.timestamp = 1000);
client.update_start_time(&stream_id, &sender, &3500);
}

#[test]
#[should_panic(expected = "stream canceled")]
fn test_update_start_time_rejects_on_canceled_stream() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarStreamContract);
let client = StellarStreamContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let token = create_token(&env, &admin);
let token_admin = token::StellarAssetClient::new(&env, &token);
token_admin.mint(&sender, &1000);

let stream_id = client.create_stream(&sender, &recipient, &token, &1000, &2000, &3000, &0, &None);
client.cancel(&stream_id, &sender);
env.ledger().with_mut(|l| l.timestamp = 500);
client.update_start_time(&stream_id, &sender, &2500);
}