diff --git a/apps/docs/src/pages/klesia/rpc.mdx b/apps/docs/src/pages/klesia/rpc.mdx index 846c60e..392c0f4 100644 --- a/apps/docs/src/pages/klesia/rpc.mdx +++ b/apps/docs/src/pages/klesia/rpc.mdx @@ -18,6 +18,12 @@ https://mainnet.klesia.palladians.xyz/api https://devnet.klesia.palladians.xyz/api ``` +### Zeko Devnet (soon™️) + +``` +https://zeko-devnet.klesia.palladians.xyz/api +``` + ## RPC Methods Below you can find the complete list of RPC methods available on Klesia. @@ -50,6 +56,10 @@ Array of strings: Returns the hash of the most recent block. +:::warning +Not supported on Zeko. +::: + --- ### mina_chainId diff --git a/apps/docs/src/pages/klesia/sdk.mdx b/apps/docs/src/pages/klesia/sdk.mdx index 11ce818..a9e0ba0 100644 --- a/apps/docs/src/pages/klesia/sdk.mdx +++ b/apps/docs/src/pages/klesia/sdk.mdx @@ -12,14 +12,20 @@ $ npm install @mina-js/klesia-sdk For now there are only [nightly builds](/klesia/sdk#nightly-builds) available. The stable version will be released soon™️. ::: +## Client Options + +- `network`: The network to connect to. One of: `mainnet`, `devnet`, `zeko-devnet`. +- `customUrl`: A custom URL to connect to in case of self-hosted RPCs. +- `throwable`: If `true`, the client will throw an error if the response contains an error. Default is `true`. + ## Usage -```typescript +```typescript twoslash import { createClient } from '@mina-js/klesia-sdk' const client = createClient({ network: 'devnet' }) -const { result } = await client.request({ +const { result } = await client.request<'mina_getTransactionCount'>({ method: 'mina_getTransactionCount', params: ['B62qkYa1o6Mj6uTTjDQCob7FYZspuhkm4RRQhgJg9j4koEBWiSrTQrS'] }) diff --git a/apps/klesia/.env.example b/apps/klesia/.env.example index d45fa30..bfbef12 100644 --- a/apps/klesia/.env.example +++ b/apps/klesia/.env.example @@ -1,3 +1,4 @@ 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 +NODE_API_ZEKO_DEVNET=https://devnet.zeko.io/graphql diff --git a/apps/klesia/src/index.spec.ts b/apps/klesia/src/index.spec.ts index bd2cd7f..6ff6ad2 100644 --- a/apps/klesia/src/index.spec.ts +++ b/apps/klesia/src/index.spec.ts @@ -11,8 +11,8 @@ it("returns result for mina_getTransactionCount", async () => { params: ["B62qkYa1o6Mj6uTTjDQCob7FYZspuhkm4RRQhgJg9j4koEBWiSrTQrS"], }, }); - const { result } = await response.json(); - expect(result).toBeGreaterThan(0); + const { result } = (await response.json()) as { result: string }; + expect(BigInt(result)).toBeGreaterThan(0); }); it("returns result for mina_getBalance", async () => { @@ -22,7 +22,7 @@ it("returns result for mina_getBalance", async () => { params: ["B62qkYa1o6Mj6uTTjDQCob7FYZspuhkm4RRQhgJg9j4koEBWiSrTQrS"], }, }); - const { result } = await response.json(); + const { result } = (await response.json()) as { result: string }; expect(BigInt(String(result))).toBeGreaterThan(0); }); @@ -30,15 +30,15 @@ it("returns result for mina_blockHash", async () => { const response = await client.api.$post({ json: { method: "mina_blockHash" }, }); - const { result } = await response.json(); - expect((result as unknown as string).length).toBeGreaterThan(0); + const { result } = (await response.json()) as { result: string }; + expect(result.length).toBeGreaterThan(0); }); it("returns result for mina_chainId", async () => { const response = await client.api.$post({ json: { method: "mina_chainId" }, }); - const { result } = await response.json(); + const { result } = (await response.json()) as { result: string }; expect(result.length).toBeGreaterThan(0); }); @@ -49,7 +49,8 @@ it("returns result for mina_getAccount", async () => { params: ["B62qkYa1o6Mj6uTTjDQCob7FYZspuhkm4RRQhgJg9j4koEBWiSrTQrS"], }, }); - const { result } = await response.json(); + // biome-ignore lint/suspicious/noExplicitAny: TODO + const { result } = (await response.json()) as any; expect(BigInt(result.nonce)).toBeGreaterThanOrEqual(0); expect(BigInt(result.balance)).toBeGreaterThanOrEqual(0); }); diff --git a/apps/klesia/src/index.ts b/apps/klesia/src/index.ts index 293636e..7c95eb2 100644 --- a/apps/klesia/src/index.ts +++ b/apps/klesia/src/index.ts @@ -11,7 +11,7 @@ import { match } from "ts-pattern"; import mainDocs from "../docs/index.txt"; import rpcDocs from "../docs/rpc.txt"; import { mina } from "./methods/mina"; -import { RpcMethodSchema, RpcResponseSchema } from "./schema"; +import { RpcMethod, RpcMethodSchema, RpcResponseSchema } from "./schema"; import { buildResponse } from "./utils/build-response"; export const api = new OpenAPIHono(); @@ -63,39 +63,81 @@ const rpcRoute = createRoute({ export const klesiaRpcRoute = 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: PublicKeySchema.parse(publicKey), - }); - return json(buildResponse(result), 200); - }) - .with({ method: "mina_getBalance" }, async ({ params }) => { + .with( + { method: RpcMethod.enum.mina_getTransactionCount }, + async ({ params }) => { + const [publicKey] = params; + const result = await mina.getTransactionCount({ + publicKey: PublicKeySchema.parse(publicKey), + }); + return json( + buildResponse({ + method: RpcMethod.enum.mina_getTransactionCount, + result, + }), + 200, + ); + }, + ) + .with({ method: RpcMethod.enum.mina_getBalance }, async ({ params }) => { const [publicKey] = params; const result = await mina.getBalance({ publicKey: PublicKeySchema.parse(publicKey), }); - return json(buildResponse(result), 200); + return json( + buildResponse({ method: RpcMethod.enum.mina_getBalance, result }), + 200, + ); }) - .with({ method: "mina_blockHash" }, async () => { + .with({ method: RpcMethod.enum.mina_blockHash }, async () => { + if (process.env.MINA_NETWORK === "zeko_devnet") { + return json( + buildResponse({ + method: RpcMethod.enum.mina_blockHash, + error: { + code: -32600, + message: "Network not supported.", + }, + }), + 200, + ); + } const result = await mina.blockHash(); - return json(buildResponse(result), 200); + return json( + buildResponse({ method: RpcMethod.enum.mina_blockHash, result }), + 200, + ); }) - .with({ method: "mina_chainId" }, async () => { + .with({ method: RpcMethod.enum.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); + return json( + buildResponse({ method: RpcMethod.enum.mina_chainId, result }), + 200, + ); }) - .with({ method: "mina_getAccount" }, async ({ params }) => { + .with( + { method: RpcMethod.enum.mina_sendTransaction }, + async ({ params }) => { + const [signedTransaction, type] = params; + const result = await mina.sendTransaction({ signedTransaction, type }); + return json( + buildResponse({ + method: RpcMethod.enum.mina_sendTransaction, + result, + }), + 200, + ); + }, + ) + .with({ method: RpcMethod.enum.mina_getAccount }, async ({ params }) => { const [publicKey] = params; const result = await mina.getAccount({ publicKey: PublicKeySchema.parse(publicKey), }); - return json(buildResponse(result), 200); + return json( + buildResponse({ method: RpcMethod.enum.mina_getAccount, result }), + 200, + ); }) .exhaustive(); }); @@ -103,3 +145,10 @@ export const klesiaRpcRoute = api.openapi(rpcRoute, async ({ req, json }) => { serve(api); export type KlesiaRpc = typeof klesiaRpcRoute; +export { + KlesiaNetwork, + RpcMethod, + type RpcMethodType, + type RpcResponseType, + type RpcErrorType, +} from "./schema"; diff --git a/apps/klesia/src/methods/mina.spec.ts b/apps/klesia/src/methods/mina.spec.ts index 8672c67..b012ee1 100644 --- a/apps/klesia/src/methods/mina.spec.ts +++ b/apps/klesia/src/methods/mina.spec.ts @@ -5,7 +5,7 @@ const TEST_PKEY = "B62qkYa1o6Mj6uTTjDQCob7FYZspuhkm4RRQhgJg9j4koEBWiSrTQrS"; it("should return transactions count", async () => { const result = await mina.getTransactionCount({ publicKey: TEST_PKEY }); - expect(result).toBeGreaterThan(0); + expect(BigInt(result)).toBeGreaterThan(0); }); it("should return balance", async () => { diff --git a/apps/klesia/src/methods/mina.ts b/apps/klesia/src/methods/mina.ts index a33a86d..b343506 100644 --- a/apps/klesia/src/methods/mina.ts +++ b/apps/klesia/src/methods/mina.ts @@ -15,7 +15,7 @@ const getTransactionCount = async ({ publicKey }: { publicKey: string }) => { `, { publicKey }, ); - return Number.parseInt(data.account.nonce); + return data.account.nonce; }; const getBalance = async ({ publicKey }: { publicKey: string }) => { diff --git a/apps/klesia/src/schema.ts b/apps/klesia/src/schema.ts index 3608c63..4b07c83 100644 --- a/apps/klesia/src/schema.ts +++ b/apps/klesia/src/schema.ts @@ -1,38 +1,94 @@ import { PublicKeySchema } from "@mina-js/shared"; import { z } from "zod"; +export const KlesiaNetwork = z.enum(["devnet", "mainnet", "zeko_devnet"]); 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 RpcMethod = z.enum([ + "mina_getTransactionCount", + "mina_getBalance", + "mina_blockHash", + "mina_chainId", + "mina_sendTransaction", + "mina_getAccount", +]); +export type RpcMethodType = z.infer; + export const RpcMethodSchema = z.discriminatedUnion("method", [ z.object({ - method: z.literal("mina_getTransactionCount"), + method: z.literal(RpcMethod.enum.mina_getTransactionCount), params: PublicKeyParamsSchema, }), z.object({ - method: z.literal("mina_getBalance"), + method: z.literal(RpcMethod.enum.mina_getBalance), params: PublicKeyParamsSchema, }), z.object({ - method: z.literal("mina_blockHash"), + method: z.literal(RpcMethod.enum.mina_blockHash), params: EmptyParamsSchema, }), z.object({ - method: z.literal("mina_chainId"), + method: z.literal(RpcMethod.enum.mina_chainId), params: EmptyParamsSchema, }), z.object({ - method: z.literal("mina_sendTransaction"), + method: z.literal(RpcMethod.enum.mina_sendTransaction), params: SendTransactionSchema, }), z.object({ - method: z.literal("mina_getAccount"), + method: z.literal(RpcMethod.enum.mina_getAccount), params: PublicKeyParamsSchema, }), ]); -export const RpcResponseSchema = z.object({ +export const JsonRpcResponse = z.object({ jsonrpc: z.literal("2.0"), - result: z.any(), }); + +export const RpcError = z.object({ + code: z.number(), + message: z.string(), +}); + +export type RpcErrorType = z.infer; + +export const ErrorSchema = JsonRpcResponse.extend({ + error: RpcError, +}); + +export const RpcResponseSchema = z.union([ + z.discriminatedUnion("method", [ + JsonRpcResponse.extend({ + method: z.literal(RpcMethod.enum.mina_getTransactionCount), + result: z.string(), + }), + JsonRpcResponse.extend({ + method: z.literal(RpcMethod.enum.mina_getBalance), + result: z.string(), + }), + JsonRpcResponse.extend({ + method: z.literal(RpcMethod.enum.mina_blockHash), + result: z.string(), + }), + JsonRpcResponse.extend({ + method: z.literal(RpcMethod.enum.mina_chainId), + result: z.string(), + }), + JsonRpcResponse.extend({ + method: z.literal(RpcMethod.enum.mina_sendTransaction), + result: z.string(), + }), + JsonRpcResponse.extend({ + method: z.literal(RpcMethod.enum.mina_getAccount), + result: z.object({ + nonce: z.string(), + balance: z.string(), + }), + }), + ]), + ErrorSchema, +]); + +export type RpcResponseType = z.infer; diff --git a/apps/klesia/src/utils/build-response.ts b/apps/klesia/src/utils/build-response.ts index 48af077..69914a0 100644 --- a/apps/klesia/src/utils/build-response.ts +++ b/apps/klesia/src/utils/build-response.ts @@ -1,4 +1,16 @@ -export const buildResponse = (data: unknown) => ({ - jsonrpc: "2.0", - result: data, -}); +import type { RpcErrorType, RpcMethodType } from "../schema"; + +export const buildResponse = ({ + result, + error, + method, +}: { result?: unknown; error?: RpcErrorType; method: RpcMethodType }) => { + if (error) { + return { + jsonrpc: "2.0", + error, + method, + }; + } + return { jsonrpc: "2.0", result, method }; +}; diff --git a/apps/klesia/src/utils/node.ts b/apps/klesia/src/utils/node.ts index e2ad5a5..0125a87 100644 --- a/apps/klesia/src/utils/node.ts +++ b/apps/klesia/src/utils/node.ts @@ -1,10 +1,9 @@ import { Client, cacheExchange, fetchExchange } from "@urql/core"; import { match } from "ts-pattern"; import { z } from "zod"; +import { KlesiaNetwork } from "../schema"; -const NetworkMatcher = z.enum(["devnet", "mainnet"]); - -const MINA_NETWORK = NetworkMatcher.parse(process.env.MINA_NETWORK ?? "devnet"); +const MINA_NETWORK = KlesiaNetwork.parse(process.env.MINA_NETWORK ?? "devnet"); const NODE_API_DEVNET = z .string() .parse( @@ -17,11 +16,15 @@ const NODE_API_MAINNET = z process.env.NODE_API_MAINNET ?? "https://api.minascan.io/node/mainnet/v1/graphql", ); +const NODE_API_ZEKO_DEVNET = z + .string() + .parse(process.env.NODE_API_ZEKO_DEVNET ?? "https://devnet.zeko.io/graphql"); export const getNodeApiUrl = () => { return match(MINA_NETWORK) .with("devnet", () => NODE_API_DEVNET) .with("mainnet", () => NODE_API_MAINNET) + .with("zeko_devnet", () => NODE_API_ZEKO_DEVNET) .exhaustive(); }; diff --git a/bun.lockb b/bun.lockb index 1538fb9..5662d73 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/klesia-sdk/package.json b/packages/klesia-sdk/package.json index 4f7220b..7cc9e07 100644 --- a/packages/klesia-sdk/package.json +++ b/packages/klesia-sdk/package.json @@ -25,7 +25,6 @@ }, "dependencies": { "hono": "^4.5.10", - "ts-pattern": "^5.3.1", - "zod": "^3.23.8" + "ts-pattern": "^5.3.1" } } diff --git a/packages/klesia-sdk/src/client.spec.ts b/packages/klesia-sdk/src/client.spec.ts index acb2051..109e0c2 100644 --- a/packages/klesia-sdk/src/client.spec.ts +++ b/packages/klesia-sdk/src/client.spec.ts @@ -3,9 +3,9 @@ import { createClient } from "./client"; it("fetches transaction count", async () => { const client = createClient({ network: "devnet" }); - const { result } = await client.request({ + const { result } = await client.request<"mina_getTransactionCount">({ method: "mina_getTransactionCount", params: ["B62qkYa1o6Mj6uTTjDQCob7FYZspuhkm4RRQhgJg9j4koEBWiSrTQrS"], }); - expect(result).toBeGreaterThan(0); + expect(BigInt(result)).toBeGreaterThan(0); }); diff --git a/packages/klesia-sdk/src/client.ts b/packages/klesia-sdk/src/client.ts index d93493b..e63c364 100644 --- a/packages/klesia-sdk/src/client.ts +++ b/packages/klesia-sdk/src/client.ts @@ -1,25 +1,57 @@ -import type { KlesiaRpc } from "@mina-js/klesia"; +import { + KlesiaNetwork, + type KlesiaRpc, + type RpcErrorType, + type RpcResponseType, +} from "@mina-js/klesia"; import { hc } from "hono/client"; import { match } from "ts-pattern"; -import { z } from "zod"; -const NetworkMatcher = z.enum(["mainnet", "devnet"]); +type CreateClientProps = { + network: "mainnet" | "devnet" | "zeko_devnet"; + customUrl?: string; + throwable?: boolean; +}; -type CreateClientProps = { network: "mainnet" | "devnet"; customUrl?: string }; +const throwRpcError = ({ + code, + message, +}: { code: number; message: string }) => { + throw new Error(`${code} - ${message}`); +}; -export const createClient = ({ network, customUrl }: CreateClientProps) => { - const baseClient = match(NetworkMatcher.parse(network)) +export const createClient = ({ + network, + customUrl, + throwable = true, +}: CreateClientProps) => { + const baseClient = match(KlesiaNetwork.parse(network)) .with("devnet", () => hc(customUrl ?? "https://devnet.klesia.palladians.xyz"), ) .with("mainnet", () => hc(customUrl ?? "https://mainnet.klesia.palladians.xyz"), ) + .with("zeko_devnet", () => + hc(customUrl ?? "https://zeko-devnet.klesia.palladians.xyz"), + ) .exhaustive(); const rpcHandler = baseClient.api.$post; type RpcRequest = Parameters[0]; - const request = async (req: RpcRequest["json"]) => { - return (await baseClient.api.$post({ json: req })).json(); + const request = async (req: RpcRequest["json"]) => { + const json = (await ( + await baseClient.api.$post({ json: req }) + ).json()) as Extract & { + error?: RpcErrorType; + }; + if (!throwable) { + return json; + } + if (json?.error) { + const { code, message } = json.error; + return throwRpcError({ code, message }); + } + return json; }; return { request,