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
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ yarn-error.log*
.pnpm-debug.log*

# local env files
.env
.env.local
.env.development.local
.env.test.local
Expand Down Expand Up @@ -83,5 +84,5 @@ __pycache__
# logs
*.log

# e2e predeployed accounts
accounts.json
# e2e predeployed accounts (legacy, replaced by .env ACCOUNTS var)
accounts.json
3 changes: 3 additions & 0 deletions crates/discovery-core/src/storage_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub enum StorageError {
/// Failed to convert value to u64.
#[error("value is too large to convert to u64: {0}")]
CastToU64Error(Felt),
/// The contract does not exist at the queried address/block.
#[error("contract not found")]
ContractNotFound,
/// Backend-specific error.
#[error("{0}")]
Backend(#[source] Box<dyn std::error::Error + Send + Sync>),
Expand Down
26 changes: 21 additions & 5 deletions crates/discovery-service/src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,26 @@ impl ApiErrorResponse {
pub fn discovery_error_to_response(error: DiscoveryError) -> (StatusCode, ApiErrorResponse) {
match error {
DiscoveryError::Storage(storage_err) => {
warn!("Storage error during discovery: {}", storage_err);
(
StatusCode::SERVICE_UNAVAILABLE,
ApiErrorResponse::new(error_codes::RPC_UNAVAILABLE, "Upstream RPC is unavailable"),
)
use discovery_core::storage_backend::StorageError;
if matches!(storage_err, StorageError::ContractNotFound) {
warn!("Contract not found during discovery");
(
StatusCode::NOT_FOUND,
ApiErrorResponse::new(
error_codes::CONTRACT_NOT_FOUND,
"Contract not found at the configured address",
),
)
} else {
warn!("Storage error during discovery: {}", storage_err);
(
StatusCode::SERVICE_UNAVAILABLE,
ApiErrorResponse::new(
error_codes::RPC_UNAVAILABLE,
"Upstream RPC is unavailable",
),
)
}
}
DiscoveryError::Decryption { index, source } => (
StatusCode::BAD_REQUEST,
Expand Down Expand Up @@ -264,6 +279,7 @@ pub mod error_codes {
pub const DECRYPTION_FAILED: &str = "DECRYPTION_FAILED";
pub const BLOCK_REORGED: &str = "BLOCK_REORGED";
pub const SERVICE_UNAVAILABLE: &str = "SERVICE_UNAVAILABLE";
pub const CONTRACT_NOT_FOUND: &str = "CONTRACT_NOT_FOUND";
pub const RPC_UNAVAILABLE: &str = "RPC_UNAVAILABLE";
pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
}
41 changes: 37 additions & 4 deletions crates/discovery-service/src/rpc_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use async_trait::async_trait;
use discovery_core::storage_backend::{
RawStorageAccess, StorageBackend, StorageError, StorageSnapshot,
};
use starknet_core::types::{requests::GetStorageAtRequest, BlockId, BlockTag, Felt, StarknetError};
use starknet_core::types::{
requests::GetStorageAtRequest, BlockId, BlockTag, Felt, MaybePreConfirmedBlockWithTxHashes,
StarknetError,
};
use starknet_providers::{
jsonrpc::{HttpTransport, JsonRpcClient},
Provider, ProviderError, ProviderRequestData, ProviderResponseData,
Expand Down Expand Up @@ -129,7 +132,12 @@ impl RpcSnapshot {
.provider
.batch_requests(&requests)
.await
.map_err(|e| RpcBackendError::Request(e.to_string()))?;
.map_err(|e| match &e {
ProviderError::StarknetError(StarknetError::ContractNotFound) => {
StorageError::ContractNotFound
}
_ => StorageError::from(RpcBackendError::Request(e.to_string())),
})?;

responses
.into_iter()
Expand All @@ -149,7 +157,12 @@ impl RawStorageAccess for RpcSnapshot {
.provider
.get_storage_at(self.contract_address, slot, self.block_id)
.await
.map_err(|e| RpcBackendError::Request(e.to_string()).into())
.map_err(|e| match &e {
ProviderError::StarknetError(StarknetError::ContractNotFound) => {
StorageError::ContractNotFound
}
_ => RpcBackendError::Request(e.to_string()).into(),
})
}

async fn read_slots(&self, slots: Vec<Felt>) -> Result<Vec<Felt>, StorageError> {
Expand All @@ -173,7 +186,27 @@ impl RawStorageAccess for RpcSnapshot {
#[async_trait]
impl ChainState for RpcBackend {
async fn get_head(&self) -> Option<ChainHead> {
*self.inner.head.read().await
if let Some(head) = *self.inner.head.read().await {
return Some(head);
}

// Fallback: fetch latest block via RPC when WS subscription hasn't
// provided a head yet (e.g. WS not available on the node).
let block = self
.inner
.provider
.get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest))
.await
.ok()?;

match block {
MaybePreConfirmedBlockWithTxHashes::Block(head) => Some(ChainHead {
block_number: head.block_number,
block_hash: head.block_hash,
timestamp: head.timestamp,
}),
MaybePreConfirmedBlockWithTxHashes::PreConfirmedBlock(_) => None,
}
}

async fn set_head(&self, head: ChainHead) {
Expand Down
27 changes: 27 additions & 0 deletions crates/discovery-service/tests/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,40 @@ async fn test_incoming_sync_no_head() {
let (status, error) = indexer.incoming_sync_error(&request).await.unwrap();

// Should return 503 SERVICE_UNAVAILABLE since no head is indexed yet
// (fresh devnet genesis block is pre-confirmed, so the RPC fallback
// returns None and the validator rejects with SERVICE_UNAVAILABLE)
assert_eq!(status, 503);
assert_eq!(error.error.code, "SERVICE_UNAVAILABLE");

indexer.signal_shutdown().unwrap();
indexer.wait().await.unwrap();
}

#[tokio::test]
async fn test_incoming_sync_contract_not_found() {
let (devnet, metadata) = setup_devnet_with_dump().await;
let indexer = setup_indexer(&devnet, Some(&metadata)).await;

let request = IncomingSyncRequest {
recipient_address: Felt::from_hex("0x1234").unwrap(),
base: SyncRequestBase {
contract_address: Felt::from_hex("0xdeadbeef").unwrap(),
viewing_key: Felt::from_hex("0x5678").unwrap(),
last_known_block: None,
block_ref: None,
cursor: Default::default(),
},
};

let (status, error) = indexer.incoming_sync_error(&request).await.unwrap();

assert_eq!(status, 404);
assert_eq!(error.error.code, "CONTRACT_NOT_FOUND");

indexer.signal_shutdown().unwrap();
indexer.wait().await.unwrap();
}

#[tokio::test]
async fn test_outgoing_sync_basic() {
let (devnet, metadata) = setup_devnet_with_dump().await;
Expand Down
18 changes: 18 additions & 0 deletions e2e/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# RPC endpoints (integration Sepolia)
RPC_URL=http://localhost:9545/rpc/v0_10
WS_URL=ws://localhost:9545/ws/rpc/v0_8

# ERC-20 token contract address
TOKEN_ADDRESS=0x0

# Chain ID (hex-encoded felt, e.g. SN_INTEGRATION_SEPOLIA)
CHAIN_ID=0x534e5f494e544547524154494f4e5f5345504f4c4941

# Privacy pool class hash (already declared on-chain)
POOL_CLASS_HASH=0x0

# Compliance public key
COMPLIANCE_PUBLIC_KEY=0x0

# Accounts (JSON array with name, address, privateKey, viewingKey)
ACCOUNTS=[{"name":"admin","address":"0x0","privateKey":"0x0","viewingKey":"0x0"},{"name":"alice","address":"0x0","privateKey":"0x0","viewingKey":"0x0"}]
28 changes: 28 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,31 @@ This writes fixtures directly to:
- `crates/discovery-service/tests/fixtures/devnet-dump.metadata.json` -- timestamp + addresses

After regenerating, run `cargo test` from the repo root to confirm Rust tests still pass.

## Privacy StarkNet integration (`tests/privacy-starknet-integration.test.ts`)

Tests against a real (non-devnet) StarkNet deployment on integration sepolia.
Spawns the discovery service indexer, runs preflight and deposit flows via the SDK.

Requires network access and a `.env` file with account credentials and contract addresses.

### Setting up `.env`

1. Copy the example file: `cp .env.example .env`
2. Fill in real values from the shared team document (search for "PrivacyDummyAccount")

The `ACCOUNTS` env var is a JSON array of account entries:

```json
[
{"name": "admin", "address": "0x...", "privateKey": "0x...", "viewingKey": "0x..."},
{"name": "alice", "address": "0x...", "privateKey": "0x...", "viewingKey": "0x..."}
]
```

The test uses `admin` as the minter (OZ account) and `alice` as the privacy account.
`alice` uses the `PrivacyDummyAccount` class (trivial signer, no signature required).

```bash
npx vitest run tests/privacy-starknet-integration.test.ts
```
5 changes: 3 additions & 2 deletions e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"starknet": "^9.2.1",
"starknet": "github:m-kus/starknet.js#375736f96cf928b1555407767d96a7751b546808",
"starknet-sdk": "file:../sdk"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion e2e/src/indexer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class IndexerClient {
...(process.env as Record<string, string>),
WS_URL: config.wsUrl,
API_HOST: `127.0.0.1:${port}`,
RUST_LOG: "debug,hyper_util=warn,hyper=warn",
RUST_LOG: process.env.RUST_LOG ?? "debug,hyper_util=warn,hyper=warn",
};
if (config.rpcUrl) env.RPC_URL = config.rpcUrl;

Expand Down
Loading