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
41 changes: 0 additions & 41 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,44 +72,3 @@ jobs:
- name: Test
# --locked: same rationale as Clippy above — fail loudly on lockfile drift.
run: cargo test --workspace --locked

frontend:
name: frontend · typecheck · lint · build · test
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
# Pin to v4 major version tag and specify Node.js version explicitly
# to decouple action upgrades from runtime version pinning.
# Matches the project's @types/node: ^20 dependency.
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci

- name: TypeScript typecheck
run: npx tsc --noEmit

- name: Lint
run: npm run lint

- name: Build
run: npm run build

- name: Test
run: npm run test -- --run

# No-op trigger: re-run CI on PR #35 to verify whether the failure mode persists across two clean runs.
# Useful for distinguishing a transient cache issue from a real toolchain-action regression.

# No-op trigger: re-run CI on PR #35 to verify whether the failure mode persists
# across two clean runs. Useful for distinguishing a transient cache issue from a real
# toolchain-action regression. No semantic change.
78 changes: 54 additions & 24 deletions bin/backfill-operation-index/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@

use anyhow::{anyhow, Context, Result};
use clap::Parser;
use octo_store::Store;
use octo_ingest::operation_index_from_toid;
use octo_store::Store;
use sqlx::Row;
use tracing::{info, warn, error};
use tracing::{error, info, warn};

