Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wrappers): add Infura WASM FDW #395

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
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
Loading