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
73 changes: 50 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ A Cloudflare worker that extends Solana RPC with account parsing capabilities th
Start local development server:

```bash
wrangler dev
npx wrangler dev
```

You can start it with a custom rpc endpoint by setting the `RPC_ENDPOINT` environment variable.

```bash
npx wrangler dev --var RPC_ENDPOINT:"https://api.devnet.solana.com"
```

### Deploy

Deploy to Cloudflare to parse on the edge:

```bash
wrangler deploy
npx wrangler deploy
```

Set the `RPC_ENDPOINT` environment variable to the URL of the base rpc endpoint.
Expand All @@ -42,27 +48,36 @@ Example response:

```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": {
"slot": 322677507,
"apiVersion": "2.1.9"
},
"value": {
"lamports": 6876480,
"data": {
"text": "You are an AI agent ..."
},
"owner": "LLMrieZMpbJFwN52WgmBNMxYojrpRVYXdC1RCweEbab",
"executable": false,
"rentEpoch": 18446744073709552000,
"space": 860
}
}
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": {
"slot": 322677507,
"apiVersion": "2.1.9"
},
"value": {
"lamports": 6876480,
"data": {
"text": "You are an AI agent ..."
},
"owner": "LLMrieZMpbJFwN52WgmBNMxYojrpRVYXdC1RCweEbab",
"executable": false,
"rentEpoch": 18446744073709552000,
"space": 860
}
}
}
```

or for running locally:

```bash
curl "http://localhost:8787" \
-X POST \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getParsedAccountData","params":["FPxc7bcafdCQqHS8S1KX4ENCPP3vncxsKK3yRZ3mMzGn", {"encoding": "base64"}]}'
```

### 2. Fetch Multiple Parsed Accounts

```bash
Expand All @@ -81,19 +96,31 @@ curl -s "https://rpcx.magicblock.app" \
-X POST \
-H "Content-Type: application/json" \
-H "Rpc: https://api.mainnet-beta.solana.com/" \
-d '{"jsonrpc":"2.0","id":1,"method":"getParsedAccountData","params":["FPxc7bcafdCQqHS8S1KX4ENCPP3vncxsKK3yRZ3mMzGn"]}' | jq .
-d '{"jsonrpc":"2.0","id":1,"method":"getParsedAccountData","params":["5RgeA5P8bRaynJovch3zQURfJxXL3QK2JYg1YamSvyLb"]}' | jq .
```

### 4. Subscribe to Parsed Account Updates

Connect:

```bash
wscat -c "wss://rpcx.magicblock.app"
npx wscat -c "wss://rpcx.magicblock.app"
```

or for devnet:

```bash
npx wscat -c "wss://rpcx.magicblock.app" -H "Rpc: https://api.devnet.solana.com"
```

of for running locally:

```bash
npx wscat -c "ws://localhost:8787" -H "Rpc: https://api.devnet.solana.com"
```

Subscribe to updates:

```bash
{"jsonrpc":"2.0","id":1,"method":"subscribeParsedAccount","params":["5RgeA5P8bRaynJovch3zQURfJxXL3QK2JYg1YamSvyLb",{"encoding":"jsonParsed","commitment":"confirmed"}]}
{"jsonrpc":"2.0","id":1,"method":"subscribeParsedAccount","params":["F9xLoh5xxLFNb4wYnhAPm73VWyxgBTL1HiPFVEz6uW8X",{"encoding":"jsonParsed","commitment":"confirmed"}]}
```
20 changes: 20 additions & 0 deletions getParsedAccountData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Connection, PublicKey } from '@solana/web3.js';

async function getParsedAccountData() {
console.log('Connecting to RPC...');
const connection = new Connection('http://localhost:8787', 'confirmed');

// Account we want to fetch
const accountPubkey = new PublicKey('F9xLoh5xxLFNb4wYnhAPm73VWyxgBTL1HiPFVEz6uW8X');
console.log('Fetching account:', accountPubkey.toString());

try {
// Fetch the raw account data
const accountInfo = await connection.getParsedAccountInfo(accountPubkey, 'confirmed');
console.log(JSON.stringify(accountInfo, null, 2));
} catch (error) {
console.error('Error fetching account data:', error);
}
}

getParsedAccountData().catch(console.error);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@types/bn.js": "^5.1.6",
"typescript": "^5.5.2",
"vitest": "~2.1.9",
"wrangler": "^3.109.2"
"wrangler": "^3.113.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
Expand Down
58 changes: 31 additions & 27 deletions src/handlers/getParsedAccountData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import { Buffer } from 'buffer';
import { errorResponse, getIdl, decodeAccount } from '../utils/utils';

