From b225e763fcccb8047c39c8dfe02ea70f61cc568d Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:27:52 -0800 Subject: [PATCH] Add borsh types/serialization (#6) * Add borsh types/serialization * Add borsher * Add forgotten type --- package.json | 1 + pnpm-lock.yaml | 16 +++ src/types/locker.test.ts | 243 +++++++++++++++++++++++++++++++++++++++ src/types/locker.ts | 108 +++++++++++++++++ src/types/prover.test.ts | 109 ++++++++++++++++++ src/types/prover.ts | 145 +++++++++++++++++++++++ 6 files changed, 622 insertions(+) create mode 100644 src/types/locker.test.ts create mode 100644 src/types/locker.ts create mode 100644 src/types/prover.test.ts create mode 100644 src/types/prover.ts diff --git a/package.json b/package.json index 992ef63..5c2821b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "MIT", "dependencies": { "@solana/web3.js": "^1.95.5", + "borsher": "^3.5.0", "ethers": "^6.13.4", "near-api-js": "^5.0.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3107346..b7b2bd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@solana/web3.js': specifier: ^1.95.5 version: 1.95.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) + borsher: + specifier: ^3.5.0 + version: 3.5.0 ethers: specifier: ^6.13.4 version: 6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -494,6 +497,12 @@ packages: borsh@1.0.0: resolution: {integrity: sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==} + borsh@2.0.0: + resolution: {integrity: sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==} + + borsher@3.5.0: + resolution: {integrity: sha512-53UlE2ukArKGrw3u+MKR5CBqYR+Fr47tGAeIRmAy+W5G6FMRnoM7G8mHYFeijGyhKje5aaQTCqZu/hugQ2HiBA==} + brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} @@ -1409,6 +1418,13 @@ snapshots: borsh@1.0.0: {} + borsh@2.0.0: {} + + borsher@3.5.0: + dependencies: + borsh: 2.0.0 + buffer: 6.0.3 + brorand@1.1.0: {} bs58@4.0.0: diff --git a/src/types/locker.test.ts b/src/types/locker.test.ts new file mode 100644 index 0000000..6c4d8a4 --- /dev/null +++ b/src/types/locker.test.ts @@ -0,0 +1,243 @@ +import { borshDeserialize } from "borsher" +import { describe, expect, test } from "vitest" +import { + type BindTokenArgs, + BindTokenArgsSchema, + ChainKind, + type ClaimFeeArgs, + ClaimFeeArgsSchema, + type DeployTokenArgs, + DeployTokenArgsSchema, + type FinTransferArgs, + FinTransferArgsSchema, + type StorageDepositAction, + StorageDepositActionSchema, + serializeBindTokenArgs, + serializeClaimFeeArgs, + serializeDeployTokenArgs, + serializeFinTransferArgs, + serializeStorageDepositAction, +} from "./locker" + +describe("Chain Kind Types", () => { + describe("StorageDepositAction", () => { + test("should serialize and deserialize with storage deposit amount", () => { + const action: StorageDepositAction = { + token_id: "token.near", + account_id: "user.near", + storage_deposit_amount: 1000000n, + } + + const serialized = serializeStorageDepositAction(action) + const deserialized = borshDeserialize(StorageDepositActionSchema, serialized) + + expect(deserialized).toEqual(action) + }) + + test("should handle null storage deposit amount", () => { + const action: StorageDepositAction = { + token_id: "token.near", + account_id: "user.near", + storage_deposit_amount: null, + } + + const serialized = serializeStorageDepositAction(action) + const deserialized = borshDeserialize(StorageDepositActionSchema, serialized) + + expect(deserialized).toEqual(action) + }) + + test("should handle long account IDs", () => { + const action: StorageDepositAction = { + token_id: "very.long.token.name.near", + account_id: "very.long.account.name.near", + storage_deposit_amount: 1000000n, + } + + const serialized = serializeStorageDepositAction(action) + const deserialized = borshDeserialize(StorageDepositActionSchema, serialized) + + expect(deserialized).toEqual(action) + }) + }) + + describe("FinTransferArgs", () => { + test("should serialize and deserialize with multiple storage deposit actions", () => { + const args: FinTransferArgs = { + chain_kind: ChainKind.Near, + storage_deposit_actions: [ + { + token_id: "token1.near", + account_id: "user1.near", + storage_deposit_amount: 1000000n, + }, + { + token_id: "token2.near", + account_id: "user2.near", + storage_deposit_amount: null, + }, + ], + prover_args: new Uint8Array([1, 2, 3]), + } + + const serialized = serializeFinTransferArgs(args) + const deserialized: FinTransferArgs = borshDeserialize(FinTransferArgsSchema, serialized) + + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + + test("should handle empty storage deposit actions array", () => { + const args: FinTransferArgs = { + chain_kind: ChainKind.Near, + storage_deposit_actions: [], + prover_args: new Uint8Array([1, 2, 3]), + } + + const serialized = serializeFinTransferArgs(args) + const deserialized: FinTransferArgs = borshDeserialize(FinTransferArgsSchema, serialized) + + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + + test("should handle different chain kinds", () => { + const chainKinds = [ + ChainKind.Eth, + ChainKind.Near, + ChainKind.Sol, + ChainKind.Arb, + ChainKind.Base, + ] + + for (const chainKind of chainKinds) { + const args: FinTransferArgs = { + chain_kind: chainKind, + storage_deposit_actions: [], + prover_args: new Uint8Array([1, 2, 3]), + } + + const serialized = serializeFinTransferArgs(args) + const deserialized: FinTransferArgs = borshDeserialize(FinTransferArgsSchema, serialized) + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + } + }) + }) + + describe("ClaimFeeArgs", () => { + test("should serialize and deserialize with different chain kinds", () => { + const args: ClaimFeeArgs = { + chain_kind: ChainKind.Eth, + prover_args: new Uint8Array([1, 2, 3]), + } + + const serialized = serializeClaimFeeArgs(args) + const deserialized: ClaimFeeArgs = borshDeserialize(ClaimFeeArgsSchema, serialized) + + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + + test("should handle empty prover args", () => { + const args: ClaimFeeArgs = { + chain_kind: ChainKind.Sol, + prover_args: new Uint8Array([]), + } + + const serialized = serializeClaimFeeArgs(args) + const deserialized: ClaimFeeArgs = borshDeserialize(ClaimFeeArgsSchema, serialized) + + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + }) + + describe("BindTokenArgs", () => { + test("should serialize and deserialize correctly", () => { + const args: BindTokenArgs = { + chain_kind: ChainKind.Eth, + prover_args: new Uint8Array([1, 2, 3]), + } + + const serialized = serializeBindTokenArgs(args) + const deserialized: BindTokenArgs = borshDeserialize(BindTokenArgsSchema, serialized) + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + }) + + describe("DeployTokenArgs", () => { + test("should serialize and deserialize correctly", () => { + const args: DeployTokenArgs = { + chain_kind: ChainKind.Base, + prover_args: new Uint8Array([1, 2, 3]), + } + + const serialized = serializeDeployTokenArgs(args) + const deserialized: DeployTokenArgs = borshDeserialize(DeployTokenArgsSchema, serialized) + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + }) + + describe("Edge Cases", () => { + test("should handle large storage deposit amounts", () => { + const action: StorageDepositAction = { + token_id: "token.near", + account_id: "user.near", + storage_deposit_amount: BigInt("340282366920938463463374607431768211455"), // u128 max + } + + const serialized = serializeStorageDepositAction(action) + const deserialized = borshDeserialize(StorageDepositActionSchema, serialized) + + expect(deserialized).toEqual(action) + }) + + test("should handle large prover args", () => { + const largeProverArgs = new Uint8Array(1000).fill(1) + const args: FinTransferArgs = { + chain_kind: ChainKind.Near, + storage_deposit_actions: [], + prover_args: largeProverArgs, + } + + const serialized = serializeFinTransferArgs(args) + const deserialized: FinTransferArgs = borshDeserialize(FinTransferArgsSchema, serialized) + expect({ + ...deserialized, + prover_args: Uint8Array.from(deserialized.prover_args), + }).toEqual(args) + }) + + test("should handle maximum length account IDs", () => { + // NEAR account IDs have a maximum length of 64 characters + const maxLengthAccountId = "a".repeat(64) + const action: StorageDepositAction = { + token_id: maxLengthAccountId, + account_id: maxLengthAccountId, + storage_deposit_amount: 1000000n, + } + + const serialized = serializeStorageDepositAction(action) + const deserialized = borshDeserialize(StorageDepositActionSchema, serialized) + + expect(deserialized).toEqual(action) + }) + }) +}) diff --git a/src/types/locker.ts b/src/types/locker.ts new file mode 100644 index 0000000..185b256 --- /dev/null +++ b/src/types/locker.ts @@ -0,0 +1,108 @@ +import { BorshSchema, type Unit, borshSerialize } from "borsher" + +// Basic type aliases +export type AccountId = string +export type U128 = bigint + +export type ChainKind = + | { Eth: Unit } + | { Near: Unit } + | { Sol: Unit } + | { Arb: Unit } + | { Base: Unit } + +export const ChainKind = { + Eth: { Eth: {} } as ChainKind, + Near: { Near: {} } as ChainKind, + Sol: { Sol: {} } as ChainKind, + Arb: { Arb: {} } as ChainKind, + Base: { Base: {} } as ChainKind, +} as const + +export const ChainKindSchema = BorshSchema.Enum({ + Eth: BorshSchema.Unit, + Near: BorshSchema.Unit, + Sol: BorshSchema.Unit, + Arb: BorshSchema.Unit, + Base: BorshSchema.Unit, +}) + +// StorageDepositAction type +export type StorageDepositAction = { + token_id: AccountId + account_id: AccountId + storage_deposit_amount: bigint | null +} + +export const StorageDepositActionSchema = BorshSchema.Struct({ + token_id: BorshSchema.String, + account_id: BorshSchema.String, + storage_deposit_amount: BorshSchema.Option(BorshSchema.u128), +}) + +// FinTransferArgs type +export type FinTransferArgs = { + chain_kind: ChainKind + storage_deposit_actions: StorageDepositAction[] + prover_args: Uint8Array +} + +export const FinTransferArgsSchema = BorshSchema.Struct({ + chain_kind: ChainKindSchema, + storage_deposit_actions: BorshSchema.Vec(StorageDepositActionSchema), + prover_args: BorshSchema.Vec(BorshSchema.u8), +}) + +// ClaimFeeArgs type +export type ClaimFeeArgs = { + chain_kind: ChainKind + prover_args: Uint8Array +} + +export const ClaimFeeArgsSchema = BorshSchema.Struct({ + chain_kind: ChainKindSchema, + prover_args: BorshSchema.Vec(BorshSchema.u8), +}) + +// BindTokenArgs type +export type BindTokenArgs = { + chain_kind: ChainKind + prover_args: Uint8Array +} + +export const BindTokenArgsSchema = BorshSchema.Struct({ + chain_kind: ChainKindSchema, + prover_args: BorshSchema.Vec(BorshSchema.u8), +}) + +// DeployTokenArgs type +export type DeployTokenArgs = { + chain_kind: ChainKind + prover_args: Uint8Array +} + +export const DeployTokenArgsSchema = BorshSchema.Struct({ + chain_kind: ChainKindSchema, + prover_args: BorshSchema.Vec(BorshSchema.u8), +}) + +// Serialization helper functions +export const serializeStorageDepositAction = (action: StorageDepositAction): Uint8Array => { + return borshSerialize(StorageDepositActionSchema, action) +} + +export const serializeFinTransferArgs = (args: FinTransferArgs): Uint8Array => { + return borshSerialize(FinTransferArgsSchema, args) +} + +export const serializeClaimFeeArgs = (args: ClaimFeeArgs): Uint8Array => { + return borshSerialize(ClaimFeeArgsSchema, args) +} + +export const serializeBindTokenArgs = (args: BindTokenArgs): Uint8Array => { + return borshSerialize(BindTokenArgsSchema, args) +} + +export const serializeDeployTokenArgs = (args: DeployTokenArgs): Uint8Array => { + return borshSerialize(DeployTokenArgsSchema, args) +} diff --git a/src/types/prover.test.ts b/src/types/prover.test.ts new file mode 100644 index 0000000..3c3a6d2 --- /dev/null +++ b/src/types/prover.test.ts @@ -0,0 +1,109 @@ +import { borshDeserialize, borshSerialize } from "borsher" +import { describe, expect, it } from "vitest" +import { type InitTransferResult, type ProverResult, ProverResultSchema } from "./prover" + +describe("Borsh Serialization", () => { + describe("InitTransfer", () => { + const initTransfer: ProverResult = { + InitTransfer: { + origin_nonce: 1234n, + token: "near:token.near", + amount: 1000000n, + recipient: "near:recipient.near", + fee: 100n, + sender: "near:sender.near", + msg: "transfer message", + emitter_address: "near:emitter.near", + }, + } + + it("should correctly serialize and deserialize InitTransfer", () => { + const serialized = borshSerialize(ProverResultSchema, initTransfer) + const deserialized = borshDeserialize(ProverResultSchema, serialized) + expect(deserialized).toEqual(initTransfer) + }) + + it("should maintain bigint precision", () => { + const largeAmount: ProverResult = { + InitTransfer: { + ...initTransfer.InitTransfer, + amount: 9007199254740991n, // Number.MAX_SAFE_INTEGER + }, + } + const serialized = borshSerialize(ProverResultSchema, largeAmount) + const deserialized = borshDeserialize(ProverResultSchema, serialized) + expect(deserialized.InitTransfer.amount).toBe(9007199254740991n) + }) + }) + + describe("FinTransfer", () => { + const finTransfer: ProverResult = { + FinTransfer: { + transfer_id: "transfer123", + fee_recipient: "fee.near", + amount: 500000n, + emitter_address: "near:emitter.near", + }, + } + + it("should correctly serialize and deserialize FinTransfer", () => { + const serialized = borshSerialize(ProverResultSchema, finTransfer) + const deserialized = borshDeserialize(ProverResultSchema, serialized) + expect(deserialized).toEqual(finTransfer) + }) + }) + + describe("DeployToken", () => { + const deployToken: ProverResult = { + DeployToken: { + token: "token.near", + token_address: "eth:0x1234567890", + emitter_address: "near:emitter.near", + }, + } + + it("should correctly serialize and deserialize DeployToken", () => { + const serialized = borshSerialize(ProverResultSchema, deployToken) + const deserialized = borshDeserialize(ProverResultSchema, serialized) + expect(deserialized).toEqual(deployToken) + }) + }) + + describe("LogMetadata", () => { + const logMetadata: ProverResult = { + LogMetadata: { + token_address: "eth:0x1234567890", + name: "Test Token", + symbol: "TEST", + decimals: 18, + emitter_address: "near:emitter.near", + }, + } + + it("should correctly serialize and deserialize LogMetadata", () => { + const serialized = borshSerialize(ProverResultSchema, logMetadata) + const deserialized = borshDeserialize(ProverResultSchema, serialized) + expect(deserialized).toEqual(logMetadata) + }) + }) + + describe("Edge Cases", () => { + it("should handle zero values", () => { + const zeroValuesMsg: ProverResult = { + InitTransfer: { + origin_nonce: 0n, + token: "near:token.near", + amount: 0n, + recipient: "near:recipient.near", + fee: 0n, + sender: "near:sender.near", + msg: "message", + emitter_address: "near:emitter.near", + }, + } + const serialized = borshSerialize(ProverResultSchema, zeroValuesMsg) + const deserialized = borshDeserialize(ProverResultSchema, serialized) + expect(deserialized).toEqual(zeroValuesMsg) + }) + }) +}) diff --git a/src/types/prover.ts b/src/types/prover.ts new file mode 100644 index 0000000..fc097e0 --- /dev/null +++ b/src/types/prover.ts @@ -0,0 +1,145 @@ +import { BorshSchema } from "borsher" + +export type AccountId = string +export type U128 = bigint +export type Nonce = bigint +export type TransferId = string +export type Fee = bigint +export type OmniAddress = + | `eth:${string}` + | `near:${string}` + | `sol:${string}` + | `arb:${string}` + | `base:${string}` + +export enum ProofKind { + InitTransfer = 0, + FinTransfer = 1, + DeployToken = 2, + LogMetadata = 3, +} + +export const ProofKindSchema = BorshSchema.Enum({ + InitTransfer: BorshSchema.Unit, + FinTransfer: BorshSchema.Unit, + DeployToken: BorshSchema.Unit, + LogMetadata: BorshSchema.Unit, +}) + +export type InitTransferMessage = { + origin_nonce: Nonce + token: OmniAddress + amount: U128 + recipient: OmniAddress + fee: Fee + sender: OmniAddress + msg: string + emitter_address: OmniAddress +} + +export const InitTransferMessageSchema = BorshSchema.Struct({ + origin_nonce: BorshSchema.u64, + token: BorshSchema.String, + amount: BorshSchema.u128, + recipient: BorshSchema.String, + fee: BorshSchema.u128, + sender: BorshSchema.String, + msg: BorshSchema.String, + emitter_address: BorshSchema.String, +}) + +export type FinTransferMessage = { + transfer_id: TransferId + fee_recipient: AccountId + amount: U128 + emitter_address: OmniAddress +} + +export const FinTransferMessageSchema = BorshSchema.Struct({ + transfer_id: BorshSchema.String, + fee_recipient: BorshSchema.String, + amount: BorshSchema.u128, + emitter_address: BorshSchema.String, +}) + +export type DeployTokenMessage = { + token: AccountId + token_address: OmniAddress + emitter_address: OmniAddress +} + +export const DeployTokenMessageSchema = BorshSchema.Struct({ + token: BorshSchema.String, + token_address: BorshSchema.String, + emitter_address: BorshSchema.String, +}) + +export type LogMetadataMessage = { + token_address: OmniAddress + name: string + symbol: string + decimals: number + emitter_address: OmniAddress +} + +export const LogMetadataMessageSchema = BorshSchema.Struct({ + token_address: BorshSchema.String, + name: BorshSchema.String, + symbol: BorshSchema.String, + decimals: BorshSchema.u8, + emitter_address: BorshSchema.String, +}) + +export type ProverResult = + | { InitTransfer: InitTransferMessage } + | { FinTransfer: FinTransferMessage } + | { DeployToken: DeployTokenMessage } + | { LogMetadata: LogMetadataMessage } + +export type InitTransferResult = Extract +export type FinTransferResult = Extract +export type DeployTokenResult = Extract +export type LogMetadataResult = Extract + +export const ProverResultSchema = BorshSchema.Enum({ + InitTransfer: InitTransferMessageSchema, + FinTransfer: FinTransferMessageSchema, + DeployToken: DeployTokenMessageSchema, + LogMetadata: LogMetadataMessageSchema, +}) + +export type EvmProof = { + log_index: bigint + log_entry_data: Uint8Array + receipt_index: bigint + receipt_data: Uint8Array + header_data: Uint8Array + proof: Uint8Array[] +} + +export const EvmProofSchema = BorshSchema.Struct({ + log_index: BorshSchema.u64, + log_entry_data: BorshSchema.Vec(BorshSchema.u8), + receipt_index: BorshSchema.u64, + receipt_data: BorshSchema.Vec(BorshSchema.u8), + header_data: BorshSchema.Vec(BorshSchema.u8), + proof: BorshSchema.Vec(BorshSchema.Vec(BorshSchema.u8)), +}) + +export type EvmVerifyProofArgs = { + proof_kind: ProofKind + proof: EvmProof +} +export const EvmVerifyProofArgsSchema = BorshSchema.Struct({ + proof_kind: ProofKindSchema, + proof: EvmProofSchema, // assuming EvmProofSchema is defined as before +}) + +export type WormholeVerifyProofArgs = { + proof_kind: ProofKind + vaa: string +} +export const WormholeVerifyProofArgsSchema = BorshSchema.Struct({ + proof_kind: ProofKindSchema, + vaa: BorshSchema.String, +})