diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d77462f --- /dev/null +++ b/.gitignore @@ -0,0 +1,177 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +dist + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..a72d553 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# mina-js + +The TypeScript interface for Mina Protocol. + +## Features + +- Interact with Mina Wallets such as [Pallad](https://pallad.co/). +- Browser native BigInt. +- TypeScript ready. + +## Acknowledgements + +- This is not a library to write provable programs. In that case head over to [o1js](https://github.com/o1-labs/o1js). +- Inspired heavily by [viem](https://github.com/wevm/viem). +- Great part of the code is based our work and code of [Pallad](https://pallad.co). + +## Authors + +- [@mrcnk](https://github.com/mrcnk) ([X.com](https://x.com/schoolboytom)) diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..88f3b31 --- /dev/null +++ b/biome.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": ["dist/**/*"] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..887a7a2 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..768b546 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "mina-js", + "scripts": { + "build": "tsup", + "test": "bun test", + "lint": "bunx biome check .", + "format": "bunx biome check . --write", + "format:unsafe": "bunx biome check . --write --unsafe" + }, + "devDependencies": { + "@biomejs/biome": "1.8.3", + "@tsconfig/bun": "1.0.7", + "@types/bun": "1.1.6", + "tsup": "8.2.3" + }, + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0", + "mina-signer": "3.0.7", + "zod": "3.23.8" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/src/accounts/generate-mnemonic.spec.ts b/src/accounts/generate-mnemonic.spec.ts new file mode 100644 index 0000000..c14c8b2 --- /dev/null +++ b/src/accounts/generate-mnemonic.spec.ts @@ -0,0 +1,8 @@ +import { expect, it } from "bun:test"; +import { english } from "."; +import { generateMnemonic } from "./generate-mnemonic"; + +it("generates 12 word mnemonic", () => { + const mnemonic = generateMnemonic(english); + expect(mnemonic.split(" ").length).toBe(12); +}); diff --git a/src/accounts/generate-mnemonic.ts b/src/accounts/generate-mnemonic.ts new file mode 100644 index 0000000..9ed1305 --- /dev/null +++ b/src/accounts/generate-mnemonic.ts @@ -0,0 +1,16 @@ +import { generateMnemonic as generateMnemonic_ } from "@scure/bip39"; + +/** + * @description Generates a random mnemonic phrase with a given wordlist. + * + * @param wordlist The wordlist to use for generating the mnemonic phrase. + * @param strength mnemonic strength 128-256 bits + * + * @returns A randomly generated mnemonic phrase. + */ +export function generateMnemonic( + wordlist: string[], + strength?: number | undefined, +): string { + return generateMnemonic_(wordlist, strength); +} diff --git a/src/accounts/generate-private-key.spec.ts b/src/accounts/generate-private-key.spec.ts new file mode 100644 index 0000000..4ee9193 --- /dev/null +++ b/src/accounts/generate-private-key.spec.ts @@ -0,0 +1,12 @@ +import { expect, it } from "bun:test"; +import { generatePrivateKey } from "./generate-private-key"; + +it("returns a string", () => { + const privateKey = generatePrivateKey(); + expect(typeof privateKey).toBe("string"); +}); + +it("has a length of 64 characters", () => { + const privateKey = generatePrivateKey(); + expect(privateKey.length).toBe(52); +}); diff --git a/src/accounts/generate-private-key.ts b/src/accounts/generate-private-key.ts new file mode 100644 index 0000000..248abad --- /dev/null +++ b/src/accounts/generate-private-key.ts @@ -0,0 +1,11 @@ +import Client from "mina-signer"; + +/** + * @description Generates a random private key. + * + * @returns A randomly generated private key. + */ +export function generatePrivateKey(): string { + const client = new Client({ network: "mainnet" }); + return client.genKeys().privateKey; +} diff --git a/src/accounts/index.ts b/src/accounts/index.ts new file mode 100644 index 0000000..89aa83b --- /dev/null +++ b/src/accounts/index.ts @@ -0,0 +1,4 @@ +export { wordlist as english } from "@scure/bip39/wordlists/english"; + +export { generateMnemonic } from "./generate-mnemonic"; +export { generatePrivateKey } from "./generate-private-key"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6520f28 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +import "./accounts"; diff --git a/src/providers/index.ts b/src/providers/index.ts new file mode 100644 index 0000000..0b70ea2 --- /dev/null +++ b/src/providers/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./validation"; diff --git a/src/providers/types.ts b/src/providers/types.ts new file mode 100644 index 0000000..567b7bb --- /dev/null +++ b/src/providers/types.ts @@ -0,0 +1,170 @@ +import type { + AddChainData, + CreateNullifierData, + SendTransactionData, + SignFieldsData, + SignMessageData, + SignTransactionData, + SwitchChainData, +} from "./validation"; + +// biome-ignore lint/suspicious/noExplicitAny: Deal with it. +type TODO = any; + +export type Address = `b62${string}`; + +export type MinaProviderDetail = { + info: MinaProviderInfo; + provider: MinaProviderClient; +}; + +export type MinaProviderInfo = { + icon: `data:image/${string}`; + name: string; + rdns: string; + slug: string; +}; + +export interface MinaAnnounceProviderEvent + extends CustomEvent { + type: "mina:announceProvider"; +} + +export interface EIP6963RequestProviderEvent extends Event { + type: "mina:requestProvider"; +} + +export type ProviderRpcError = ( + | { message: "User Rejected Request"; code: 4001 } + | { message: "Unauthorized"; code: 4100 } + | { message: "Unsupported Method"; code: 4200 } + | { message: "Disconnected"; code: 4900 } + | { message: "Chain Disconnected"; code: 4901 } +) & + Error; + +export type ProviderRpcEvent = + | "connect" + | "disconnect" + | "chainChanged" + | "accountsChanged" + | "mina_message"; + +// Return types +type SignedMessage = { + publicKey: string; + data: string; + signature: { + field: string; + scalar: string; + }; +}; + +type SignedFieldsData = { + data: (string | number)[]; + publicKey: string; + signature: string; +}; + +// Request variants +export type AccountsRequest = (args: { + method: "mina_accounts"; +}) => Promise; + +export type ChainIdRequest = (args: { + method: "mina_chainId"; +}) => Promise; + +export type ChainInformationRequest = (args: { + method: "mina_chainInformation"; +}) => Promise<{ url: string; name: string }>; + +export type GetBalanceRequest = (args: { + method: "mina_getBalance"; +}) => Promise; + +export type SignRequest = (args: { + method: "mina_sign"; + params: SignMessageData; +}) => Promise; + +export type SignFieldsRequest = (args: { + method: "mina_signFields"; + params: SignFieldsData; +}) => Promise; + +export type SignTransactionRequest = (args: { + method: "mina_signTransaction"; + params: SignTransactionData; +}) => Promise; + +export type SendTransactionRequest = (args: { + method: "mina_sendTransaction"; + params: SendTransactionData; +}) => Promise; + +export type CreateNullifierRequest = (args: { + method: "mina_createNullifier"; + params: CreateNullifierData; +}) => Promise; + +export type SwitchChainRequest = (args: { + method: "mina_switchChain"; + params: SwitchChainData; +}) => Promise; + +export type AddChainRequest = (args: { + method: "mina_addChain"; + params: AddChainData; +}) => Promise; + +export type ProviderRequest = + | AccountsRequest + | ChainIdRequest + | ChainInformationRequest + | GetBalanceRequest + | SignRequest + | SignFieldsRequest + | SignTransactionRequest + | SendTransactionRequest + | CreateNullifierRequest + | SwitchChainRequest + | AddChainRequest; + +export type ConnectedListener = ( + event: "connected", + callback: (params: { chainId: string }) => void, +) => void; + +export type DisconnectedListener = ( + event: "disconnected", + callback: (params: { error: ProviderRpcError }) => void, +) => void; + +export type ChainChangedListener = ( + event: "chainChanged", + callback: (params: { chainId: string }) => void, +) => void; + +export type AccountsChangedListener = ( + event: "accountsChanged", + callback: (params: { accounts: Address[] }) => void, +) => void; + +export type MessageListener = ( + event: "mina_message", + callback: (params: { type: string; data: unknown }) => void, +) => void; + +export type ProviderListener = + | ConnectedListener + | DisconnectedListener + | ChainChangedListener + | AccountsChangedListener + | MessageListener; + +export type MinaProviderClient = { + request: ProviderRequest; + addListener: ProviderListener; + removeListener: ProviderListener; +}; diff --git a/src/providers/validation.ts b/src/providers/validation.ts new file mode 100644 index 0000000..1214287 --- /dev/null +++ b/src/providers/validation.ts @@ -0,0 +1,79 @@ +import { z } from "zod"; + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; +const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]), +); + +export const signFieldsRequestSchema = z.object({ + fields: z.array(z.coerce.number()), +}); + +export type SignFieldsData = z.infer; + +export const signMessageRequestSchema = z.object({ + message: z.string(), +}); + +export type SignMessageData = z.infer; + +export const createNullifierRequestSchema = z.object({ + message: z.array(z.coerce.number()), +}); + +export type CreateNullifierData = z.infer; + +export const publicKeySchema = z.string().length(55); + +export const transactionSchema = z + .object({ + to: publicKeySchema, + from: publicKeySchema, + fee: z.coerce.number(), + nonce: z.coerce.number(), + memo: z.string().optional(), + validUntil: z.coerce.number().optional(), + amount: z.coerce.number().optional(), + }) + .strict(); + +export const signatureSchema = z + .object({ + field: z.string(), + scalar: z.string(), + }) + .strict(); + +export const signedTransactionSchema = z.object({ + data: transactionSchema, + publicKey: publicKeySchema, + signature: signatureSchema, +}); + +export const signTransactionRequestSchema = z.object({ + transaction: transactionSchema.strict(), +}); + +export type SignTransactionData = z.infer; + +export const sendTransactionRequestSchema = z.object({ + signedTransaction: signedTransactionSchema.strict(), + transactionType: z.enum(["payment", "delegation", "zkapp"]), +}); + +export type SendTransactionData = z.infer; + +export const switchChainRequestSchema = z.object({ + chainId: z.string(), +}); + +export type SwitchChainData = z.infer; + +export const addChainRequestSchema = z.object({ + url: z.string().url(), + name: z.string(), +}); + +export type AddChainData = z.infer; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..78e22f0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/bun/tsconfig.json" +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..843abb8 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/accounts/index.ts", "src/providers/index.ts"], + outDir: "./dist", + format: "esm", + sourcemap: true, + clean: true, + bundle: true, + dts: true, +});