export async function handleGetParsedAccountData(
body: { id: string; params?: any },
provider: Provider,
rpcEndpoint: string,
env: Env,
ctx: ExecutionContext
body: { id: string; params?: any },
provider: Provider,
rpcEndpoint: string,
env: Env,
ctx: ExecutionContext
) {
const req = new Request(rpcEndpoint, {
method: 'POST',
const req = new Request(rpcEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
},
body: JSON.stringify({
jsonrpc: '2.0',
'Content-Type': 'application/json',
Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: body.id,
method: 'getAccountInfo',
params: [
Expand All @@ -34,20 +34,24 @@ export async function handleGetParsedAccountData(
});
const accountRes = await fetch(req);
let accountInfo;
try{
accountInfo = await accountRes.json() as { result: { value: { data: any, owner: string } } };
}catch (error: unknown) {
try {
accountInfo = (await accountRes.json()) as { result: { value: { data: any; owner: string } } };
} catch (error: unknown) {
// @ts-ignore
return errorResponse(body.id, -32602, "Error parsing response", { error: error.message, account: body.params?.[0], statusCode: accountRes.status});
return errorResponse(body.id, -32602, 'Error parsing response', {
error: error.message,
account: body.params?.[0],
statusCode: accountRes.status,
});
}
if (accountInfo.result.value) {
const dataBuffer = Buffer.from(accountInfo.result.value.data[0], 'base64');
const owner = new PublicKey(accountInfo.result.value.owner);
const idl = await getIdl(owner, provider, env, ctx);
if (accountInfo.result.value) {
const dataBuffer = Buffer.from(accountInfo.result.value.data[0], 'base64');
const owner = new PublicKey(accountInfo.result.value.owner);
const idl = await getIdl(owner, provider, env, ctx);

if (!idl) {
return errorResponse(body.id, -32602, "IDL not found for program", { programId: owner.toString() });
}
if (!idl) {
return errorResponse(body.id, -32602, 'IDL not found for program', { programId: owner.toString() });
}

try {
const program = new Program(idl as Idl, provider);
Expand All @@ -67,5 +71,5 @@ export async function handleGetParsedAccountData(
}
}

return accountInfo;
return accountInfo;
}
117 changes: 85 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,27 @@ export class SimpleProvider implements Provider {

const JSON_HEADERS = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
'Access-Control-Allow-Origin': '*',
};

// Add type for the RPC response
type RpcResponse = {
result: {
context: { slot: number };
value: {
data: any;
executable: boolean;
lamports: number;
owner: string;
rentEpoch: number;
space: number;
} | null;
};
};

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
let rpcEndpoint = request?.headers?.get("Rpc")?.trim();
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
let rpcEndpoint = request?.headers?.get('Rpc')?.trim();

if (rpcEndpoint === "devnet"){
rpcEndpoint = env?.RPC_ENDPOINT_DEVNET?.trim();
Expand All @@ -37,56 +52,94 @@ export default {

const provider = new SimpleProvider(new Connection(rpcEndpoint));

if (request.headers.get('Upgrade') === 'websocket') {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);

if (request.headers.get('Upgrade') === 'websocket') {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
const wsEndpoint = rpcEndpoint.replace('https://', 'wss://');
await handleWebSocketConnection(provider, server, wsEndpoint, env, ctx);
await handleWebSocketConnection(provider, server, wsEndpoint, env, ctx);

return new Response(null, {
status: 101,
webSocket: client,
});
}
return new Response(null, {
status: 101,
webSocket: client,
});
}

// Handle HTTP requests
// Handle HTTP requests
let body;
try {
body = await request.json() as { method: string; id: string; params?: any };
body = (await request.json()) as { method: string; id: string; params?: any };
} catch (error: any) {
return new Response(JSON.stringify({
jsonrpc: "2.0",
error: { code: -32700, message: "Parse error", data: error.message },
id: null
}), {
status: 400,
headers: JSON_HEADERS
});
return new Response(
JSON.stringify({
jsonrpc: '2.0',
error: { code: -32700, message: 'Parse error', data: error.message },
id: null,
}),
{
status: 400,
headers: JSON_HEADERS,
}
);
}

if (body.method === 'getAccountInfo') {
// Check if params contains jsonParsed encoding
const hasJsonParsed = body.params?.[1]?.encoding === 'jsonParsed';

if (hasJsonParsed) {
const result = (await handleGetParsedAccountData(body, provider, rpcEndpoint, env, ctx)) as RpcResponse;
console.log(result);
const formattedResponse = {
context: {
slot: result.result.context.slot,
},
value: result.result.value
? {
data: {
parsed: result.result.value.data,
program: result.result.value.owner,
space: result.result.value.space,
},
executable: result.result.value.executable,
lamports: result.result.value.lamports,
owner: result.result.value.owner,
rentEpoch: result.result.value.rentEpoch,
}
: null,
};

return new Response(
JSON.stringify({
jsonrpc: '2.0',
id: body.id,
result: formattedResponse,
}),
{ headers: JSON_HEADERS }
);
}
}

if (body.method === 'getParsedAccountData') {
const result = await handleGetParsedAccountData(body, provider, rpcEndpoint, env, ctx);
return new Response(JSON.stringify(result), { headers: JSON_HEADERS });
}
const result = await handleGetParsedAccountData(body, provider, rpcEndpoint, env, ctx);
return new Response(JSON.stringify(result), { headers: JSON_HEADERS });
}

if (body.method === 'getParsedAccountsData') {
const result = await handleGetParsedAccountsData(body, provider, rpcEndpoint, env, ctx);
return new Response(JSON.stringify(result), { headers: JSON_HEADERS });
const result = await handleGetParsedAccountsData(body, provider, rpcEndpoint, env, ctx);
return new Response(JSON.stringify(result), { headers: JSON_HEADERS });
}

if (body.method === 'getParsedTransaction') {
const result = await handleGetParsedTransaction(body, provider, rpcEndpoint, env, ctx);
return new Response(JSON.stringify(result), { headers: JSON_HEADERS });
}

// Proxy all other HTTP requests
const proxyReq = new Request(rpcEndpoint, {
method: request.method,
headers: { ...request.headers, 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(body)
headers: { ...request.headers, 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
});
const proxyRes = await fetch(proxyReq);
return new Response(proxyRes.body, { status: proxyRes.status, headers: JSON_HEADERS });
},
},
} satisfies ExportedHandler<Env>;
Loading