From 5eea25eee01d7fd90aa6da7e3101e2643ef9349f Mon Sep 17 00:00:00 2001 From: Tomek Marciniak Date: Wed, 9 Oct 2024 14:34:41 +0200 Subject: [PATCH] feat(minajs): improve tx send and add value utils --- .github/workflows/ci.yml | 2 +- apps/klesia/package.json | 2 +- apps/klesia/src/index.ts | 2 +- apps/klesia/src/methods/mina.ts | 17 +- apps/klesia/src/schema.ts | 34 +++- apps/klesia/src/zkapp.ts | 184 ++++++++++++++++++ apps/klesia/tsup.config.ts | 2 +- bun.lockb | Bin 327704 -> 327704 bytes .../src/accounts/mnemonic-to-account.spec.ts | 2 +- .../accounts/private-key-to-account.spec.ts | 2 +- .../src/accounts/private-key-to-account.ts | 2 +- packages/accounts/src/accounts/to-account.ts | 2 +- packages/accounts/src/types.ts | 2 +- packages/accounts/src/validation.ts | 2 +- packages/accounts/tsup.config.ts | 2 +- packages/connect/package.json | 2 +- packages/connect/src/client.spec.ts | 2 +- packages/connect/src/client.ts | 2 +- packages/connect/tsup.config.ts | 2 +- packages/klesia-sdk/tsup.config.ts | 2 +- packages/providers/package.json | 2 +- packages/providers/src/validation.ts | 2 +- packages/providers/tsup.config.ts | 2 +- packages/{shared => utils}/package.json | 2 +- packages/{shared => utils}/src/index.ts | 0 packages/utils/src/src/format-mina.spec.ts | 8 + packages/utils/src/src/format-mina.ts | 8 + packages/utils/src/src/format-units.spec.ts | 41 ++++ packages/utils/src/src/format-units.ts | 20 ++ packages/utils/src/src/parse-mina.spec.ts | 8 + packages/utils/src/src/parse-mina.ts | 8 + packages/utils/src/src/parse-units.spec.ts | 92 +++++++++ packages/utils/src/src/parse-units.ts | 52 +++++ .../{shared => utils}/src/test/constants.ts | 0 packages/{shared => utils}/src/types.ts | 0 packages/{shared => utils}/src/validation.ts | 23 ++- packages/{shared => utils}/tsup.config.ts | 0 37 files changed, 505 insertions(+), 30 deletions(-) create mode 100644 apps/klesia/src/zkapp.ts rename packages/{shared => utils}/package.json (93%) rename packages/{shared => utils}/src/index.ts (100%) create mode 100644 packages/utils/src/src/format-mina.spec.ts create mode 100644 packages/utils/src/src/format-mina.ts create mode 100644 packages/utils/src/src/format-units.spec.ts create mode 100644 packages/utils/src/src/format-units.ts create mode 100644 packages/utils/src/src/parse-mina.spec.ts create mode 100644 packages/utils/src/src/parse-mina.ts create mode 100644 packages/utils/src/src/parse-units.spec.ts create mode 100644 packages/utils/src/src/parse-units.ts rename packages/{shared => utils}/src/test/constants.ts (100%) rename packages/{shared => utils}/src/types.ts (100%) rename packages/{shared => utils}/src/validation.ts (78%) rename packages/{shared => utils}/tsup.config.ts (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9068c6a..fecb90c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,4 +12,4 @@ jobs: - run: bun i --no-save - run: bun run build - run: bun run test - - run: bunx pkg-pr-new publish './packages/klesia-sdk' './packages/accounts' './packages/connect' './packages/providers' './packages/shared' + - run: bunx pkg-pr-new publish './packages/klesia-sdk' './packages/accounts' './packages/connect' './packages/providers' './packages/utils' diff --git a/apps/klesia/package.json b/apps/klesia/package.json index 938a286..5416ebc 100644 --- a/apps/klesia/package.json +++ b/apps/klesia/package.json @@ -21,7 +21,7 @@ "dependencies": { "@hono/node-server": "^1.12.2", "@hono/zod-openapi": "^0.16.0", - "@mina-js/shared": "workspace:*", + "@mina-js/utils": "workspace:*", "@urql/core": "^5.0.6", "bigint-quantile": "^0.0.2", "dayjs": "^1.11.13", diff --git a/apps/klesia/src/index.ts b/apps/klesia/src/index.ts index b3d5064..823fa74 100644 --- a/apps/klesia/src/index.ts +++ b/apps/klesia/src/index.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import { getConnInfo } from "@hono/node-server/conninfo"; import { OpenAPIHono, createRoute } from "@hono/zod-openapi"; -import { PublicKeySchema } from "@mina-js/shared"; +import { PublicKeySchema } from "@mina-js/utils"; import { rateLimiter } from "hono-rate-limiter"; import { cors } from "hono/cors"; import { logger } from "hono/logger"; diff --git a/apps/klesia/src/methods/mina.ts b/apps/klesia/src/methods/mina.ts index df876f8..b67c123 100644 --- a/apps/klesia/src/methods/mina.ts +++ b/apps/klesia/src/methods/mina.ts @@ -1,7 +1,11 @@ -import { SignedTransactionSchema } from "@mina-js/shared"; import { gql } from "@urql/core"; import { calculateQuantile } from "bigint-quantile"; import { match } from "ts-pattern"; +import { + SendDelegationBodySchema, + SendTransactionBodySchema, + SendZkAppBodySchema, +} from "../schema"; import { getNodeClient } from "../utils/node"; export const PRIORITY = { @@ -91,8 +95,8 @@ const sendTransaction = async ({ const client = getNodeClient(); return match(type) .with("payment", async () => { - const { signature, data: input } = - SignedTransactionSchema.parse(signedTransaction); + const { signature, input } = + SendTransactionBodySchema.parse(signedTransaction); const { data } = await client.mutation( gql` mutation { @@ -108,8 +112,8 @@ const sendTransaction = async ({ return data.sendPayment.payment.hash; }) .with("delegation", async () => { - const { signature, data: input } = - SignedTransactionSchema.parse(signedTransaction); + const { signature, input } = + SendDelegationBodySchema.parse(signedTransaction); const { data } = await client.mutation( gql` mutation { @@ -125,6 +129,7 @@ const sendTransaction = async ({ return data.sendDelegation.delegation.hash; }) .with("zkapp", async () => { + const { input } = SendZkAppBodySchema.parse(signedTransaction); const { data } = await client.mutation( gql` mutation { @@ -135,7 +140,7 @@ const sendTransaction = async ({ } } `, - { input: signedTransaction }, + { input }, ); return data.sendZkapp.zkapp.hash; }) diff --git a/apps/klesia/src/schema.ts b/apps/klesia/src/schema.ts index 6e6c8d3..afd15ea 100644 --- a/apps/klesia/src/schema.ts +++ b/apps/klesia/src/schema.ts @@ -1,10 +1,40 @@ -import { PublicKeySchema } from "@mina-js/shared"; +import { + PublicKeySchema, + TransportableDelegationPayload, + TransportableTransactionPayload, +} from "@mina-js/utils"; import { z } from "zod"; +import { SendZkappInput } from "./zkapp"; 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 SignatureSchema = z.union([ + z.object({ + rawSignature: z.string(), + }), + z.object({ field: z.string(), scalar: z.string() }), +]); +export const SendTransactionBodySchema = z.object({ + input: TransportableTransactionPayload, + signature: SignatureSchema, +}); +export const SendDelegationBodySchema = z.object({ + input: TransportableDelegationPayload, + signature: SignatureSchema, +}); +export const SendZkAppBodySchema = z.object({ + input: SendZkappInput, +}); +export const SendableSchema = z.union([ + SendTransactionBodySchema, + SendDelegationBodySchema, + SendZkAppBodySchema, +]); +export const SendTransactionSchema = z.tuple([ + SendableSchema, + z.enum(["payment", "delegation", "zkapp"]), +]); export const RpcMethod = z.enum([ "mina_getTransactionCount", diff --git a/apps/klesia/src/zkapp.ts b/apps/klesia/src/zkapp.ts new file mode 100644 index 0000000..fa32013 --- /dev/null +++ b/apps/klesia/src/zkapp.ts @@ -0,0 +1,184 @@ +import { z } from "zod"; + +// Helper schemas +const PublicKey = z.string(); +const Signature = z.string(); +const Field = z.string(); +const TokenId = z.string(); +const UInt32 = z.number().int(); +const UInt64 = z.number().int(); +const BooleanSchema = z.boolean(); +const AuthRequired = z.enum([ + "None", + "Proof", + "Signature", + "Either", + "Impossible", +]); +const Sign = z.enum(["Positive", "Negative"]); +const Memo = z.string(); +const ZkappProof = z.string(); + +// Complex nested schemas +const VerificationKeyWithHashInput = z.object({ + data: z.string(), + hash: Field, +}); + +const PermissionsInput = z.object({ + editState: AuthRequired, + access: AuthRequired, + send: AuthRequired, + receive: AuthRequired, + setDelegate: AuthRequired, + setPermissions: AuthRequired, + setVerificationKey: z.object({ + auth: AuthRequired, + txnVersion: UInt32, + }), + setZkappUri: AuthRequired, + editActionState: AuthRequired, + setTokenSymbol: AuthRequired, + incrementNonce: AuthRequired, + setVotingFor: AuthRequired, + setTiming: AuthRequired, +}); + +const TimingInput = z.object({ + initialMinimumBalance: UInt64, + cliffTime: UInt32, + cliffAmount: UInt64, + vestingPeriod: UInt32, + vestingIncrement: UInt64, +}); + +const AccountUpdateModificationInput = z.object({ + appState: z.array(Field).optional(), + delegate: PublicKey.optional(), + verificationKey: VerificationKeyWithHashInput.optional(), + permissions: PermissionsInput.optional(), + zkappUri: z.string().optional(), + tokenSymbol: z.string().optional(), + timing: TimingInput.optional(), + votingFor: Field.optional(), +}); + +const BalanceChangeInput = z.object({ + magnitude: UInt64, + sgn: Sign, +}); + +const CurrencyAmountIntervalInput = z.object({ + lower: UInt64, + upper: UInt64, +}); + +const LengthIntervalInput = z.object({ + lower: UInt32, + upper: UInt32, +}); + +const GlobalSlotSinceGenesisIntervalInput = z.object({ + lower: UInt32, + upper: UInt32, +}); + +const EpochLedgerPreconditionInput = z.object({ + hash: Field.optional(), + totalCurrency: CurrencyAmountIntervalInput.optional(), +}); + +const EpochDataPreconditionInput = z.object({ + ledger: EpochLedgerPreconditionInput, + seed: Field.optional(), + startCheckpoint: Field.optional(), + lockCheckpoint: Field.optional(), + epochLength: LengthIntervalInput.optional(), +}); + +const NetworkPreconditionInput = z.object({ + snarkedLedgerHash: Field.optional(), + blockchainLength: LengthIntervalInput.optional(), + minWindowDensity: LengthIntervalInput.optional(), + totalCurrency: CurrencyAmountIntervalInput.optional(), + globalSlotSinceGenesis: GlobalSlotSinceGenesisIntervalInput.optional(), + stakingEpochData: EpochDataPreconditionInput, + nextEpochData: EpochDataPreconditionInput, +}); + +const AccountPreconditionInput = z.object({ + balance: CurrencyAmountIntervalInput.optional(), + nonce: LengthIntervalInput.optional(), + receiptChainHash: Field.optional(), + delegate: PublicKey.optional(), + state: z.array(Field), + actionState: Field.optional(), + provedState: BooleanSchema.optional(), + isNew: BooleanSchema.optional(), +}); + +const PreconditionsInput = z.object({ + network: NetworkPreconditionInput, + account: AccountPreconditionInput, + validWhile: GlobalSlotSinceGenesisIntervalInput.optional(), +}); + +const MayUseTokenInput = z.object({ + parentsOwnToken: BooleanSchema, + inheritFromParent: BooleanSchema, +}); + +const AuthorizationKindStructuredInput = z.object({ + isSigned: BooleanSchema, + isProved: BooleanSchema, + verificationKeyHash: Field, +}); + +const AccountUpdateBodyInput = z.object({ + publicKey: PublicKey, + tokenId: TokenId, + update: AccountUpdateModificationInput, + balanceChange: BalanceChangeInput, + incrementNonce: BooleanSchema, + events: z.array(z.array(Field)), + actions: z.array(z.array(Field)), + callData: Field, + callDepth: z.number().int(), + preconditions: PreconditionsInput, + useFullCommitment: BooleanSchema, + implicitAccountCreationFee: BooleanSchema, + mayUseToken: MayUseTokenInput, + authorizationKind: AuthorizationKindStructuredInput, +}); + +const ControlInput = z.object({ + proof: ZkappProof.optional(), + signature: Signature.optional(), +}); + +const ZkappAccountUpdateInput = z.object({ + body: AccountUpdateBodyInput, + authorization: ControlInput, +}); + +const FeePayerBodyInput = z.object({ + publicKey: PublicKey, + fee: UInt64, + validUntil: UInt32.optional(), + nonce: UInt32, +}); + +const ZkappFeePayerInput = z.object({ + body: FeePayerBodyInput, + authorization: Signature, +}); + +const ZkappCommandInput = z.object({ + feePayer: ZkappFeePayerInput, + accountUpdates: z.array(ZkappAccountUpdateInput), + memo: Memo, +}); + +export const SendZkappInput = z.object({ + zkappCommand: ZkappCommandInput, +}); diff --git a/apps/klesia/tsup.config.ts b/apps/klesia/tsup.config.ts index 5da0516..efbb37e 100644 --- a/apps/klesia/tsup.config.ts +++ b/apps/klesia/tsup.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "tsup"; -import sharedConfig from "../../packages/shared/tsup.config"; +import sharedConfig from "../../packages/utils/tsup.config"; export default defineConfig({ ...sharedConfig, diff --git a/bun.lockb b/bun.lockb index b53b231dc6b3b0db6699fac519bd63cc2c6640a9..eaef67b185ab153f7dd893dbd1d808d75b4475bc 100755 GIT binary patch delta 31056 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 F{}1nVO&I_H diff --git a/packages/accounts/src/accounts/mnemonic-to-account.spec.ts b/packages/accounts/src/accounts/mnemonic-to-account.spec.ts index f3ee2b4..61d66dc 100644 --- a/packages/accounts/src/accounts/mnemonic-to-account.spec.ts +++ b/packages/accounts/src/accounts/mnemonic-to-account.spec.ts @@ -1,5 +1,5 @@ import { expect, it } from "bun:test"; -import { Test } from "@mina-js/shared"; +import { Test } from "@mina-js/utils"; import { mnemonicToAccount } from "./mnemonic-to-account"; it("matches the snapshot", () => { diff --git a/packages/accounts/src/accounts/private-key-to-account.spec.ts b/packages/accounts/src/accounts/private-key-to-account.spec.ts index 42504ff..fb96efd 100644 --- a/packages/accounts/src/accounts/private-key-to-account.spec.ts +++ b/packages/accounts/src/accounts/private-key-to-account.spec.ts @@ -1,5 +1,5 @@ import { expect, it } from "bun:test"; -import { Test } from "@mina-js/shared"; +import { Test } from "@mina-js/utils"; import { privateKeyToAccount } from "./private-key-to-account"; it("matches default values", () => { diff --git a/packages/accounts/src/accounts/private-key-to-account.ts b/packages/accounts/src/accounts/private-key-to-account.ts index a247002..6cdb1b2 100644 --- a/packages/accounts/src/accounts/private-key-to-account.ts +++ b/packages/accounts/src/accounts/private-key-to-account.ts @@ -3,7 +3,7 @@ import { SignedFieldsSchema, SignedMessageSchema, SignedTransactionSchema, -} from "@mina-js/shared"; +} from "@mina-js/utils"; import MinaSigner from "mina-signer"; import type { PrivateKeyAccount } from "../types"; import { toAccount } from "./to-account"; diff --git a/packages/accounts/src/accounts/to-account.ts b/packages/accounts/src/accounts/to-account.ts index d2868bd..c9cfab5 100644 --- a/packages/accounts/src/accounts/to-account.ts +++ b/packages/accounts/src/accounts/to-account.ts @@ -1,4 +1,4 @@ -import { type PublicKey, PublicKeySchema } from "@mina-js/shared"; +import { type PublicKey, PublicKeySchema } from "@mina-js/utils"; import type { AccountSource, CustomSource, diff --git a/packages/accounts/src/types.ts b/packages/accounts/src/types.ts index 92c975a..bc23ebc 100644 --- a/packages/accounts/src/types.ts +++ b/packages/accounts/src/types.ts @@ -4,7 +4,7 @@ import type { SignedFields, SignedMessage, SignedTransaction, -} from "@mina-js/shared"; +} from "@mina-js/utils"; import type { HDKey } from "@scure/bip32"; import type { Simplify } from "type-fest"; import type { z } from "zod"; diff --git a/packages/accounts/src/validation.ts b/packages/accounts/src/validation.ts index ef4f12d..3a77296 100644 --- a/packages/accounts/src/validation.ts +++ b/packages/accounts/src/validation.ts @@ -1,4 +1,4 @@ -import { FieldSchema, TransactionPayload } from "@mina-js/shared"; +import { FieldSchema, TransactionPayload } from "@mina-js/utils"; import { z } from "zod"; export const SignFieldsParamsSchema = z diff --git a/packages/accounts/tsup.config.ts b/packages/accounts/tsup.config.ts index 6e9381b..7ba33c4 100644 --- a/packages/accounts/tsup.config.ts +++ b/packages/accounts/tsup.config.ts @@ -1,3 +1,3 @@ -import sharedConfig from "../shared/tsup.config"; +import sharedConfig from "../utils/tsup.config"; export default sharedConfig; diff --git a/packages/connect/package.json b/packages/connect/package.json index 9eae0af..d8b53eb 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -22,7 +22,7 @@ "cleanup": "rimraf dist .turbo" }, "dependencies": { - "@mina-js/shared": "workspace:*", + "@mina-js/utils": "workspace:*", "@mina-js/accounts": "workspace:*", "@mina-js/klesia-sdk": "workspace:*", "@mina-js/providers": "workspace:*", diff --git a/packages/connect/src/client.spec.ts b/packages/connect/src/client.spec.ts index aa6adf1..8c578b1 100644 --- a/packages/connect/src/client.spec.ts +++ b/packages/connect/src/client.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; import { privateKeyToAccount, toAccount } from "@mina-js/accounts"; -import { Test } from "@mina-js/shared"; +import { Test } from "@mina-js/utils"; import { createWalletClient } from "./client"; const PUBLIC_KEY = "B62qmWKtvNQTtUqo1LxfEEDLyWMg59cp6U7c4uDC7aqgaCEijSc3Hx5"; diff --git a/packages/connect/src/client.ts b/packages/connect/src/client.ts index 34520b2..97c80b1 100644 --- a/packages/connect/src/client.ts +++ b/packages/connect/src/client.ts @@ -7,7 +7,7 @@ import type { SignTransaction, } from "@mina-js/accounts"; import { createClient } from "@mina-js/klesia-sdk"; -import type { PartiallyFormedTransactionProperties } from "@mina-js/shared"; +import type { PartiallyFormedTransactionProperties } from "@mina-js/utils"; import { match } from "ts-pattern"; import { createStore } from "./store"; diff --git a/packages/connect/tsup.config.ts b/packages/connect/tsup.config.ts index 39966ef..81c2c4a 100644 --- a/packages/connect/tsup.config.ts +++ b/packages/connect/tsup.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "tsup"; -import sharedConfig from "../shared/tsup.config"; +import sharedConfig from "../utils/tsup.config"; export default defineConfig({ ...sharedConfig, diff --git a/packages/klesia-sdk/tsup.config.ts b/packages/klesia-sdk/tsup.config.ts index 6e9381b..7ba33c4 100644 --- a/packages/klesia-sdk/tsup.config.ts +++ b/packages/klesia-sdk/tsup.config.ts @@ -1,3 +1,3 @@ -import sharedConfig from "../shared/tsup.config"; +import sharedConfig from "../utils/tsup.config"; export default sharedConfig; diff --git a/packages/providers/package.json b/packages/providers/package.json index 4065105..df429eb 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -18,7 +18,7 @@ "cleanup": "rimraf dist .turbo" }, "dependencies": { - "@mina-js/shared": "workspace:*", + "@mina-js/utils": "workspace:*", "zod": "3.23.8" }, "peerDependencies": { diff --git a/packages/providers/src/validation.ts b/packages/providers/src/validation.ts index 28b9ccd..441c8bc 100644 --- a/packages/providers/src/validation.ts +++ b/packages/providers/src/validation.ts @@ -7,7 +7,7 @@ import { SignedTransactionSchema, TransactionPayload, TransactionReceiptSchema, -} from "@mina-js/shared"; +} from "@mina-js/utils"; import { z } from "zod"; export const SwitchChainRequestParams = z.object({ diff --git a/packages/providers/tsup.config.ts b/packages/providers/tsup.config.ts index 6e9381b..7ba33c4 100644 --- a/packages/providers/tsup.config.ts +++ b/packages/providers/tsup.config.ts @@ -1,3 +1,3 @@ -import sharedConfig from "../shared/tsup.config"; +import sharedConfig from "../utils/tsup.config"; export default sharedConfig; diff --git a/packages/shared/package.json b/packages/utils/package.json similarity index 93% rename from packages/shared/package.json rename to packages/utils/package.json index 4c7114e..08533e1 100644 --- a/packages/shared/package.json +++ b/packages/utils/package.json @@ -1,5 +1,5 @@ { - "name": "@mina-js/shared", + "name": "@mina-js/utils", "version": "0.0.1", "type": "module", "module": "dist/index.js", diff --git a/packages/shared/src/index.ts b/packages/utils/src/index.ts similarity index 100% rename from packages/shared/src/index.ts rename to packages/utils/src/index.ts diff --git a/packages/utils/src/src/format-mina.spec.ts b/packages/utils/src/src/format-mina.spec.ts new file mode 100644 index 0000000..6c66044 --- /dev/null +++ b/packages/utils/src/src/format-mina.spec.ts @@ -0,0 +1,8 @@ +import { expect, it } from "bun:test"; + +import { formatMina } from "./format-mina"; + +it("converts micro-Mina to Mina", () => { + const result = formatMina(5_000_000_000n); + expect(result).toEqual("5"); +}); diff --git a/packages/utils/src/src/format-mina.ts b/packages/utils/src/src/format-mina.ts new file mode 100644 index 0000000..2332da2 --- /dev/null +++ b/packages/utils/src/src/format-mina.ts @@ -0,0 +1,8 @@ +import { formatUnits } from "./format-units"; + +/** + * Formats micro-Mina (1e-6 Mina) to Mina. + */ +export function formatMina(value: bigint) { + return formatUnits(value, 9); +} diff --git a/packages/utils/src/src/format-units.spec.ts b/packages/utils/src/src/format-units.spec.ts new file mode 100644 index 0000000..eaeb97d --- /dev/null +++ b/packages/utils/src/src/format-units.spec.ts @@ -0,0 +1,41 @@ +import { expect, it } from "bun:test"; + +import { formatUnits } from "./format-units"; + +it("converts value to number", () => { + expect(formatUnits(69n, 0)).toEqual("69"); + expect(formatUnits(69n, 5)).toEqual("0.00069"); + expect(formatUnits(690n, 1)).toEqual("69"); + expect(formatUnits(1300000n, 5)).toEqual("13"); + expect(formatUnits(4200000000000n, 10)).toEqual("420"); + expect(formatUnits(20000000000n, 9)).toEqual("20"); + expect(formatUnits(40000000000000000000n, 18)).toEqual("40"); + expect(formatUnits(10000000000000n, 18)).toEqual("0.00001"); + expect(formatUnits(12345n, 4)).toEqual("1.2345"); + expect(formatUnits(12345n, 4)).toEqual("1.2345"); + expect(formatUnits(6942069420123456789123450000n, 18)).toEqual( + "6942069420.12345678912345", + ); + expect( + formatUnits( + 694212312312306942012345444446789123450000000000000000000000000000000n, + 50, + ), + ).toEqual("6942123123123069420.1234544444678912345"); + expect(formatUnits(-690n, 1)).toEqual("-69"); + expect(formatUnits(-1300000n, 5)).toEqual("-13"); + expect(formatUnits(-4200000000000n, 10)).toEqual("-420"); + expect(formatUnits(-20000000000n, 9)).toEqual("-20"); + expect(formatUnits(-40000000000000000000n, 18)).toEqual("-40"); + expect(formatUnits(-12345n, 4)).toEqual("-1.2345"); + expect(formatUnits(-12345n, 4)).toEqual("-1.2345"); + expect(formatUnits(-6942069420123456789123450000n, 18)).toEqual( + "-6942069420.12345678912345", + ); + expect( + formatUnits( + -694212312312306942012345444446789123450000000000000000000000000000000n, + 50, + ), + ).toEqual("-6942123123123069420.1234544444678912345"); +}); diff --git a/packages/utils/src/src/format-units.ts b/packages/utils/src/src/format-units.ts new file mode 100644 index 0000000..50f6461 --- /dev/null +++ b/packages/utils/src/src/format-units.ts @@ -0,0 +1,20 @@ +/** + * Divides a number by a given exponent of base 10 (10exponent), and formats it into a string representation of the number.. + */ +export function formatUnits(value: bigint, decimals: number) { + let display = value.toString(); + + const negative = display.startsWith("-"); + if (negative) display = display.slice(1); + + display = display.padStart(decimals, "0"); + + let [integer, fraction] = [ + display.slice(0, display.length - decimals), + display.slice(display.length - decimals), + ]; + fraction = fraction.replace(/(0+)$/, ""); + return `${negative ? "-" : ""}${integer || "0"}${ + fraction ? `.${fraction}` : "" + }`; +} diff --git a/packages/utils/src/src/parse-mina.spec.ts b/packages/utils/src/src/parse-mina.spec.ts new file mode 100644 index 0000000..8539aa7 --- /dev/null +++ b/packages/utils/src/src/parse-mina.spec.ts @@ -0,0 +1,8 @@ +import { expect, it } from "bun:test"; + +import { parseMina } from "./parse-mina"; + +it("converts Mina to micro-Mina", () => { + const result = parseMina("5"); + expect(result).toEqual(5_000_000_000n); +}); diff --git a/packages/utils/src/src/parse-mina.ts b/packages/utils/src/src/parse-mina.ts new file mode 100644 index 0000000..428aff6 --- /dev/null +++ b/packages/utils/src/src/parse-mina.ts @@ -0,0 +1,8 @@ +import { parseUnits } from "./parse-units"; + +/** + * Parse a mina value to a micro-Mina value. + */ +export const parseMina = (minaValue: string) => { + return parseUnits(minaValue, 9); +}; diff --git a/packages/utils/src/src/parse-units.spec.ts b/packages/utils/src/src/parse-units.spec.ts new file mode 100644 index 0000000..b23bb8d --- /dev/null +++ b/packages/utils/src/src/parse-units.spec.ts @@ -0,0 +1,92 @@ +import { expect, it } from "bun:test"; + +import { parseUnits } from "./parse-units"; + +it("converts number to unit of a given length", () => { + expect(parseUnits("69", 1)).toEqual(690n); + expect(parseUnits("13", 5)).toEqual(1300000n); + expect(parseUnits("420", 10)).toEqual(4200000000000n); + expect(parseUnits("20", 9)).toEqual(20000000000n); + expect(parseUnits("40", 18)).toEqual(40000000000000000000n); + expect(parseUnits("1.2345", 4)).toEqual(12345n); + expect(parseUnits("1.0045", 4)).toEqual(10045n); + expect(parseUnits("1.2345000", 4)).toEqual(12345n); + expect(parseUnits("6942069420.12345678912345", 18)).toEqual( + 6942069420123456789123450000n, + ); + expect(parseUnits("6942069420.00045678912345", 18)).toEqual( + 6942069420000456789123450000n, + ); + expect(parseUnits("6942123123123069420.1234544444678912345", 50)).toEqual( + 694212312312306942012345444446789123450000000000000000000000000000000n, + ); + expect(parseUnits("-69", 1)).toEqual(-690n); + expect(parseUnits("-1.2345", 4)).toEqual(-12345n); + expect(parseUnits("-6942069420.12345678912345", 18)).toEqual( + -6942069420123456789123450000n, + ); + expect(parseUnits("-6942123123123069420.1234544444678912345", 50)).toEqual( + -694212312312306942012345444446789123450000000000000000000000000000000n, + ); +}); + +it("converts when decimals === 0", () => { + expect(parseUnits("69.2352112312312451512412341231", 0)).toEqual(69n); + expect(parseUnits("69.5952141234124125231523412312", 0)).toEqual(70n); + expect(parseUnits("12301000000000000020000", 0)).toEqual( + 12301000000000000020000n, + ); + expect(parseUnits("12301000000000000020000.123", 0)).toEqual( + 12301000000000000020000n, + ); + expect(parseUnits("12301000000000000020000.5", 0)).toEqual( + 12301000000000000020001n, + ); + expect(parseUnits("99999999999999999999999.5", 0)).toEqual( + 100000000000000000000000n, + ); +}); + +it("converts when decimals < fraction length", () => { + expect(parseUnits("69.23521", 0)).toEqual(69n); + expect(parseUnits("69.56789", 0)).toEqual(70n); + expect(parseUnits("69.23521", 1)).toEqual(692n); + expect(parseUnits("69.23521", 2)).toEqual(6924n); + expect(parseUnits("69.23221", 2)).toEqual(6923n); + expect(parseUnits("69.23261", 3)).toEqual(69233n); + expect(parseUnits("999999.99999", 3)).toEqual(1000000000n); + expect(parseUnits("699999.99999", 3)).toEqual(700000000n); + expect(parseUnits("699999.98999", 3)).toEqual(699999990n); + expect(parseUnits("699959.99999", 3)).toEqual(699960000n); + expect(parseUnits("699099.99999", 3)).toEqual(699100000n); + expect(parseUnits("100000.000999", 3)).toEqual(100000001n); + expect(parseUnits("100000.990999", 3)).toEqual(100000991n); + expect(parseUnits("69.00221", 3)).toEqual(69002n); + expect(parseUnits("1.0536059576998882", 7)).toEqual(10536060n); + expect(parseUnits("1.0053059576998882", 7)).toEqual(10053060n); + expect(parseUnits("1.0000000900000000", 7)).toEqual(10000001n); + expect(parseUnits("1.0000009900000000", 7)).toEqual(10000010n); + expect(parseUnits("1.0000099900000000", 7)).toEqual(10000100n); + expect(parseUnits("1.0000092900000000", 7)).toEqual(10000093n); + expect(parseUnits("1.5536059576998882", 7)).toEqual(15536060n); + expect(parseUnits("1.0536059476998882", 7)).toEqual(10536059n); + expect(parseUnits("1.4545454545454545", 7)).toEqual(14545455n); + expect(parseUnits("1.1234567891234567", 7)).toEqual(11234568n); + expect(parseUnits("1.8989898989898989", 7)).toEqual(18989899n); + expect(parseUnits("9.9999999999999999", 7)).toEqual(100000000n); + expect(parseUnits("0.0536059576998882", 7)).toEqual(536060n); + expect(parseUnits("0.0053059576998882", 7)).toEqual(53060n); + expect(parseUnits("0.0000000900000000", 7)).toEqual(1n); + expect(parseUnits("0.0000009900000000", 7)).toEqual(10n); + expect(parseUnits("0.0000099900000000", 7)).toEqual(100n); + expect(parseUnits("0.0000092900000000", 7)).toEqual(93n); + expect(parseUnits("0.0999999999999999", 7)).toEqual(1000000n); + expect(parseUnits("0.0099999999999999", 7)).toEqual(100000n); + expect(parseUnits("0.00000000059", 9)).toEqual(1n); + expect(parseUnits("0.0000000003", 9)).toEqual(0n); + expect(parseUnits("69.00000000000", 9)).toEqual(69000000000n); + expect(parseUnits("69.00000000019", 9)).toEqual(69000000000n); + expect(parseUnits("69.00000000059", 9)).toEqual(69000000001n); + expect(parseUnits("69.59000000059", 9)).toEqual(69590000001n); + expect(parseUnits("69.59000002359", 9)).toEqual(69590000024n); +}); diff --git a/packages/utils/src/src/parse-units.ts b/packages/utils/src/src/parse-units.ts new file mode 100644 index 0000000..98d5f43 --- /dev/null +++ b/packages/utils/src/src/parse-units.ts @@ -0,0 +1,52 @@ +/** + * Multiplies a string representation of a number by a given exponent of base 10 (10exponent). + * + * - Docs: https://viem.sh/docs/utilities/parseUnits + * + * @example + * import { parseUnits } from 'viem' + * + * parseUnits('420', 9) + * // 420000000000n + */ +export function parseUnits(value: string, decimals: number) { + if (!/^(-?)([0-9]*)\.?([0-9]*)$/.test(value)) + throw new Error("Invalid decimal value."); + + let [integer, fraction = "0"] = value.split("."); + + const negative = integer.startsWith("-"); + if (negative) integer = integer.slice(1); + + // trim trailing zeros. + fraction = fraction.replace(/(0+)$/, ""); + + // round off if the fraction is larger than the number of decimals. + if (decimals === 0) { + if (Math.round(Number(`.${fraction}`)) === 1) + integer = `${BigInt(integer) + 1n}`; + fraction = ""; + } else if (fraction.length > decimals) { + const [left, unit, right] = [ + fraction.slice(0, decimals - 1), + fraction.slice(decimals - 1, decimals), + fraction.slice(decimals), + ]; + + const rounded = Math.round(Number(`${unit}.${right}`)); + if (rounded > 9) + fraction = `${BigInt(left) + BigInt(1)}0`.padStart(left.length + 1, "0"); + else fraction = `${left}${rounded}`; + + if (fraction.length > decimals) { + fraction = fraction.slice(1); + integer = `${BigInt(integer) + 1n}`; + } + + fraction = fraction.slice(0, decimals); + } else { + fraction = fraction.padEnd(decimals, "0"); + } + + return BigInt(`${negative ? "-" : ""}${integer}${fraction}`); +} diff --git a/packages/shared/src/test/constants.ts b/packages/utils/src/test/constants.ts similarity index 100% rename from packages/shared/src/test/constants.ts rename to packages/utils/src/test/constants.ts diff --git a/packages/shared/src/types.ts b/packages/utils/src/types.ts similarity index 100% rename from packages/shared/src/types.ts rename to packages/utils/src/types.ts diff --git a/packages/shared/src/validation.ts b/packages/utils/src/validation.ts similarity index 78% rename from packages/shared/src/validation.ts rename to packages/utils/src/validation.ts index 27b6957..b8b7ad4 100644 --- a/packages/shared/src/validation.ts +++ b/packages/utils/src/validation.ts @@ -27,18 +27,37 @@ export const PublicKeySchema = z.string().length(55).startsWith("B62"); export const PrivateKeySchema = z.string().length(52); -export const TransactionPayload = z +export const DelegationPayload = z .object({ from: PublicKeySchema, to: PublicKeySchema, memo: z.string().optional(), fee: z.coerce.bigint(), - amount: z.coerce.bigint(), nonce: z.coerce.bigint(), validUntil: z.coerce.bigint().optional(), }) .strict(); +export const TransportableDelegationPayload = z + .object({ + from: PublicKeySchema, + to: PublicKeySchema, + memo: z.string().optional(), + fee: z.coerce.string(), + nonce: z.coerce.string(), + validUntil: z.coerce.string().optional(), + }) + .strict(); + +export const TransactionPayload = DelegationPayload.extend({ + amount: z.coerce.bigint(), +}).strict(); + +export const TransportableTransactionPayload = + TransportableDelegationPayload.extend({ + amount: z.coerce.string(), + }).strict(); + export const PartiallyFormedTransactionPayload = TransactionPayload.extend({ fee: z.coerce.bigint().optional(), nonce: z.coerce.bigint().optional(), diff --git a/packages/shared/tsup.config.ts b/packages/utils/tsup.config.ts similarity index 100% rename from packages/shared/tsup.config.ts rename to packages/utils/tsup.config.ts