#[derive(Parser)]
#[command(name = "backfill-operation-index")]
Expand Down Expand Up @@ -66,7 +66,10 @@ impl BackfillStats {
info!(" Total examined: {}", self.total_examined);
info!(" Needs update: {}", self.needs_update);
info!(" Updated: {}", self.updated);
info!(" Skipped (already correct): {}", self.skipped_already_correct);
info!(
" Skipped (already correct): {}",
self.skipped_already_correct
);
info!(" Skipped (invalid TOID): {}", self.skipped_invalid_toid);
info!(" Errors: {}", self.errors);
}
Expand Down Expand Up @@ -95,13 +98,13 @@ async fn find_candidates_batch(
.context("Failed to fetch candidate rows")?;

let mut candidates = Vec::new();

for row in rows {
let id: uuid::Uuid = row.get("id");
let horizon_op_id: String = row.get("horizon_op_id");
let current_operation_index: i32 = row.get("operation_index");
let stellar_tx_hash: Option<String> = row.get("stellar_tx_hash");

if let Some(new_operation_index) = operation_index_from_toid(&horizon_op_id) {
if new_operation_index != current_operation_index {
candidates.push(UpdateCandidate {
Expand All @@ -114,7 +117,7 @@ async fn find_candidates_batch(
}
}
}

Ok(candidates)
}

Expand All @@ -126,7 +129,7 @@ async fn update_batch(
if candidates.is_empty() {
return Ok(0);
}

if dry_run {
for candidate in candidates {
info!(
Expand All @@ -142,19 +145,21 @@ async fn update_batch(
}

let mut updated = 0;

// Use a transaction to ensure consistency
let pool = store.pool();
let mut tx = pool.begin().await
let mut tx = pool
.begin()
.await
.context("Failed to begin database transaction")?;

for candidate in candidates {
// Double-check the current value hasn't changed since we selected it
// and update atomically
let result = sqlx::query(
"UPDATE transactions
SET operation_index = $1, updated_at = now()
WHERE id = $2 AND operation_index = $3 AND horizon_op_id = $4"
WHERE id = $2 AND operation_index = $3 AND horizon_op_id = $4",
)
.bind(candidate.new_operation_index)
.bind(candidate.id)
Expand All @@ -163,7 +168,7 @@ async fn update_batch(
.execute(&mut *tx)
.await
.context("Failed to update transaction")?;

if result.rows_affected() == 1 {
updated += 1;
info!(
Expand All @@ -180,21 +185,33 @@ async fn update_batch(
);
}
}

tx.commit().await

tx.commit()
.await
.context("Failed to commit database transaction")?;

Ok(updated)
}

async fn run_backfill(args: Args) -> Result<()> {
info!("Starting operation_index backfill");
info!("Database URL: {}", args.database_url.chars().take(20).collect::<String>() + "...");
info!(
"Database URL: {}",
args.database_url.chars().take(20).collect::<String>() + "..."
);
info!("Batch size: {}", args.batch_size);
info!("Dry run: {}", args.dry_run);
info!("Limit: {}", if args.limit == 0 { "unlimited".to_string() } else { args.limit.to_string() });
info!(
"Limit: {}",
if args.limit == 0 {
"unlimited".to_string()
} else {
args.limit.to_string()
}
);

let store = Store::connect(&args.database_url).await
let store = Store::connect(&args.database_url)
.await
.context("Failed to connect to database")?;

let mut stats = BackfillStats::default();
Expand All @@ -208,15 +225,19 @@ async fn run_backfill(args: Args) -> Result<()> {
} else {
args.batch_size
};

if current_batch_size == 0 {
info!("Reached the specified limit of {} records", args.limit);
break;
}

info!("Processing batch starting at offset {} (batch size: {})", offset, current_batch_size);
info!(
"Processing batch starting at offset {} (batch size: {})",
offset, current_batch_size
);

let candidates = find_candidates_batch(&store, current_batch_size, offset).await
let candidates = find_candidates_batch(&store, current_batch_size, offset)
.await
.context("Failed to find candidate records")?;

if candidates.is_empty() {
Expand All @@ -229,7 +250,10 @@ async fn run_backfill(args: Args) -> Result<()> {

// Log some examples of what we're about to update
if !candidates.is_empty() {
info!("Found {} records needing updates in this batch:", candidates.len());
info!(
"Found {} records needing updates in this batch:",
candidates.len()
);
for (i, candidate) in candidates.iter().take(3).enumerate() {
info!(
" {}: {} -> {} (tx_hash: {}, toid: {})",
Expand All @@ -248,7 +272,10 @@ async fn run_backfill(args: Args) -> Result<()> {
match update_batch(&store, &candidates, args.dry_run).await {
Ok(updated_count) => {
stats.updated += updated_count;
info!("Successfully processed {} records in this batch", updated_count);
info!(
"Successfully processed {} records in this batch",
updated_count
);
}
Err(e) => {
error!("Error updating batch: {}", e);
Expand Down Expand Up @@ -330,7 +357,10 @@ mod tests {
// splits the string into 4 hyphen-delimited parts (not 3), so this is correctly rejected
// by the same "exactly 3 parts" check that rejects any other malformed TOID shape.
assert_eq!(operation_index_from_toid("12345-1--1"), None);
assert_eq!(operation_index_from_toid("12345-1-2147483647"), Some(i32::MAX));
assert_eq!(
operation_index_from_toid("12345-1-2147483647"),
Some(i32::MAX)
);
assert_eq!(operation_index_from_toid("12345-1-2147483648"), None);
}
}
26 changes: 22 additions & 4 deletions crates/api/src/routes/payment_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ pub async fn list_payment_links(
if has_more {
items.truncate(limit as usize);
}
let next_cursor = if has_more { items.last().map(|l| l.id) } else { None };
let next_cursor = if has_more {
items.last().map(|l| l.id)
} else {
None
};

let ids: Vec<Uuid> = items.iter().map(|l| l.id).collect();
let totals: std::collections::HashMap<Uuid, i64> = state
Expand Down Expand Up @@ -228,7 +232,11 @@ pub async fn get_public_payment_link(
if !link.active {
return Err(ApiError::NotFound);
}
let address = state.store().get_address(link.address_id).await?.ok_or(ApiError::NotFound)?;
let address = state
.store()
.get_address(link.address_id)
.await?
.ok_or(ApiError::NotFound)?;
Ok(Envelope::ok(PublicPaymentLinkView {
name: link.name,
description: link.description,
Expand Down Expand Up @@ -283,7 +291,11 @@ pub async fn create_payment_intent(
amount,
)
.await?;
let address = state.store().get_address(link.address_id).await?.ok_or(ApiError::NotFound)?;
let address = state
.store()
.get_address(link.address_id)
.await?
.ok_or(ApiError::NotFound)?;

let (code, json) = Envelope::created(PaymentIntentView {
payment_id: payment.id,
Expand Down Expand Up @@ -337,7 +349,13 @@ pub async fn public_signing_info(

let account = match params.account {
Some(a) => a,
None => state.store().get_wallet(link.wallet_id).await?.stellar_account_g,
None => {
state
.store()
.get_wallet(link.wallet_id)
.await?
.stellar_account_g
}
};
let sequence = state.horizon().account_sequence(&account).await?;
Ok(Envelope::ok(crate::routes::submit::SigningInfo {
Expand Down
4 changes: 3 additions & 1 deletion crates/api/src/routes/sponsor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ pub async fn sponsor(
};
// Keep the versioned-scheme path (PR #158) so master-key rotation keeps working. Rows
// written before the scheme tag existed fall back to V1.
let scheme = wallet.sealed_scheme.unwrap_or(octo_crypto::SCHEME_V1 as i16);
let scheme = wallet
.sealed_scheme
.unwrap_or(octo_crypto::SCHEME_V1 as i16);
let sealed = SealedSeed::from_parts_with_scheme(ciphertext.clone(), nonce, salt, scheme as u8)
.map_err(|_| ApiError::Internal)?;
let fb = FeeBumpRequest {
Expand Down
3 changes: 1 addition & 2 deletions crates/api/src/routes/wallets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,7 @@ pub async fn create_gas_tank(

// Provision a fresh keypair inside wallet-core. The mnemonic is deliberately dropped: the
// tank is a disposable fee account, recoverable only by re-provisioning.
let provisioned =
octo_wallet_core::provision_wallet(state.master_key(), state.network())?;
let provisioned = octo_wallet_core::provision_wallet(state.master_key(), state.network())?;
let wallet = state
.store()
.set_gas_tank(
Expand Down
6 changes: 3 additions & 3 deletions crates/api/src/sponsor_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ mod tests {
InvokeHostFunctionOp, LiquidityPoolDepositOp, LiquidityPoolWithdrawOp, ManageBuyOfferOp,
ManageDataOp, ManageSellOfferOp, Memo, MuxedAccount, Operation, OperationBody,
PathPaymentStrictReceiveOp, PathPaymentStrictSendOp, PaymentOp, Preconditions,
RestoreFootprintOp, RevokeSponsorshipOp, SequenceNumber, SetOptionsOp,
SetTrustLineFlagsOp, Transaction, TransactionEnvelope, TransactionExt,
TransactionV1Envelope, Uint256, XDRSerialize,
RestoreFootprintOp, RevokeSponsorshipOp, SequenceNumber, SetOptionsOp, SetTrustLineFlagsOp,
Transaction, TransactionEnvelope, TransactionExt, TransactionV1Envelope, Uint256,
XDRSerialize,
};

const VECTOR_MK: [u8; 32] = [7u8; 32];
Expand Down
18 changes: 13 additions & 5 deletions crates/api/src/submit_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
//! no I/O — so it can be unit-tested independently of the database or Horizon.

use crate::error::ApiError;
use stellar_base::xdr::{
Asset, MuxedAccount, OperationBody, TransactionEnvelope, XDRDeserialize,
};
use stellar_base::xdr::{Asset, MuxedAccount, OperationBody, TransactionEnvelope, XDRDeserialize};
use stellar_strkey::ed25519::PublicKey as StrkeyPK;

/// Operation types a client-signed submission may contain.
Expand Down Expand Up @@ -135,11 +133,21 @@ pub(crate) fn asset_parts(asset: &Asset) -> (String, Option<String>) {
Asset::Native => ("native".to_string(), None),
Asset::CreditAlphanum4(a) => (
trimmed_code(&a.asset_code.0),
Some(StrkeyPK(account_bytes(&a.issuer)).to_string().as_str().to_owned()),
Some(
StrkeyPK(account_bytes(&a.issuer))
.to_string()
.as_str()
.to_owned(),
),
),
Asset::CreditAlphanum12(a) => (
trimmed_code(&a.asset_code.0),
Some(StrkeyPK(account_bytes(&a.issuer)).to_string().as_str().to_owned()),
Some(
StrkeyPK(account_bytes(&a.issuer))
.to_string()
.as_str()
.to_owned(),
),
),
}
}
Expand Down
Loading
Loading