-
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Implements Infura JSON-RPC API wrapper - Adds mock server for testing - Handles hex to decimal conversion - Implements comprehensive test suite Fixes #394 Co-Authored-By: [email protected] <[email protected]>
- Loading branch information
1 parent
c42223c
commit e867be1
Showing
5 changed files
with
452 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "infura_fdw" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
|
||
[dependencies] | ||
wit-bindgen-rt = "0.26.0" | ||
serde_json = "1.0" | ||
|
||
[package.metadata.component] | ||
package = "supabase:infura-fdw" | ||
|
||
[package.metadata.component.dependencies] | ||
|
||
[package.metadata.component.target] | ||
path = "wit" | ||
|
||
[package.metadata.component.target.dependencies] | ||
"supabase:wrappers" = { path = "../../wit" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
--- | ||
source: https://docs.infura.io/ | ||
documentation: https://docs.infura.io/api/networks/ethereum | ||
author: supabase | ||
tags: | ||
- wasm | ||
- official | ||
--- | ||
|
||
# Infura | ||
|
||
Infura provides blockchain infrastructure that allows applications to access Ethereum network data through JSON-RPC APIs. | ||
|
||
## Available Versions | ||
|
||
| Version | Release Date | | ||
| ------- | ----------- | | ||
| 0.1.0 | TBD | | ||
|
||
## Preparation | ||
|
||
1. Create an Infura account and project | ||
2. Get your Project ID from the project settings | ||
3. Create a foreign data wrapper and server | ||
|
||
```sql | ||
create extension if not exists wrappers with schema extensions; | ||
|
||
create foreign data wrapper wasm_wrapper | ||
handler wasm_handler | ||
validator wasm_validator; | ||
|
||
-- Using direct project ID | ||
create server infura_server | ||
foreign data wrapper wasm_wrapper | ||
options ( | ||
project_id 'your-project-id' | ||
); | ||
|
||
-- Using vault for project ID | ||
insert into vault.secrets (name, secret) | ||
values ('infura', 'your-project-id') | ||
returning key_id; | ||
|
||
create server infura_server | ||
foreign data wrapper wasm_wrapper | ||
options ( | ||
project_id_id 'key-id-from-above' | ||
); | ||
``` | ||
|
||
## Entities | ||
|
||
### Block Number | ||
|
||
```sql | ||
create foreign table infura.block_number ( | ||
number bigint | ||
) | ||
server infura_server | ||
options ( | ||
table 'eth_blockNumber' | ||
); | ||
``` | ||
|
||
### Blocks | ||
|
||
```sql | ||
create foreign table infura.blocks ( | ||
number bigint, | ||
hash text, | ||
parent_hash text, | ||
nonce text, | ||
miner text, | ||
difficulty bigint, | ||
total_difficulty bigint, | ||
size bigint, | ||
gas_limit bigint, | ||
gas_used bigint, | ||
timestamp bigint | ||
) | ||
server infura_server | ||
options ( | ||
table 'eth_getBlockByNumber' | ||
); | ||
``` | ||
|
||
## Supported Data Types | ||
|
||
| Postgres Type | Infura Type | Description | | ||
| ------------ | ----------- | ----------- | | ||
| bigint | hex number | Used for block numbers, gas values, and timestamps | | ||
| text | string/hex | Used for hashes, addresses, and other hex strings | | ||
|
||
## Limitations | ||
|
||
- Only supports HTTP endpoints (no WebSocket support) | ||
- Rate limits apply based on your Infura plan | ||
- Some complex queries may timeout due to blockchain data size | ||
- Currently only supports eth_blockNumber and eth_getBlockByNumber methods | ||
- All numeric values are returned as bigint, which may not capture the full range of some Ethereum values | ||
|
||
## Examples | ||
|
||
```sql | ||
-- Get latest block number | ||
SELECT number FROM infura.block_number; | ||
|
||
-- Get block details | ||
SELECT number, hash, miner, timestamp | ||
FROM infura.blocks | ||
WHERE number = (SELECT number FROM infura.block_number); | ||
|
||
-- Get gas usage over time | ||
SELECT | ||
number, | ||
gas_used, | ||
gas_limit, | ||
(gas_used::float / gas_limit::float * 100)::numeric(5,2) as gas_usage_percent | ||
FROM infura.blocks | ||
ORDER BY number DESC | ||
LIMIT 10; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
#[cfg(test)] | ||
pub use self::InfuraFdw; | ||
|
||
use serde_json::{json, Value as JsonValue}; | ||
use wit_bindgen_rt::*; | ||
|
||
struct InfuraFdw { | ||
base_url: String, | ||
headers: Vec<(String, String)>, | ||
project_id: String, | ||
table: String, | ||
src_rows: Vec<JsonValue>, | ||
src_idx: usize, | ||
} | ||
|
||
impl InfuraFdw { | ||
// Convert hex string to decimal | ||
fn hex_to_decimal(hex: &str) -> Result<i64, String> { | ||
i64::from_str_radix(hex.trim_start_matches("0x"), 16) | ||
.map_err(|e| format!("Failed to convert hex to decimal: {}", e)) | ||
} | ||
|
||
// Create JSON-RPC request | ||
fn create_request(&self, method: &str, params: Vec<String>) -> Result<http::Request, String> { | ||
let url = format!("{}/{}", self.base_url, self.project_id); | ||
let body = json!({ | ||
"jsonrpc": "2.0", | ||
"method": method, | ||
"params": params, | ||
"id": 1 | ||
}); | ||
|
||
let mut headers = self.headers.clone(); | ||
headers.push(("Content-Type".to_string(), "application/json".to_string())); | ||
|
||
Ok(http::Request { | ||
method: http::Method::Post, | ||
url, | ||
headers, | ||
body: body.to_string(), | ||
}) | ||
} | ||
|
||
// Create new instance | ||
fn new(base_url: String, project_id: String) -> Self { | ||
Self { | ||
base_url, | ||
headers: Vec::new(), | ||
project_id, | ||
table: String::new(), | ||
src_rows: Vec::new(), | ||
src_idx: 0, | ||
} | ||
} | ||
|
||
// Process API response | ||
fn process_response(&mut self, response: &str) -> Result<(), String> { | ||
let value: JsonValue = serde_json::from_str(response) | ||
.map_err(|e| format!("Failed to parse response: {}", e))?; | ||
|
||
if let Some(error) = value.get("error") { | ||
return Err(format!("API error: {}", error)); | ||
} | ||
|
||
if let Some(result) = value.get("result") { | ||
self.src_rows = vec![result.clone()]; | ||
self.src_idx = 0; | ||
Ok(()) | ||
} else { | ||
Err("No result in response".to_string()) | ||
} | ||
} | ||
} | ||
|
||
wit_bindgen::generate!({ | ||
path: "../../wit", | ||
world: "wrappers", | ||
}); | ||
|
||
struct Component; | ||
|
||
impl wrappers::Wrappers for Component { | ||
fn create_fdw() -> Box<dyn wrappers::Fdw> { | ||
Box::new(InfuraFdw::new( | ||
String::from("https://mainnet.infura.io/v3"), | ||
String::new(), // project_id will be set from options | ||
)) | ||
} | ||
} | ||
|
||
impl wrappers::Fdw for InfuraFdw { | ||
fn begin_scan( | ||
&mut self, | ||
quals: Vec<wrappers::Qual>, | ||
attrs: Vec<wrappers::Attr>, | ||
table_name: String, | ||
) -> Result<(), String> { | ||
self.table = table_name; | ||
|
||
let method = match self.table.as_str() { | ||
"eth_getBlockByNumber" => "eth_getBlockByNumber", | ||
"eth_blockNumber" => "eth_blockNumber", | ||
_ => return Err(format!("Unsupported table: {}", self.table)), | ||
}; | ||
|
||
let request = self.create_request(method, vec![])?; | ||
let response = http::send_request(&request) | ||
.map_err(|e| format!("HTTP request failed: {}", e))?; | ||
|
||
if response.status_code != 200 { | ||
return Err(format!("HTTP error: {}", response.status_code)); | ||
} | ||
|
||
self.process_response(&response.body) | ||
} | ||
|
||
fn iter_scan(&mut self) -> Result<Option<Vec<wrappers::Datum>>, String> { | ||
if self.src_idx >= self.src_rows.len() { | ||
return Ok(None); | ||
} | ||
|
||
let row = &self.src_rows[self.src_idx]; | ||
self.src_idx += 1; | ||
|
||
let mut data = Vec::new(); | ||
match self.table.as_str() { | ||
"eth_blockNumber" => { | ||
if let Some(block_num) = row.as_str() { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(block_num)?)); | ||
} | ||
} | ||
"eth_getBlockByNumber" => { | ||
if let Some(obj) = row.as_object() { | ||
// Convert block data to appropriate types | ||
if let Some(num) = obj.get("number").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(num)?)); | ||
} | ||
if let Some(hash) = obj.get("hash").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::String(hash.to_string())); | ||
} | ||
if let Some(parent) = obj.get("parentHash").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::String(parent.to_string())); | ||
} | ||
if let Some(nonce) = obj.get("nonce").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::String(nonce.to_string())); | ||
} | ||
if let Some(miner) = obj.get("miner").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::String(miner.to_string())); | ||
} | ||
if let Some(difficulty) = obj.get("difficulty").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(difficulty)?)); | ||
} | ||
if let Some(total) = obj.get("totalDifficulty").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(total)?)); | ||
} | ||
if let Some(size) = obj.get("size").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(size)?)); | ||
} | ||
if let Some(gas_limit) = obj.get("gasLimit").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(gas_limit)?)); | ||
} | ||
if let Some(gas_used) = obj.get("gasUsed").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(gas_used)?)); | ||
} | ||
if let Some(timestamp) = obj.get("timestamp").and_then(|v| v.as_str()) { | ||
data.push(wrappers::Datum::I64(Self::hex_to_decimal(timestamp)?)); | ||
} | ||
} | ||
} | ||
_ => return Err(format!("Unsupported table: {}", self.table)), | ||
} | ||
|
||
Ok(Some(data)) | ||
} | ||
|
||
fn end_scan(&mut self) -> Result<(), String> { | ||
self.src_rows.clear(); | ||
self.src_idx = 0; | ||
Ok(()) | ||
} | ||
|
||
fn validator(&self, options: Vec<(String, Option<String>)>) -> Result<(), String> { | ||
for (key, value) in options { | ||
match key.as_str() { | ||
"project_id" => { | ||
if value.is_none() { | ||
return Err("project_id is required".to_string()); | ||
} | ||
self.project_id = value.unwrap(); | ||
} | ||
_ => return Err(format!("Unknown option: {}", key)), | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.