Skip to content

Commit

Permalink
feat(wrappers): add Infura WASM FDW
Browse files Browse the repository at this point in the history
- 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
Show file tree
Hide file tree
Showing 5 changed files with 452 additions and 0 deletions.
22 changes: 22 additions & 0 deletions wasm-wrappers/fdw/infura_fdw/Cargo.toml
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" }
123 changes: 123 additions & 0 deletions wasm-wrappers/fdw/infura_fdw/README.md
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;
```
196 changes: 196 additions & 0 deletions wasm-wrappers/fdw/infura_fdw/src/lib.rs
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(())
}
}
Loading

0 comments on commit e867be1

Please sign in to comment.