-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore | ||
|
||
.turbo | ||
dist | ||
|
||
# Logs | ||
|
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
MINA_NETWORK=devnet | ||
NODE_API_DEVNET=https://api.minascan.io/node/devnet/v1/graphql | ||
NODE_API_MAINNET=https://api.minascan.io/node/mainnet/v1/graphql |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Klesia | ||
|
||
JSON-RPC 2.0 wrapper over common Mina Protocol tools and services. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "@mina-js/klesia", | ||
"scripts": { | ||
"dev": "bun run --hot src/index.ts" | ||
}, | ||
"dependencies": { | ||
"@hono/zod-openapi": "^0.16.0", | ||
"@scalar/hono-api-reference": "^0.5.143", | ||
"@urql/core": "^5.0.6", | ||
"dayjs": "^1.11.13", | ||
"hono": "^4.5.10", | ||
"ofetch": "^1.3.4", | ||
"ts-pattern": "^5.3.1", | ||
"zod": "^3.23.8" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; | ||
import { apiReference } from "@scalar/hono-api-reference"; | ||
import { match } from "ts-pattern"; | ||
import { mina } from "./methods/mina"; | ||
import { RpcMethodSchema, RpcResponseSchema } from "./schema"; | ||
import { buildResponse } from "./utils/build-response"; | ||
|
||
const api = new OpenAPIHono(); | ||
|
||
api.doc("/api/openapi", { | ||
openapi: "3.0.0", | ||
info: { | ||
version: "1.0.0", | ||
title: "Klesia API", | ||
}, | ||
}); | ||
|
||
const rpcRoute = createRoute({ | ||
method: "post", | ||
path: "/api", | ||
request: { | ||
body: { content: { "application/json": { schema: RpcMethodSchema } } }, | ||
}, | ||
responses: { | ||
200: { | ||
content: { | ||
"application/json": { | ||
schema: RpcResponseSchema, | ||
}, | ||
}, | ||
description: "JSON-RPC response.", | ||
}, | ||
}, | ||
}); | ||
|
||
api.openapi(rpcRoute, async ({ req, json }) => { | ||
const body = req.valid("json"); | ||
return match(body) | ||
.with({ method: "mina_getTransactionCount" }, async ({ params }) => { | ||
const [publicKey] = params; | ||
const result = await mina.getTransactionCount({ publicKey }); | ||
return json(buildResponse(result), 200); | ||
}) | ||
.with({ method: "mina_getBalance" }, async ({ params }) => { | ||
const [publicKey] = params; | ||
const result = await mina.getBalance({ publicKey }); | ||
return json(buildResponse(result), 200); | ||
}) | ||
.with({ method: "mina_blockHash" }, async () => { | ||
const result = await mina.blockHash(); | ||
return json(buildResponse(result), 200); | ||
}) | ||
.with({ method: "mina_chainId" }, async () => { | ||
const result = await mina.chainId(); | ||
return json(buildResponse(result), 200); | ||
}) | ||
.with({ method: "mina_sendTransaction" }, async ({ params }) => { | ||
const [signedTransaction, type] = params; | ||
const result = await mina.sendTransaction({ signedTransaction, type }); | ||
return json(buildResponse(result), 200); | ||
}) | ||
.exhaustive(); | ||
}); | ||
|
||
api.get( | ||
"/api", | ||
apiReference({ | ||
spec: { | ||
url: "/api/openapi", | ||
}, | ||
}), | ||
); | ||
|
||
export default api; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { gql } from "@urql/core"; | ||
import { match } from "ts-pattern"; | ||
import { getNodeClient } from "../utils/node"; | ||
|
||
const getTransactionCount = async ({ publicKey }: { publicKey: string }) => { | ||
const client = getNodeClient(); | ||
const { data } = await client.query( | ||
gql` | ||
query { | ||
account(publicKey: $publicKey) { | ||
nonce | ||
} | ||
} | ||
`, | ||
{ publicKey }, | ||
); | ||
return Number.parseInt(data.account.nonce); | ||
}; | ||
|
||
const getBalance = async ({ publicKey }: { publicKey: string }) => { | ||
const client = getNodeClient(); | ||
const { data } = await client.query( | ||
gql` | ||
query { | ||
account(publicKey: $publicKey) { | ||
balance { | ||
total | ||
} | ||
} | ||
} | ||
`, | ||
{ publicKey }, | ||
); | ||
return data.account.balance.total; | ||
}; | ||
|
||
const blockHash = async () => { | ||
const client = getNodeClient(); | ||
const { data } = await client.query( | ||
gql` | ||
query { | ||
daemonStatus { | ||
stateHash | ||
} | ||
} | ||
`, | ||
{}, | ||
); | ||
return data.daemonStatus.stateHash; | ||
}; | ||
|
||
const chainId = async () => { | ||
const client = getNodeClient(); | ||
const { data } = await client.query( | ||
gql` | ||
query { | ||
daemonStatus { | ||
chainId | ||
} | ||
} | ||
`, | ||
{}, | ||
); | ||
return data.daemonStatus.chainId; | ||
}; | ||
|
||
const sendTransaction = async ({ | ||
signedTransaction, | ||
type, | ||
}: { | ||
// biome-ignore lint/suspicious/noExplicitAny: TODO | ||
signedTransaction: any; | ||
type: "payment" | "delegation" | "zkapp"; | ||
}) => { | ||
const client = getNodeClient(); | ||
return match(type) | ||
.with("payment", async () => { | ||
const { signature, data: input } = signedTransaction; | ||
const { data } = await client.mutation( | ||
gql` | ||
mutation { | ||
sendPayment(signature: $signature, input: $input) { | ||
payment { | ||
hash | ||
} | ||
} | ||
} | ||
`, | ||
{ signature, input }, | ||
); | ||
return data.sendPayment.payment.hash; | ||
}) | ||
.with("delegation", async () => { | ||
const { signature, data: input } = signedTransaction; | ||
const { data } = await client.mutation( | ||
gql` | ||
mutation { | ||
sendDelegation(signature: $signature, input: $input) { | ||
delegation { | ||
hash | ||
} | ||
} | ||
} | ||
`, | ||
{ signature, input }, | ||
); | ||
return data.sendDelegation.delegation.hash; | ||
}) | ||
.with("zkapp", async () => { | ||
const { data } = await client.mutation( | ||
gql` | ||
mutation { | ||
sendZkapp(input: $input) { | ||
zkapp { | ||
hash | ||
} | ||
} | ||
} | ||
`, | ||
{ input: signedTransaction }, | ||
); | ||
return data.sendZkapp.zkapp.hash; | ||
}) | ||
.exhaustive(); | ||
}; | ||
|
||
export const mina = { | ||
getTransactionCount, | ||
getBalance, | ||
blockHash, | ||
chainId, | ||
sendTransaction, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { z } from "zod"; | ||
|
||
export const PublicKeySchema = z.string().length(55); | ||
export const PublicKeyParamsSchema = z.array(PublicKeySchema).length(1); | ||
export const EmptyParamsSchema = z.array(z.string()).length(0).optional(); | ||
export const SendTransactionSchema = z.array(z.any(), z.string()).length(2); | ||
|
||
export const RpcMethodSchema = z.discriminatedUnion("method", [ | ||
z.object({ | ||
method: z.literal("mina_getTransactionCount"), | ||
params: PublicKeyParamsSchema, | ||
}), | ||
z.object({ | ||
method: z.literal("mina_getBalance"), | ||
params: PublicKeyParamsSchema, | ||
}), | ||
z.object({ | ||
method: z.literal("mina_blockHash"), | ||
params: EmptyParamsSchema, | ||
}), | ||
z.object({ | ||
method: z.literal("mina_chainId"), | ||
params: EmptyParamsSchema, | ||
}), | ||
z.object({ | ||
method: z.literal("mina_sendTransaction"), | ||
params: SendTransactionSchema, | ||
}), | ||
]); | ||
|
||
export const RpcResponseSchema = z.object({ | ||
jsonrpc: z.literal("2.0"), | ||
result: z.unknown(), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const buildResponse = (data: unknown) => ({ | ||
jsonrpc: "2.0", | ||
result: data, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Client, cacheExchange, fetchExchange } from "@urql/core"; | ||
import { match } from "ts-pattern"; | ||
|
||
const MINA_NETWORK = process.env.MINA_NETWORK; | ||
|
||
export const getNodeApiUrl = () => { | ||
return match(MINA_NETWORK) | ||
.with("devnet", () => process.env.NODE_API_DEVNET) | ||
.with("mainnet", () => process.env.NODE_API_MAINNET) | ||
.run(); | ||
}; | ||
|
||
export const getNodeClient = () => { | ||
const url = getNodeApiUrl(); | ||
if (!url) throw new Error("Invalid network config."); | ||
return new Client({ url, exchanges: [cacheExchange, fetchExchange] }); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"compilerOptions": { | ||
"strict": true, | ||
"jsx": "react-jsx", | ||
"jsxImportSource": "hono/jsx" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,7 @@ | |
"test": "bun run --filter '*' test", | ||
"cleanup": "bun run --filter '*' cleanup", | ||
"lint": "bunx biome check .", | ||
"format": "bunx biome check . --write", | ||
"format:unsafe": "bunx biome check . --write --unsafe" | ||
"format": "bunx biome check . --write --unsafe" | ||
}, | ||
"devDependencies": { | ||
"@biomejs/biome": "1.8.3", | ||
|
@@ -19,6 +18,6 @@ | |
"turbo": "^2.0.11", | ||
"typescript": "5.5.4" | ||
}, | ||
"workspaces": ["packages/*"], | ||
"workspaces": ["packages/*", "apps/*"], | ||
"packageManager": "[email protected]" | ||
} |