Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ members = [
]

[workspace.dependencies]
soroban-sdk = "23.0.1"
soroban-sdk = "=23.0.1"
proptest = "1.0"
rand = "0.8"

Expand Down
17 changes: 17 additions & 0 deletions bindings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,22 @@ export interface Client {
*/
get_recent_archived_rounds: ({limit}: {limit: u32}, options?: MethodOptions) => Promise<AssembledTransaction<Array<ArchivedRoundSummary>>>

/**
* Construct and simulate a get_user_archive_history transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
* Returns paginated archived participation history for a user (newest first).
*
* Reads the user's on-chain index of archived round IDs, applies offset/limit
* pagination, and resolves each ID to its ArchivedRoundSummary. Stale
* entries (rounds pruned by FIFO retention) are silently skipped.
*
* Standard pagination semantics:
* - offset past the end → empty page
* - limit == 0 → empty page
* - limit capped at MAX_PAGE_SIZE (100)
* - Ordering is newest-first (descending round ID)
*/
get_user_archive_history: ({user, offset, limit}: {user: string, offset: u32, limit: u32}, options?: MethodOptions) => Promise<AssembledTransaction<Array<ArchivedRoundSummary>>>

/**
* Construct and simulate a place_precision_prediction transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
* Places a precision prediction on the active round (Precision/Legends mode only)
Expand Down Expand Up @@ -1320,6 +1336,7 @@ export class Client extends ContractClient {
get_updown_positions_page: this.txFromJSON<Array<readonly [string, UserPosition]>>,
get_oracle_stale_threshold: this.txFromJSON<u64>,
get_recent_archived_rounds: this.txFromJSON<Array<ArchivedRoundSummary>>,
get_user_archive_history: this.txFromJSON<Array<ArchivedRoundSummary>>,
place_precision_prediction: this.txFromJSON<Result<void>>,
schedule_max_user_exposure: this.txFromJSON<Result<void>>,
set_oracle_stale_threshold: this.txFromJSON<Result<void>>,
Expand Down
9 changes: 9 additions & 0 deletions contracts/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ impl VirtualTokenContract {
admin::set_runtime_mode(env, mode)
}

/// Returns paginated archived participation history for a user (newest first).
pub fn get_user_archive_history(
env: Env,
user: Address,
offset: u32,
limit: u32,
) -> Vec<ArchivedRoundSummary> {
queries::get_user_archive_history(env, user, offset, limit)
/// Returns whether `action` is currently permitted under the PolicyGate
/// for the contract's runtime mode (Issue #261). Read-only; does not
/// mutate state. See [`admin::_policy_gate`] for the full matrix.
Expand Down Expand Up @@ -1127,6 +1135,7 @@ impl VirtualTokenContract {
limit: u32,
) -> Vec<(Address, UserPosition)> {
queries::get_updown_positions_page(env, offset, limit)

}

/// Returns user's vXLM balance
Expand Down
47 changes: 47 additions & 0 deletions contracts/src/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,53 @@ pub fn get_user_archived_participation(
env.storage().persistent().get(&key)
}

/// Returns paginated archived participation history for a user (newest first).
pub fn get_user_archive_history(
env: Env,
user: Address,
offset: u32,
limit: u32,
) -> Vec<ArchivedRoundSummary> {
let env_ref = &env;
let limit = limit.min(MAX_PAGE_SIZE);
if limit == 0 {
return Vec::new(env_ref);
}

let user_rounds: Vec<u64> = env
.storage()
.persistent()
.get(&DataKey::UserArchivedRoundIds(user))
.unwrap_or(Vec::new(env_ref));

let total = user_rounds.len();
if offset >= total {
return Vec::new(env_ref);
}

let start = total.saturating_sub(offset + 1);
let end = start.saturating_sub(limit.saturating_sub(1));
let mut idx = start;
let mut result: Vec<ArchivedRoundSummary> = Vec::new(env_ref);
loop {
if let Some(round_id) = user_rounds.get(idx) {
if let Some(summary) = env
.storage()
.persistent()
.get(&DataKey::ArchivedRound(round_id))
{
result.push_back(summary);
}
}
if idx == end || result.len() as u32 == limit {
break;
}
idx = idx.saturating_sub(1);
}

result
}

/// Returns user statistics (wins, losses, streaks)
pub fn get_user_stats(env: Env, user: Address) -> UserStats {
let key = DataKeyScoped::UserStats(user);
Expand Down
28 changes: 26 additions & 2 deletions contracts/src/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ pub fn cancel_round(env: Env, _reason: u32) -> Result<(), ContractError> {
&round,
RoundArchiveStatus::Cancelled,
0,
participant_count,
&participants,
0,
None,
);
Expand Down Expand Up @@ -917,6 +917,9 @@ fn _settle_round_with_price(
let threshold_participants: Vec<Address> = env
.storage()
.persistent()
.get(&DataKey::RoundParticipants(round_id))
.unwrap_or(Vec::new(&env));
if threshold_participants.len() < min {
.get(&DataKeyScoped::RoundParticipants(round_id))
.unwrap_or(Vec::new(env));
let count = threshold_participants.len();
Expand All @@ -925,6 +928,8 @@ fn _settle_round_with_price(
env,
round,
RoundArchiveStatus::FallbackRefund,
payload.price,
&threshold_participants,
final_price,
count,
0,
Expand Down Expand Up @@ -960,6 +965,8 @@ fn _settle_round_with_price(
let participants: Vec<Address> = env
.storage()
.persistent()
.get(&DataKey::RoundParticipants(round_id))
.unwrap_or(Vec::new(&env));
.get(&DataKeyScoped::RoundParticipants(round_id))
.unwrap_or(Vec::new(env));
let participant_count = participants.len();
Expand All @@ -968,6 +975,8 @@ fn _settle_round_with_price(
env,
round,
RoundArchiveStatus::Resolved,
payload.price,
&participants,
final_price,
participant_count,
fee_amount,
Expand Down Expand Up @@ -1753,11 +1762,12 @@ pub fn _archive_round(
round: &Round,
status: RoundArchiveStatus,
final_price: u128,
participant_count: u32,
participants: &[Address],
fee_amount: i128,
confidence: Option<u32>,
) {
let status_val = status.clone() as u32;
let participant_count = participants.len() as u32;
let settled_at_ledger = env.ledger().sequence();
let summary = ArchivedRoundSummary {
round_id: round.round_id,
Expand All @@ -1771,6 +1781,20 @@ pub fn _archive_round(
settled_at_ledger,
};

// Record per-user participation index for paginated history queries.
for i in 0..participants.len() {
if let Some(user) = participants.get(i) {
let index_key = DataKey::UserArchivedRoundIds(user.clone());
let mut user_rounds: Vec<u64> = env
.storage()
.persistent()
.get(&index_key)
.unwrap_or(Vec::new(env));
user_rounds.push_back(round.round_id);
env.storage().persistent().set(&index_key, &user_rounds);
}
}

env.storage()
.persistent()
.set(&DataKeyScoped::ArchivedRound(round.round_id), &summary);
Expand Down
Loading