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
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,13 @@ Validation scenarios compare Superbank RPC responses against a reference endpoin
REFERENCE_RPC_URL=https://api.mainnet-beta.solana.com scripts/test/run-k6.sh
```

`getTransactionsForAddress` is a Superbank-specific method, so its validation scenario uses
`TFA_REFERENCE_RPC_URL` instead of the standard Solana reference endpoint:
`getTransactionsForAddress` and `getTransfersByAddress` are Superbank-specific methods, so its validation scenarios use
`TFA_REFERENCE_RPC_URL` and `TBA_REFERENCE_RPC_URL` instead of the standard Solana reference endpoint:

```bash
REFERENCE_RPC_URL=https://api.mainnet-beta.solana.com \
TFA_REFERENCE_RPC_URL=http://localhost:8898 \
TBA_REFERENCE_RPC_URL=http://localhost:8898 \
scripts/test/run-k6.sh
```

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ ClickHouse.

For single-node ClickHouse (local dev), apply the schemas under `ddl/local/` in this order.
`transactions.sql` must be applied before the materialized-view schemas (`gsfa*.sql`,
`signatures.sql`, and `token_owner_activity.sql`) because those views read from the transactions
table.
`signatures.sql`, `token_owner_activity.sql`, and `transfers.sql`) because those views
read from the transactions table.

```bash
cat ddl/local/transactions.sql | docker exec -i clickhouse clickhouse-client --multiquery
Expand All @@ -97,6 +97,7 @@ cat ddl/local/gsfa.sql | docker exec -i clickhouse clickhouse-client --multiquer
cat ddl/local/signatures.sql | docker exec -i clickhouse clickhouse-client --multiquery
# Optional: required only for `tokenAccounts` filters in `getTransactionsForAddress`.
cat ddl/local/token_owner_activity.sql | docker exec -i clickhouse clickhouse-client --multiquery
cat ddl/local/transfers.sql | docker exec -i clickhouse clickhouse-client --multiquery
```

If you use `gsfa_hot.sql` and want hot addresses excluded from the main GSFA table, apply
Expand Down
7 changes: 6 additions & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ data:
{signatures}
token_owner_activity.sql: |
{token_owner_activity}
transfers.sql: |
{transfers}
""".format(
namespace=namespace,
transactions=_indent_block(read_file("ddl/local/transactions.sql"), 4),
Expand All @@ -126,6 +128,7 @@ data:
gsfa_hot=_indent_block(read_file("ddl/local/gsfa_hot.sql"), 4),
signatures=_indent_block(read_file("ddl/local/signatures.sql"), 4),
token_owner_activity=_indent_block(read_file("ddl/local/token_owner_activity.sql"), 4),
transfers=_indent_block(read_file("ddl/local/transfers.sql"), 4),
)
)

Expand Down Expand Up @@ -297,7 +300,8 @@ for f in \\
ddl/local/gsfa.sql \\
ddl/local/gsfa_hot.sql \\
ddl/local/signatures.sql \\
ddl/local/token_owner_activity.sql
ddl/local/token_owner_activity.sql \\
ddl/local/transfers.sql
do
echo "[apply-clickhouse-schema] Applying $f..."
cat "$f" | kubectl -n "$ns" exec -i "$pod" -c clickhouse -- clickhouse-client --user "$ch_user" --password "$ch_password" --multiquery
Expand All @@ -313,6 +317,7 @@ echo "[apply-clickhouse-schema] Done."
"ddl/local/gsfa_hot.sql",
"ddl/local/signatures.sql",
"ddl/local/token_owner_activity.sql",
"ddl/local/transfers.sql",
],
resource_deps=["clickhouse"],
trigger_mode=TRIGGER_MODE_MANUAL,
Expand Down
20 changes: 18 additions & 2 deletions crates/superbank-rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ writer that matches the same schemas).
- `getInflationReward`
- `getEpochSchedule`
- `getTransactionsForAddress` (custom)
- `getTransfersByAddress` (custom)

Notes:
- JSON-RPC batch envelopes are supported. Batch execution is bounded by
Expand Down Expand Up @@ -72,6 +73,15 @@ Notes:
these aliases cannot be combined with same-side slot filters (`lt`/`lte` for `beforeSlot`,
`gt`/`gte` for `untilSlot`).
Token account filters require the token-owner activity table (see below).
- `getTransfersByAddress` supports historical SOL/SPL transfers with
top-level `with`, `direction=in|out`, `mint`, `solMode=merged|separate`, `sortOrder=asc|desc`,
`limit`, `paginationToken` (`slot:transactionIdx:instructionIdx:innerInstructionIdx:type`),
`commitment`, `minContextSlot`, and `filters.amount|slot|blockTime`. Amounts are raw integer
strings (lamports for SOL, token base units for SPL), with `uiAmount` derived from decimals.
The index is derived from pre/post balance deltas, so the opposite user account is nullable when
no counterparty can be inferred. This method requires the transfers table (see below).
Response `paginationToken` is non-null only when another matching page exists after all filters
are applied; the token points to the last row returned by the current page.

## ClickHouse schemas

Expand All @@ -89,10 +99,12 @@ Required files in the chosen set:
Optional:
- `gsfa_hot.sql` when using hot-address routing.
- `token_owner_activity.sql` when using token-owner filters in `getTransactionsForAddress`.
- `transfers.sql` when using `getTransfersByAddress`.

Apply `transactions.sql` before the materialized-view schemas (`gsfa*.sql`, `signatures.sql`, and
`token_owner_activity.sql`) because those views read from the transactions table. If you use
`gsfa_hot.sql`, apply `gsfa_nohot.sql` instead of `gsfa.sql`, then apply `gsfa_hot.sql`.
`token_owner_activity.sql`, `transfers.sql`) because those views read from the
transactions table. If you use `gsfa_hot.sql`, apply `gsfa_nohot.sql` instead of `gsfa.sql`, then
apply `gsfa_hot.sql`.

## Run

Expand Down Expand Up @@ -391,6 +403,7 @@ CLI flags and environment variables (see `crates/superbank-rpc/src/config.rs`):
| `--clickhouse-gsfa-hot-local-table` | `CLICKHOUSE_GSFA_HOT_LOCAL_TABLE` | `default.gsfa_hot_local` | Shard-local backing table behind `CLICKHOUSE_GSFA_HOT_TABLE`. |
| `--clickhouse-signatures-local-table` | `CLICKHOUSE_SIGNATURES_LOCAL_TABLE` | — | — |
| `--clickhouse-token-owner-activity-local-table` | `CLICKHOUSE_TOKEN_OWNER_ACTIVITY_LOCAL_TABLE` | — | — |
| `--clickhouse-transfers-local-table` | `CLICKHOUSE_TRANSFERS_LOCAL_TABLE` | — | — |
| `--clickhouse-transactions-local-table` | `CLICKHOUSE_TRANSACTIONS_LOCAL_TABLE` | — | — |
| `--clickhouse-blocks-metadata-local-table` | `CLICKHOUSE_BLOCKS_METADATA_LOCAL_TABLE` | — | — |
| `--clickhouse-shard-http-port` | `CLICKHOUSE_SHARD_HTTP_PORT` | — | — |
Expand All @@ -406,6 +419,8 @@ Table selection (environment variables, read at startup):
| `CLICKHOUSE_GSFA_HOT_TABLE` | `default.gsfa_hot` | — |
| `CLICKHOUSE_SIGNATURE_STATUSES_TABLE` | `default.signatures` | — |
| `CLICKHOUSE_TOKEN_OWNER_ACTIVITY_TABLE` | `default.token_owner_activity` | — |
| `CLICKHOUSE_TRANSFERS_TABLE` | `default.transfers` | Transfers ledger used by `getTransfersByAddress`. |
| `CLICKHOUSE_TRANSFERS_BY_ADDRESS_TABLE` | — | Legacy alias for `CLICKHOUSE_TRANSFERS_TABLE`. |

Shard routing:
When `CLICKHOUSE_SCOPE=shard-direct`, superbank-rpc discovers shards from `system.clusters` and
Expand Down Expand Up @@ -476,6 +491,7 @@ Additional env flags:

- Scope: `superbank-rpc` applies `use_query_condition_cache=1` only on selected historical address-filtered reads:
- `getTransactionsForAddress`
- `getTransfersByAddress`
- the transactions-table fallback path for `getSignaturesForAddress`
- Point lookups and slot-range reads do not opt in.
- This setting is enabled separately from the query-result cache via:
Expand Down
96 changes: 96 additions & 0 deletions crates/superbank-rpc/src/clickhouse/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use super::constants::DEFAULT_BUCKET_MODULUS;
use super::gsfa::GsfaShardRouter;
use super::queries::{
GSFA_REQUIRED_COLUMNS, SIGNATURES_REQUIRED_COLUMNS, TOKEN_OWNER_REQUIRED_COLUMNS,
TRANSFERS_REQUIRED_COLUMNS,
};
use super::sharding::{
BucketColumn, ClusterRow, DescribeTableRow, RoutingPolicy, RoutingScope, RoutingTransport,
Expand Down Expand Up @@ -416,11 +417,14 @@ pub struct ClickHouseClient {
pub(crate) gsfa_hot_pubkeys: HashSet<Pubkey>,
pub(crate) signature_statuses_table: String,
pub(crate) token_owner_activity_table: String,
pub(crate) transfers_table: String,
pub(crate) signatures_local_table: Option<String>,
pub(crate) token_owner_activity_local_table: Option<String>,
pub(crate) transfers_local_table: Option<String>,
pub(crate) transactions_local_table: Option<String>,
pub(crate) blocks_metadata_local_table: Option<String>,
pub(crate) token_owner_activity_available: bool,
pub(crate) transfers_available: bool,
pub(crate) bucket_moduli: BucketModuli,
pub(crate) allow_query_settings: bool,
pub(crate) query_cache: QueryCacheConfig,
Expand Down Expand Up @@ -657,6 +661,9 @@ impl ClickHouseClient {
.unwrap_or_else(|_| "default.signatures".to_string());
let token_owner_activity_table = std::env::var("CLICKHOUSE_TOKEN_OWNER_ACTIVITY_TABLE")
.unwrap_or_else(|_| "default.token_owner_activity".to_string());
let transfers_table = std::env::var("CLICKHOUSE_TRANSFERS_TABLE")
.or_else(|_| std::env::var("CLICKHOUSE_TRANSFERS_BY_ADDRESS_TABLE"))
.unwrap_or_else(|_| "default.transfers".to_string());

let signatures_local_table = shard_routing
.as_ref()
Expand All @@ -666,6 +673,10 @@ impl ClickHouseClient {
.as_ref()
.and_then(|config| config.token_owner_activity_local_table.clone())
.or_else(|| derive_local_table_name(&token_owner_activity_table, None));
let transfers_local_table = shard_routing
.as_ref()
.and_then(|config| config.transfers_local_table.clone())
.or_else(|| derive_local_table_name(&transfers_table, None));
let transactions_local_table = shard_routing
.as_ref()
.and_then(|config| config.transactions_local_table.clone())
Expand All @@ -690,6 +701,9 @@ impl ClickHouseClient {
if config.token_owner_activity_local_table.is_none() {
config.token_owner_activity_local_table = token_owner_activity_local_table.clone();
}
if config.transfers_local_table.is_none() {
config.transfers_local_table = transfers_local_table.clone();
}
if config.transactions_local_table.is_none() {
config.transactions_local_table = transactions_local_table.clone();
}
Expand All @@ -715,11 +729,14 @@ impl ClickHouseClient {
gsfa_hot_pubkeys: HashSet::new(),
signature_statuses_table,
token_owner_activity_table,
transfers_table,
signatures_local_table,
token_owner_activity_local_table,
transfers_local_table,
transactions_local_table,
blocks_metadata_local_table,
token_owner_activity_available: true,
transfers_available: true,
bucket_moduli: BucketModuli::default(),
allow_query_settings: !std::env::var("CLICKHOUSE_DISABLE_QUERY_SETTINGS")
.map(|value| env_truthy(&value))
Expand Down Expand Up @@ -1394,6 +1411,68 @@ impl ClickHouseClient {
}
}

let transfers_table = &self.transfers_table;
match self.startup_table_check {
ClickHouseStartupTableCheck::Count => {
match self
.with_timeout("startup_transfers_count", async {
self.client
.query(&format!("SELECT COUNT(*) FROM {}", transfers_table))
.fetch_one::<u64>()
.await
.map_err(|e| ProcessingError::database(e.to_string(), e))
})
.await
{
Ok(count) => {
self.transfers_available = true;
tracing::info!(
"📊 Database initialized - {} table: {} rows",
transfers_table,
count
);
}
Err(e) => {
self.transfers_available = false;
tracing::warn!(
"Transfers-by-address table '{}' unavailable; getTransfersByAddress disabled. Error: {}",
transfers_table,
e
);
}
}
}
ClickHouseStartupTableCheck::Exists => {
match self
.with_timeout("startup_transfers_exists", async {
self.client
.query(&format!("SELECT count() FROM {} WHERE 0", transfers_table))
.fetch_one::<u64>()
.await
.map(|_| ())
.map_err(|e| ProcessingError::database(e.to_string(), e))
})
.await
{
Ok(()) => {
self.transfers_available = true;
tracing::info!(
"📊 Database initialized - {} table accessible",
transfers_table
);
}
Err(e) => {
self.transfers_available = false;
tracing::warn!(
"Transfers-by-address table '{}' unavailable; getTransfersByAddress disabled. Error: {}",
transfers_table,
e
);
}
}
}
}

let blocks_metadata_table = &self.blocks_metadata_table;
match self.startup_table_check {
ClickHouseStartupTableCheck::Count => {
Expand Down Expand Up @@ -1585,6 +1664,22 @@ impl ClickHouseClient {
);
self.token_owner_activity_local_table = None;
}

if let Some(local_table) = config.transfers_local_table.clone()
&& let Err(e) = validate_table_schema_on_shards(
topology.as_ref(),
&local_table,
&TRANSFERS_REQUIRED_COLUMNS,
self.query_timeout,
)
.await
{
tracing::warn!(
"Transfers shard routing disabled; local table validation failed: {}",
e
);
self.transfers_local_table = None;
}
}

if hot_routing_configured {
Expand Down Expand Up @@ -2222,6 +2317,7 @@ nodes:
gsfa_local_table: None,
signatures_local_table: None,
token_owner_activity_local_table: None,
transfers_local_table: None,
transactions_local_table: None,
blocks_metadata_local_table: None,
};
Expand Down
6 changes: 4 additions & 2 deletions crates/superbank-rpc/src/clickhouse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod rows;
mod sharding;
mod signatures;
mod transactions;
mod transfers;
mod types;
mod util;

Expand All @@ -21,9 +22,10 @@ pub use client::{ClickHouseClient, ClickHouseClientOptions, InflationRewardQuery
pub use types::TransactionsForAddressRecord;
pub use types::{
BlockMetadataRecord, NumericFilter, PaginationToken, QueryTimings, SignatureFilter,
SignatureRecord, SignatureStatusRecord, SortOrder, StoredAccountsTransactionRecord,
SignatureRecord, SignatureStatusRecord, SolMode, SortOrder, StoredAccountsTransactionRecord,
StoredBlockPayload, StoredBlockRecord, StoredTransactionRecord, TokenAccountsFilter,
TransactionStatusFilter, TransactionsForAddressQuery,
TokenTransferTypes, TransactionStatusFilter, TransactionsForAddressQuery,
TransferDirectionFilter, TransferPositionFilter, TransferRecord, TransfersByAddressQuery,
};

pub(crate) use types::{ResolvedSignatureFilter, SignatureSlot, SlotBoundary};
Expand Down
18 changes: 18 additions & 0 deletions crates/superbank-rpc/src/clickhouse/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,24 @@ pub(crate) const TOKEN_OWNER_REQUIRED_COLUMNS: [&str; 9] = [
"balance_changed",
];

pub(crate) const TRANSFERS_REQUIRED_COLUMNS: [&str; 15] = [
"signature",
"slot",
"slot_idx",
"transfer_idx",
"inner_instruction_idx",
"block_time",
"transfer_type",
"amount",
"mint",
"decimals",
"from_user_account",
"to_user_account",
"from_token_account",
"to_token_account",
"fee_amount",
];

pub(crate) struct TransactionsForAddressTables<'a> {
pub(crate) gsfa_table: &'a str,
pub(crate) gsfa_bucket_modulus: u64,
Expand Down
1 change: 1 addition & 0 deletions crates/superbank-rpc/src/clickhouse/sharding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) struct ShardRoutingConfig {
pub(crate) gsfa_local_table: Option<String>,
pub(crate) signatures_local_table: Option<String>,
pub(crate) token_owner_activity_local_table: Option<String>,
pub(crate) transfers_local_table: Option<String>,
pub(crate) transactions_local_table: Option<String>,
pub(crate) blocks_metadata_local_table: Option<String>,
}
Expand Down
Loading