diff --git a/.gitignore b/.gitignore index a70dbf7d..76cdfb05 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ v8-compile-cache-0 /packages/crypto-target-*/wasm-target /packages/core/crypto/wasm-target **/*_generated_.ts +/packages/core/data-model/schema/schema.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..51d9ec0f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing to Iroha JavaScript + +This document explains how this repo works, and how to work with it. + +Steps, in short: + +1. Install Rust and `wasm-pack`. +2. Install Deno v2 and Node.js v22 +3. Link Iroha repository +4. Explore `tasks` in the root `deno.json` +5. Work with the code + +TODO: setting up commit hooks + +## Installing Rust and `wasm-pack` + +```sh +# install rustup +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# add necessary components +rustup component add rust-src --target wasm32-unknown-unknown + +# install wasm-pack +curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +``` + +## Installing Deno & Node.js + +This must be pretty straightforward. A few notes though: + +- While this project is mostly driven by Deno, there are some Node.js parts, unfortunately. Specifically, + `tests/browser` is a `pnpm` project, because otherwise it's hard to make Cypress work. +- Thus, run `corepack enable` (or install `npm i -g pnpm`) so that `pnpm` is also available. + +## Linking & building Iroha + +For this project to function, you must link Iroha repository. There are two ways to do so: + +```sh +# symlink to the local path +deno task prep:iroha --path /path/to/local/iroha/clone +``` + +```sh +# clone the repo +deno task prep:iroha --git https://github.com/hyperledger-iroha/iroha.git --rev v2.0.0-rc.1.0 +``` + +After Iroha is linked, you need to prepare some artifacts from it (binaries such as `irohad`, `kagami`, `iroha_codec`; +`executor.wasm`; `schema.json`): + +```sh +deno task prep:iroha:build +``` + +## Running tests + +Please explore `tasks` in the root `deno.jsonc`. + +```sh +# run them all +deno task test + +# unit, non-integration tests +deno run npm:vitest +# or +pnpm dlx vitest +``` + +Tests are mostly written in Vitest (except the doctests), and it doesn't work very well with Deno (considering +migration). To improve development experience, consider running Vitest via `pnpm dlx` or similar. diff --git a/README.md b/README.md index 1a8d73b9..e28ef5d6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,54 @@ # Iroha JavaScript -This is a JavaScript (TypeScript) SDK for Iroha 2. +The JavaScript (TypeScript) SDK of [Iroha 2](https://github.com/hyperledger-iroha/iroha). -TODO +Works in Deno, Node.js, and the Browser. (TODO: check in Bun and Cloudflare Workers). + +Packages and documentation are available on JSR: https://jsr.io/@iroha + +## Usage + +### Installation + +```shell +# deno +deno add jsr:@iroha/core + +# npm (one of the below, depending on your package manager) +npx jsr add @iroha/core +yarn dlx jsr add @iroha/core +pnpm dlx jsr add @iroha/core +bunx jsr add @iroha/core +``` + +### Quick Example + +```ts +import '@iroha/crypto-target-node/install' +import { Client } from '@iroha/client' +import * as types from '@iroha/core/data-model' + +const kp = types.KeyPair.random() + +const client = new Client({ + toriiBaseURL: new URL('http://localhost:8080'), + chain: '000-000', + accountDomain: new types.Name('wonderland'), + accountKeyPair: kp, +}) + +async function test() { + const { blocks } = await client.api.telemetry.status() + console.log(blocks) // => 3 +} +``` + +This example assumes running in Deno/Node.js. + +## Iroha Compatibility + +See the ["Compatibility" secion in `@iroha/core` package documentation](https://jsr.io/@iroha/core#iroha-compatibility). + +## Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/deno.jsonc b/deno.jsonc index 1fefa3c1..f34033a3 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,10 +1,13 @@ { + "license": "Apache 2.0", "nodeModulesDir": "auto", + "exclude": ["**/docs"], "workspace": [ "./packages/core", "./packages/crypto-target-node", "./packages/crypto-target-web", "./packages/client", + "./packages/client-web-socket-node", "./tests/support/test-configuration", "./tests/support/test-peer", @@ -24,15 +27,19 @@ }, "prep:crypto-wasm": "deno run --allow-read --allow-env --allow-run --allow-write ./etc/task-prep-crypto-wasm.ts", "prep:crypto-wasm:copy": "deno task prep:crypto-wasm --onlyCopy", - "prep:codegen": "deno run --allow-write --allow-read ./etc/task-codegen.ts", + "prep:codegen": { + "command": "deno run --allow-write --allow-read ./etc/task-codegen.ts", + "dependencies": ["prep:iroha:check"] + }, "prep:codegen:watch": "watchexec -e ts deno task prep:codegen", "prep:ensure-ready": { "dependencies": ["prep:codegen", "prep:crypto-wasm:copy", "prep:iroha:check"] }, "check:all": { - "command": "deno check .", + "command": "deno check --doc .", "dependencies": ["prep:ensure-ready"] }, + "test:deno": "deno test --doc --allow-read", "test:vitest": { "dependencies": ["prep:ensure-ready"], "description": "Run Vitest", @@ -48,7 +55,7 @@ }, "test": { "description": "Run all tests, from unit to integration", - "command": "deno task test:vitest && deno task test:integration:node && deno task test:integration:browser" + "command": "deno task test:deno && deno task test:vitest && deno task test:integration:node && deno task test:integration:browser" }, "dev:run-test-peer": { "dependencies": ["prep:ensure-ready"], @@ -98,7 +105,10 @@ "publish": { "exclude": [ "!**/*_generated_.ts", - "!packages/crypto-target-*/wasm-target" + "!packages/crypto-target-*/wasm-target", + "!packages/core/crypto/wasm-target", + "!packages/core/data-model/schema/schema.json" ] - } + }, + "test": { "exclude": ["prep", "crypto-wasm"] } } diff --git a/deno.lock b/deno.lock index 1c8d3912..36fc64c4 100644 --- a/deno.lock +++ b/deno.lock @@ -2942,9 +2942,12 @@ "members": { "packages/client": { "dependencies": [ - "npm:debug@^4.4.0", "npm:emittery@^1.1.0", - "npm:p-defer@^4.0.1", + "npm:p-defer@^4.0.1" + ] + }, + "packages/client-web-socket-node": { + "dependencies": [ "npm:ws@^8.18.0" ] }, diff --git a/etc/task-codegen.ts b/etc/task-codegen.ts index f37c0bd9..09b51a9f 100644 --- a/etc/task-codegen.ts +++ b/etc/task-codegen.ts @@ -11,19 +11,20 @@ const tsFormatter = dprint.createFromBuffer(await Deno.readFile(dprintTS.getPath const formatTS = (code: string) => tsFormatter.formatText({ filePath: 'file.ts', fileText: code }) async function write({ file, code }: { file: string; code: string }) { + let status: string try { const prevCode = await Deno.readTextFile(file) if (prevCode === code) { - $.logStep(`Skipping ${colors.cyan(file)}, no change`) - return + status = 'unchanged' } else { await Deno.writeTextFile(file, code) - $.logStep(`Re-written ${colors.cyan(file)}`) + status = 'updated' } } catch { await Deno.writeTextFile(file, code) - $.logStep(`Written ${colors.cyan(file)}`) + status = 'created' } + $.logStep(`Generated ${colors.cyan(file)} (${status})`) } /** diff --git a/etc/task-prep-iroha.ts b/etc/task-prep-iroha.ts index 8527bb8c..2f09be28 100644 --- a/etc/task-prep-iroha.ts +++ b/etc/task-prep-iroha.ts @@ -82,6 +82,21 @@ async function copyArtifacts() { $.logStep(`Finished copying artifacts to ${colors.bold(colors.cyan(PREP_OUTPUT_DIR))}`) } +async function copySchemaJson() { + const dest = resolveFromRoot('packages/core/data-model/schema/schema.json') + + try { + await Deno.remove(dest) + } catch (err) { + if (!(err instanceof Deno.errors.NotFound)) { + throw err + } + } + + await copy(path.join(PREP_OUTPUT_DIR, 'schema.json'), dest) + $.logStep('Copied', dest) +} + const args = parseArgs(Deno.args, { string: ['git', 'git-rev', 'path'], boolean: ['check', 'build'], @@ -100,6 +115,7 @@ await match(args) .with({ check: true }, async () => { if (await dirExists(PREP_OUTPUT_DIR)) { $.logStep(`Checked that ${colors.cyan(PREP_OUTPUT_DIR)} exists`) + await copySchemaJson() } else { $.logError( `Error: ${PREP_OUTPUT_DIR} doesn't exist. Make sure to run ${ diff --git a/etc/task-publish.ts b/etc/task-publish.ts index 69c9c5aa..41451a87 100644 --- a/etc/task-publish.ts +++ b/etc/task-publish.ts @@ -6,6 +6,7 @@ const PATHS = [ 'packages/crypto-target-node', 'packages/crypto-target-web', 'packages/client', + 'packages/client-web-socket-node', ].map( (x) => resolveFromRoot(x), ) diff --git a/packages/client-web-socket-node/deno.jsonc b/packages/client-web-socket-node/deno.jsonc new file mode 100644 index 00000000..2981dcd3 --- /dev/null +++ b/packages/client-web-socket-node/deno.jsonc @@ -0,0 +1,8 @@ +{ + "name": "@iroha/client-web-socket-node", + "version": "0.1.0", + "exports": "./mod.ts", + "imports": { + "ws": "npm:ws@^8.18.0" + } +} diff --git a/packages/client/web-socket/node.ts b/packages/client-web-socket-node/mod.ts similarity index 63% rename from packages/client/web-socket/node.ts rename to packages/client-web-socket-node/mod.ts index 24ef2bb8..8333e2f2 100644 --- a/packages/client/web-socket/node.ts +++ b/packages/client-web-socket-node/mod.ts @@ -1,4 +1,18 @@ -import type { IncomingData, IsomorphicWebSocketAdapter } from './types.ts' +/** + * WebSocket adapter for Node.js environment. Build on top of `npm:ws`. + * + * @example + * ```ts + * import ws from '@iroha/client-web-socket-node' + * import { WebSocketAPI } from '@iroha/client' + * + * new WebSocketAPI(new URL('http://localhost:8080'), ws) + * ``` + * + * @module + */ + +import type { IncomingData, IsomorphicWebSocketAdapter } from '@iroha/client/web-socket' import WebSocket from 'ws' import { Buffer } from 'node:buffer' @@ -13,7 +27,10 @@ function handleIncomingData( throw new Error('Unable to parse incoming data') } -export const adapter: IsomorphicWebSocketAdapter = { +/** + * The WebSocket adapter. + */ +const adapter: IsomorphicWebSocketAdapter = { initWebSocket: (params) => { const socket = new WebSocket(params.url) @@ -33,3 +50,5 @@ export const adapter: IsomorphicWebSocketAdapter = { } }, } + +export default adapter diff --git a/packages/client/api-ws.ts b/packages/client/api-ws.ts index 8b323a1a..056f0c3f 100644 --- a/packages/client/api-ws.ts +++ b/packages/client/api-ws.ts @@ -1,22 +1,18 @@ import type Emittery from 'emittery' -import Debug from 'debug' import * as dm from '@iroha/core/data-model' import { getCodec } from '@iroha/core' import { ENDPOINT_BLOCKS_STREAM, ENDPOINT_EVENTS } from './const.ts' import type { SocketEmitMapBase } from './util.ts' import { setupWebSocket } from './util.ts' -import type { IsomorphicWebSocketAdapter } from './web-socket/types.ts' - -const debugBlocksStream = Debug('@iroha/client:blocks-stream') -const debugEvents = Debug('@iroha/client:events') +import { type IsomorphicWebSocketAdapter, nativeWS } from './web-socket/mod.ts' export class WebSocketAPI { public readonly toriiBaseURL: URL public readonly adapter: IsomorphicWebSocketAdapter - public constructor(toriiBaseURL: URL, adapter: IsomorphicWebSocketAdapter) { + public constructor(toriiBaseURL: URL, adapter?: IsomorphicWebSocketAdapter) { this.toriiBaseURL = toriiBaseURL - this.adapter = adapter + this.adapter = adapter ?? nativeWS } public async blocksStream(params?: SetupBlocksStreamParams): Promise { @@ -29,7 +25,6 @@ export class WebSocketAPI { } = setupWebSocket({ baseURL: this.toriiBaseURL, endpoint: ENDPOINT_BLOCKS_STREAM, - parentDebugger: debugBlocksStream, adapter: this.adapter, }) @@ -67,7 +62,6 @@ export class WebSocketAPI { } = setupWebSocket({ baseURL: this.toriiBaseURL, endpoint: ENDPOINT_EVENTS, - parentDebugger: debugEvents, adapter: this.adapter, }) diff --git a/packages/client/api.ts b/packages/client/api.ts index b24de8f6..4cff3649 100644 --- a/packages/client/api.ts +++ b/packages/client/api.ts @@ -64,7 +64,7 @@ export class HttpTransport { /** * @param toriiBaseURL URL of Torii (Iroha API Gateway) - * @param fetch `fetch` implementation for environments where it is not available globally. + * @param fetch `fetch` implementation for environments where it is not available natively. * For example, you might need to use `node-fetch` or `undici` in older versions of Node.js. */ public constructor(toriiBaseURL: URL, fetch?: Fetch) { diff --git a/packages/client/client.ts b/packages/client/client.ts index 4165f20e..2286c11f 100644 --- a/packages/client/client.ts +++ b/packages/client/client.ts @@ -1,16 +1,5 @@ -/** - * @module @iroha2/client - */ - -/** - * @packageDocumentation - * - * Client library to interact with Iroha v2 Peer. Library implements Transactions, Queries, - * Events, Status & Health check. - */ - import type { KeyPair, PrivateKey } from '@iroha/core/crypto' -import * as dm from '@iroha/core/data-model' +import * as types from '@iroha/core/data-model' import type { Except } from 'type-fest' import defer from 'p-defer' import { buildTransactionPayload, signTransaction, transactionHash, type TransactionPayloadParams } from '@iroha/core' @@ -23,16 +12,27 @@ import { type SetupEventsReturn, WebSocketAPI, } from './api-ws.ts' -import type { IsomorphicWebSocketAdapter } from './web-socket/types.ts' +import type { IsomorphicWebSocketAdapter } from './web-socket/mod.ts' import { FindAPI } from './find-api._generated_.ts' import { QueryExecutor } from './query.ts' +export { FindAPI } + export interface CreateClientParams { + /** + * Custom {@linkcode fetch} for environments where it is not available natively. + */ fetch?: Fetch - ws: IsomorphicWebSocketAdapter + /** + * WebSocket adapter. For environments where {@linkcode WebSocket} is not available natively. + */ + ws?: IsomorphicWebSocketAdapter + /** + * The base URL of **Torii**, Iroha API Gateway. + */ toriiBaseURL: URL chain: string - accountDomain: dm.DomainId + accountDomain: types.DomainId accountKeyPair: KeyPair } @@ -46,9 +46,9 @@ export interface SubmitParams { } export class TransactionRejectedError extends Error { - public reason: dm.TransactionRejectionReason + public reason: types.TransactionRejectionReason - public constructor(reason: dm.TransactionRejectionReason) { + public constructor(reason: types.TransactionRejectionReason) { super() this.name = 'TransactionRejectedError' this.reason = reason @@ -66,11 +66,11 @@ export class Client { public readonly params: CreateClientParams /** - * Raw API calls. + * Lower-level API calls. */ public readonly api: MainAPI /** - * Raw WebSocket API calls. + * Lower-level WebSocket API calls. */ public readonly socket: WebSocketAPI /** @@ -89,8 +89,8 @@ export class Client { this.socket = new WebSocketAPI(params.toriiBaseURL, params.ws) } - public authority(): dm.AccountId { - return new dm.AccountId( + public authority(): types.AccountId { + return new types.AccountId( this.params.accountKeyPair.publicKey(), this.params.accountDomain, ) @@ -100,8 +100,15 @@ export class Client { return this.params.accountKeyPair.privateKey() } + /** + * Create a transaction. + * + * @param executable the executable of the transactions + * @param params parameters to adjust the constructed transaction payload + * @returns the handle to perform further operations, such as computing transaction's hash or submitting it to Iroha. + */ public transaction( - executable: dm.Executable, + executable: types.Executable, params?: Except, ): TransactionHandle { const tx = signTransaction( @@ -116,10 +123,16 @@ export class Client { return new TransactionHandle(tx, this) } + /** + * Receive events from Iroha in real time. + */ public async events(params?: SetupEventsParams): Promise { return this.socket.events(params) } + /** + * Receive blocks from Iroha in real time. + */ public async blocks(params?: SetupBlocksStreamParams): Promise { return this.socket.blocksStream(params) } @@ -127,16 +140,16 @@ export class Client { export class TransactionHandle { private readonly client: Client - private readonly tx: dm.SignedTransaction - private readonly txHash: dm.Hash + private readonly tx: types.SignedTransaction + private readonly txHash: types.Hash - public constructor(tx: dm.SignedTransaction, client: Client) { + public constructor(tx: types.SignedTransaction, client: Client) { this.client = client this.tx = tx this.txHash = transactionHash(tx) } - public get hash(): dm.Hash { + public get hash(): types.Hash { return this.txHash } @@ -145,7 +158,7 @@ export class TransactionHandle { // const hash = transactionHash(tx) const stream = await this.client.events({ filters: [ - dm.EventFilterBox.Pipeline.Transaction({ + types.EventFilterBox.Pipeline.Transaction({ hash: this.txHash, blockHeight: null, // TODO: include "status" when Iroha API is fixed about it diff --git a/packages/client/deno.jsonc b/packages/client/deno.jsonc index b23dd202..a8d95227 100644 --- a/packages/client/deno.jsonc +++ b/packages/client/deno.jsonc @@ -1,15 +1,13 @@ { "name": "@iroha/client", - "version": "0.0.0", + "version": "0.1.0", "exports": { ".": "./mod.ts", - "./web-socket/native": "./web-socket/native.ts", - "./web-socket/node": "./web-socket/node.ts" + "./web-socket": "./web-socket/mod.ts" }, "imports": { - "debug": "npm:debug@^4.4.0", + // TODO: remove these deps "emittery": "npm:emittery@^1.1.0", - "p-defer": "npm:p-defer@^4.0.1", - "ws": "npm:ws@^8.18.0" + "p-defer": "npm:p-defer@^4.0.1" } } diff --git a/packages/client/mod.ts b/packages/client/mod.ts index 0cf5fa5f..ec056c06 100644 --- a/packages/client/mod.ts +++ b/packages/client/mod.ts @@ -1,3 +1,105 @@ +/** + * Utilities to interact with Iroha via HTTP and WebSocket APIs. + * + * The primary functionality is exposed via the {@linkcode Client}. + * + * @example Constructing the client + * ```ts + * import '@iroha/crypto-target-node/install' + * import ws from '@iroha/client-web-socket-node' + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * const kp = types.KeyPair.random() + * + * const client = new Client({ + * toriiBaseURL: new URL('http://localhost:8080'), + * chain: '000-000', + * accountDomain: new types.Name('wonderland'), + * accountKeyPair: kp, + * // This is necessary in Node.js, which doesn't support WebSocket API natively + * // Remove this if you are using Deno/Browser + * ws + * }) + * ``` + * + * @example Querying data + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * async function test(client: Client) { + * // fetch all assets + * const assets: types.Asset[] = await client.find.assets().executeAll(); + * + * // find all accounts in a domain ending with `land` + * const accounts: types.Account[] = await client.find + * .accounts({ + * predicate: types.CompoundPredicate.Atom( + * types.AccountProjectionPredicate.Id.Domain.Name.Atom.EndsWith('land') + * ) + * }) + * .executeAll() + * + * // use selectors and pagination + * const items: [types.Hash, types.AccountId][] = await client.find + * .transactions({ + * selector: [ + * types.CommittedTransactionProjectionSelector.BlockHash.Atom, + * types.CommittedTransactionProjectionSelector.Value.Authority.Atom, + * ], + * offset: 10, + * limit: new types.NonZero(50), + * }) + * .executeAll() + * } + * ``` + * + * Note that resulting types are inferred based on the selectors you pass. + * + * @example Submitting a transaction + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * async function test(client: Client) { + * const txHandle = client.transaction( + * types.Executable.Instructions([ + * types.InstructionBox.Register.Domain({ + * id: new types.Name('test'), + * logo: null, + * metadata: [], + * }), + * ]), + * ) + * + * // could be used to watch for events + * const hash: types.Hash = txHandle.hash; + * + * // submit and wait until the transaction is committed + * await txHandle.submit({ verify: true }) + * } + * ``` + * + * @example Using lower-level API utilitites + * ```ts + * import { MainAPI, HttpTransport } from '@iroha/client' + * import { assertEquals } from '@std/assert/equals' + * + * const toriiURL = new URL('http://localhost:8080') + * const transport = new HttpTransport(toriiURL) + * const api = new MainAPI(transport) + * + * async function test() { + * const result = await api.health() + * + * assertEquals(result.kind, 'ok') + * } + * ``` + * + * @module + */ + export * from './api.ts' export * from './api-ws.ts' export * from './query.ts' diff --git a/packages/client/util.ts b/packages/client/util.ts index 4988f675..3da21ebc 100644 --- a/packages/client/util.ts +++ b/packages/client/util.ts @@ -1,5 +1,5 @@ import type { CloseEvent, Event as WsEvent, IsomorphicWebSocketAdapter, SendData } from './web-socket/types.ts' -import type { Debugger } from 'debug' +// import type { Debugger } from 'debug' import Emittery from 'emittery' export function transformProtocolInUrlFromHttpToWs(url: URL): URL { @@ -36,7 +36,7 @@ export interface SocketEmitMapBase { export function setupWebSocket(params: { baseURL: URL endpoint: string - parentDebugger: Debugger + // parentDebugger: Debugger adapter: IsomorphicWebSocketAdapter }): { ee: Emittery @@ -45,38 +45,38 @@ export function setupWebSocket(params: { close: () => Promise accepted: () => Promise } { - const debug = params.parentDebugger.extend('websocket') + // const debug = params.parentDebugger.extend('websocket') const url = urlJoinPath(transformProtocolInUrlFromHttpToWs(params.baseURL), params.endpoint) const ee = new Emittery() const onceOpened = ee.once('open') const onceClosed = ee.once('close') - debug('opening connection to %o', url) + // debug('opening connection to %o', url) const { isClosed, send, close } = params.adapter.initWebSocket({ url, onopen: (e) => { - debug('connection opened') + // debug('connection opened') ee.emit('open', e) }, onclose: (e) => { - debug('connection closed; code: %o, reason: %o, was clean: %o', e.code, e.reason, e.wasClean) + // debug('connection closed; code: %o, reason: %o, was clean: %o', e.code, e.reason, e.wasClean) ee.emit('close', e) }, onerror: (e) => { - debug('connection error %o', e) + // debug('connection error %o', e) ee.emit('error', e) }, onmessage: ({ data }) => { - debug('message', data) + // debug('message', data) ee.emit('message', data) }, }) async function closeAsync() { if (isClosed()) return - debug('closing connection...') + // debug('closing connection...') close() return ee.once('close').then(() => {}) } diff --git a/packages/client/web-socket/mod.ts b/packages/client/web-socket/mod.ts new file mode 100644 index 00000000..25780652 --- /dev/null +++ b/packages/client/web-socket/mod.ts @@ -0,0 +1,2 @@ +export * from './types.ts' +export { default as nativeWS } from './native.ts' diff --git a/packages/client/web-socket/native.ts b/packages/client/web-socket/native.ts index 33984107..5606e837 100644 --- a/packages/client/web-socket/native.ts +++ b/packages/client/web-socket/native.ts @@ -10,7 +10,7 @@ async function handleIncomingData(data: any): Promise { throw new Error('Unable to parse incoming data') } -export const adapter: IsomorphicWebSocketAdapter = { +const adapter: IsomorphicWebSocketAdapter = { initWebSocket: (params) => { const socket = new WebSocket(params.url) @@ -28,3 +28,5 @@ export const adapter: IsomorphicWebSocketAdapter = { } }, } + +export default adapter diff --git a/packages/core/codec.spec.ts b/packages/core/codec.spec.ts new file mode 100644 index 00000000..12ef91ae --- /dev/null +++ b/packages/core/codec.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from 'vitest' +import { enumCodec } from './codec.ts' + +describe('EnumCodec', () => { + test('.discriminated(): when decodes a unit variant, does produce only `kind`, not `value`', () => { + const codec = enumCodec<{ Test: [] }>({ Test: [0] }).discriminated() + + const decoded = codec.decode(codec.encode({ kind: 'Test' })) + + expect(decoded).toEqual({ kind: 'Test' }) + expect('value' in decoded).toBe(false) + }) +}) diff --git a/packages/core/codec.ts b/packages/core/codec.ts index 7357c9e8..555285bc 100644 --- a/packages/core/codec.ts +++ b/packages/core/codec.ts @@ -1,7 +1,40 @@ +/** + * [SCALE](https://docs.polkadot.com/polkadot-protocol/basics/data-encoding) codec utilities. + * + * These are mostly used internally, but you can use it in case you need to extend codec functionality. + * + * This module is mostly based on the [`@scale-codec/core`](https://www.npmjs.com/package/@scale-codec/core) package. + * + * @module + */ + import * as scale from '@scale-codec/core' import { decodeHex } from '@std/encoding' import type { Variant, VariantUnit } from './util.ts' +export const SYMBOL_CODEC = '$codec' + +/** + * Extracts codec from its container. + */ +export function getCodec(type: CodecContainer): GenCodec { + return type[SYMBOL_CODEC] +} + +/** + * Wraps a codec into {@link CodecContainer}. + */ +export function defineCodec(codec: GenCodec): CodecContainer { + return { [SYMBOL_CODEC]: codec } +} + +/** + * A value that contains a codec under a "special" key ({@link SYMBOL_CODEC}). + */ +export interface CodecContainer { + [SYMBOL_CODEC]: GenCodec +} + export interface RawScaleCodec { encode: scale.Encode decode: scale.Decode @@ -70,7 +103,7 @@ export class EnumCodec extends GenCodec(value.kind, value.value) return scale.variant(value.kind) }, - fromBase: (value) => ({ kind: value.tag, value: value.content }), + fromBase: (value) => (value.unit ? { kind: value.tag } : { kind: value.tag, value: value.content }), }) as any } diff --git a/packages/core/crypto/mod.ts b/packages/core/crypto/mod.ts index db0cf5dc..832e6f9a 100644 --- a/packages/core/crypto/mod.ts +++ b/packages/core/crypto/mod.ts @@ -1,6 +1,53 @@ /** - * @module @iroha/crypto + * Port of `iroha_crypto` Rust crate via WebAssembly. + * + * ## Installing the WebAssembly + * + * In order for this module (and its dependants) to operate properly, the WASM must be set via {@linkcode setWASM}. + * This module itself doesn't contain the WASMs, as they are dependant on the target platform. Instead, they are shipped + * as separate packages: + * + * - `@iroha/crypto-target-node` - for Node.js and Deno. + * - `@iroha/crypto-target-web` - for native browser ESModule environment, e.g. it will work out of the box with Vite. + * + * The shortest way to install a target is by the following: + * + * ```ts + * // In Node.js/Deno + * import '@iroha/crypto-target-node/install' + * ``` + * + * ```ts + * // In Browser + * import '@iroha/crypto-target-web/install' + * ``` + * + * Please consult to the relevant packages documentation for more details. + * + * @example Deriving a KeyPair from seed + * ```ts + * import '@iroha/crypto-target-node/install' + * import { Bytes } from '@iroha/core/crypto' + * import { assertEquals } from '@std/assert/equals' + * + * const kp = KeyPair.deriveFromSeed(Bytes.hex('001122')) + * + * assertEquals(kp.privateKey().multihash(), '8026205720A4B3BFFA5C9BBD83D09C88CD1DB08CA3F0C302EC4C8C37A26BD734C37616') + * ``` + * + * @example Constructing a private key from a multihash + * ```ts + * import '@iroha/crypto-target-node/install' + * import { assertEquals } from '@std/assert/equals' + * + * const pk = PrivateKey.fromMultihash('8026205720A4B3BFFA5C9BBD83D09C88CD1DB08CA3F0C302EC4C8C37A26BD734C37616') + * + * assertEquals(pk.algorithm, 'ed25519') + * ``` + * + * @module */ + export * from './types.ts' export * from './singleton.ts' export * from './util.ts' @@ -8,7 +55,8 @@ export * from './util.ts' import { Bytes } from './util.ts' import { getWASM } from './singleton.ts' import type { wasmPkg } from './types.ts' -import { type CodecContainer, defineCodec, getCodec, type Ord, ordCompare, SYMBOL_CODEC } from '../traits.ts' +import { type CodecContainer, defineCodec, getCodec, SYMBOL_CODEC } from '../codec.ts' +import { type Ord, ordCompare } from '../traits.ts' import { enumCodec, GenCodec, structCodec } from '../codec.ts' import * as scale from '@scale-codec/core' import { assert } from '@std/assert/assert' @@ -50,10 +98,6 @@ export interface HasAlgorithm { export interface HasPayload { readonly payload: Bytes - // readonly payload: { - // (): Uint8Array - // (kind: 'hex'): string - // } } const HASH_ARR_LEN = 32 @@ -233,7 +277,7 @@ export class KeyPair implements HasAlgorithm { } /** - * Derive the key pair from a given seed. + * Derive the key pair from the given seed. * @param seed some binary data. * @param options key generation options */ diff --git a/packages/core/data-model/compound.ts b/packages/core/data-model/compound.ts index 3da7ff98..f28bb0f1 100644 --- a/packages/core/data-model/compound.ts +++ b/packages/core/data-model/compound.ts @@ -1,7 +1,7 @@ -import * as crypto from '@iroha/core/crypto' +import * as crypto from '../crypto/mod.ts' import type { JsonValue } from 'type-fest' -import { enumCodec, type GenCodec, lazyCodec, structCodec } from '../codec.ts' -import { getCodec, type IsZero, type Ord, ordCompare, SYMBOL_CODEC } from '../traits.ts' +import { enumCodec, type GenCodec, getCodec, lazyCodec, structCodec, SYMBOL_CODEC } from '../codec.ts' +import { type IsZero, type Ord, ordCompare } from '../traits.ts' import type { Variant } from '../util.ts' import { String, U64, Vec } from './primitives.ts' @@ -180,291 +180,18 @@ export const CompoundPredicate: { }, } -// // Crypto specials -// export type Algorithm = VariantUnit - -// export const Algorithm: { [K in crypto.Algorithm]: VariantUnit } & CodecContainer = { -// ed25519: Object.freeze({ kind: 'ed25519' }), -// secp256k1: Object.freeze({ kind: 'secp256k1' }), -// bls_normal: Object.freeze({ kind: 'bls_normal' }), -// bls_small: Object.freeze({ kind: 'bls_small' }), -// ...defineCodec( -// enumCodec<{ -// ed25519: [] -// secp256k1: [] -// bls_normal: [] -// bls_small: [] -// }>({ -// ed25519: [0], -// secp256k1: [1], -// bls_normal: [2], -// bls_small: [3], -// }).discriminated() satisfies GenCodec, -// ), -// } - -// const HASH_ARR_LEN = 32 - -// export class HashRepr { -// public static [SYMBOL_CODEC]: GenCodec = new GenCodec({ -// encode: scale.createUint8ArrayEncoder(HASH_ARR_LEN), -// decode: scale.createUint8ArrayDecoder(HASH_ARR_LEN), -// }).wrap({ -// fromBase: (lower) => HashRepr.fromRaw(lower), -// toBase: (higher) => higher.asRaw(), -// }) - -// public static fromHex(hex: string): HashRepr { -// return new HashRepr(hex, null) -// } - -// public static fromRaw(raw: Uint8Array): HashRepr { -// return new HashRepr(null, raw) -// } - -// public static fromCrypto(hash: crypto.Hash): HashRepr { -// return new HashRepr(null, hash.payload()) -// } - -// private _hex: string | null = null -// private _raw: null | Uint8Array = null - -// private constructor(hex: null | string, raw: null | Uint8Array) { -// this._hex = hex -// this._raw = raw -// } - -// public asRaw(): Uint8Array { -// if (!this._raw) { -// this._raw = decodeHex(this._hex!) -// } -// return this._raw! -// } - -// public asHex(): string { -// if (!this._hex) { -// this._hex = encodeHex(this._raw!) -// } -// return this._hex -// } - -// public toJSON(): string { -// return this.toString() -// } -// } - -// interface PubKeyObj { -// algorithm: Algorithm -// payload: BytesVec -// } - -// /** -// * {@link crypto.PublicKey} representation in the data model. -// * -// * It could be created from any representation and transformed into another. -// * -// * Note that transformations are lazy, thus the validity of input data is not immediately validated -// * (unless {@link crypto.PublicKey} is passed). -// */ -// export class PublicKeyRepr implements Ord { -// public static [SYMBOL_CODEC]: GenCodec = structCodec(['algorithm', 'payload'], { -// algorithm: getCodec(Algorithm), -// payload: getCodec(BytesVec), -// }).wrap({ -// toBase: (higher) => higher, -// fromBase: (x) => new PublicKeyRepr(x, null, null), -// }) - -// /** -// * Create from hex-encoded bytes. -// * -// * Throws if the input is not a valid hex or not a valid public key. -// */ -// public static fromHex(hex: string): PublicKeyRepr { -// try { -// const checked = crypto.PublicKey.fromMultihash(hex) -// return new PublicKeyRepr(null, hex, checked) -// } catch (err) { -// throw new SyntaxError(`Cannot parse PublicKey from "${hex}": ${globalThis.String(err)}`) -// } -// } - -// /** -// * Create from an instance of {@link crypto.PublicKey} -// */ -// public static fromCrypto(pubkey: crypto.PublicKey): PublicKeyRepr { -// return new PublicKeyRepr(null, null, pubkey) -// } - -// /** -// * Create from an algorithm and payload. -// * -// * Throws if the algorithm and payload don't form a valid public key. -// */ -// public static fromParts(algorithm: Algorithm, payload: BytesVec): PublicKeyRepr { -// const asCrypto = crypto.PublicKey.fromBytes(algorithm.kind, crypto.Bytes.array(payload)) -// return new PublicKeyRepr({ algorithm, payload }, null, asCrypto) -// } - -// private _obj: null | PubKeyObj -// private _hex: null | string = null -// private _crypto: null | crypto.PublicKey = null - -// private constructor(obj: null | PubKeyObj, hex: null | string, crypto: null | crypto.PublicKey) { -// this._obj = obj -// this._hex = hex -// this._crypto = crypto -// } - -// public get algorithm(): Algorithm { -// return this.getOrCreateObj().algorithm -// } - -// public get payload(): BytesVec { -// return this.getOrCreateObj().payload -// } - -// /** -// * Get as {@link crypto.PublicKey} -// */ -// public asCrypto(): crypto.PublicKey { -// if (!this._crypto) { -// if (this._hex) this._crypto = crypto.PublicKey.fromMultihash(this._hex) -// else this._crypto = crypto.PublicKey.fromBytes(this._obj!.algorithm.kind, crypto.Bytes.array(this._obj!.payload)) -// } -// return this._crypto -// } - -// /** -// * Get as a public key multihash, i.e. by {@link crypto.PublicKey.toMultihash} -// */ -// public asHex(): string { -// if (!this._hex) { -// if (!this._crypto) { -// this._crypto = crypto.PublicKey.fromBytes(this._obj!.algorithm.kind, crypto.Bytes.array(this._obj!.payload)) -// } -// this._hex = this._crypto.toMultihash() -// } -// return this._hex -// } - -// public toJSON(): string { -// return this.asHex() -// } - -// public compare(other: PublicKeyRepr): number { -// return ordCompare(this.asHex(), other.asHex()) -// } - -// private getOrCreateObj(): PubKeyObj { -// if (!this._obj) { -// if (!this._crypto) this._crypto = crypto.PublicKey.fromMultihash(this._hex!) -// this._obj = { algorithm: { kind: this._crypto.algorithm }, payload: this._crypto.payload() } -// } -// return this._obj -// } -// } - -// /** -// * {@link crypto.Signature} representation in the data model. -// * -// * It could be created from any representation and transformed into another: -// * -// * ```ts -// * const hex = '01019292afafaaff' // some hex, not real -// * -// * const wrap = SignatureWrap.fromHex(hex) -// * const actualSignature = wrap.asCrypto() -// * ``` -// */ -// export class SignatureRepr { -// public static [SYMBOL_CODEC]: GenCodec = getCodec(BytesVec).wrap({ -// toBase: (higher) => higher.asRaw(), -// fromBase: (lower) => SignatureRepr.fromRaw(lower), -// }) - -// /** -// * Create from a hex string. -// * -// * Throws if input is not a valid hex. -// */ -// public static fromHex(hex: string): SignatureRepr { -// const raw = decodeHex(hex) -// return new SignatureRepr(hex, raw, null) -// } - -// /** -// * Create from an instance of {@link crypto.Signature}. -// */ -// public static fromCrypto(signature: crypto.Signature): SignatureRepr { -// return new SignatureRepr(null, null, signature) -// } - -// /** -// * Create from an array of bytes. -// */ -// private static fromRaw(bytes: Uint8Array): SignatureRepr { -// return new SignatureRepr(null, bytes, null) -// } - -// private _hex: null | string -// private _raw: null | Uint8Array -// private _crypto: null | crypto.Signature - -// private constructor(hex: null | string, raw: null | Uint8Array, crypto: null | crypto.Signature) { -// this._hex = hex -// this._raw = raw -// this._crypto = crypto -// } - -// /** -// * Representation as {@link crypto.Signature}. -// */ -// public asCrypto(): crypto.Signature { -// if (!this._crypto) { -// if (this._raw) this._crypto = crypto.Signature.fromRaw(crypto.Bytes.array(this._raw)) -// else this._crypto = crypto.Signature.fromRaw(crypto.Bytes.hex(this._hex!)) -// } -// return this._crypto -// } - -// /** -// * Representation as a hex string. -// */ -// public asHex(): string { -// if (!this._hex) { -// if (this._raw) this._hex = encodeHex(this._raw) -// else this._hex = this._crypto!.payload('hex') -// } -// return this._hex -// } - -// public toJSON(): string { -// return this.asHex() -// } - -// /** -// * Representation as raw bytes. -// */ -// private asRaw(): Uint8Array { -// if (!this._raw) { -// // only if created from crypto -// this._raw = this._crypto!.payload() -// } -// return this._raw -// } -// } - /** * Name is a simple wrap around string that ensures that it * doesn't contain whitespaces characters, `@`, and `#`. * * @example * ```ts + * import { assertEquals, assertThrows } from '@std/assert' + * * const name1 = new Name('alice') - * console.log(name1.value) // => alice + * assertEquals(name1.value, 'alice') * - * new Name('alice and bob') // Error: whitespace characters. + * assertThrows(() => new Name('alice and bob')) // Error: whitespace characters. * ``` */ export class Name implements Ord { diff --git a/packages/core/data-model/mod.ts b/packages/core/data-model/mod.ts index 9c74c451..a7b9d32d 100644 --- a/packages/core/data-model/mod.ts +++ b/packages/core/data-model/mod.ts @@ -1,4 +1,79 @@ +/** + * Iroha data model - types and codecs. + * + * It is based on the `schema.json` generated by Iroha. The schema itself is also available, see the `@iroha/core/data-model/schema` and `@iroha/core/data-model/schema-json` modules. + * + * ## Encoding + * + * Each data model type has a codec to encode/decode it from [SCALE representation](https://docs.substrate.io/reference/scale-codec/) + * (a simple binary format). + * + * For example, here is how to encode and decode a {@linkcode BlockStatus}: + * + * ```ts + * import { getCodec } from '@iroha/core' + * import * as types from '@iroha/core/data-model' + * import { assertEquals } from '@std/assert/equals' + * import { encodeHex } from '@std/encoding/hex' + * + * const value = types.BlockStatus.Rejected.ConsensusBlockRejection + * + * const encoded: Uint8Array = getCodec(types.BlockStatus).encode(value) + * assertEquals(encodeHex(encoded), '0200') + * + * const decoded = getCodec(types.BlockStatus).decode(encoded) + * assertEquals(decoded, value) + * ``` + * + * ## Enumerations + * + * The data model contains many enumeration (i.e. discriminated unions), represented either as {@linkcode Variant} + * (`kind` + `value`) or {@linkcode VariantUnit} (just `kind`). An example of it is {@linkcode AssetType}: + * + * ```ts + * import * as types from '@iroha/core/data-model' + * + * const store: types.AssetType = { kind: 'Store' } + * const numeric: types.AssetType = { kind: 'Numeric', value: { scale: 5 } } + * ``` + * + * Alternatively, enums could be constructed with pre-generated constructors, which makes it less verbose: + * + * ```ts + * import * as types from '@iroha/core/data-model' + * import { assertEquals } from '@std/assert/equals' + * + * const store = types.AssetType.Store + * const numeric = types.AssetType.Numeric({ scale: 5 }) + * + * assertEquals(store, { kind: "Store" }) + * assertEquals(numeric, { kind: "Numeric", value: { scale: 5 } }) + * ``` + * + * Constructors approach is especially useful when it comes to enums nested into each other, e.g. {@link Parameter}: + * + * ```ts + * import * as types from '@iroha/core/data-model' + * import { assertEquals } from '@std/assert/equals' + * + * const value = types.Parameter.Sumeragi.BlockTime( + * types.Duration.fromMillis(500) + * ) + * + * assertEquals(value, { + * kind: "Sumeragi", + * value: { + * kind: "BlockTime", + * value: types.Duration.fromMillis(500) + * } + * }) + * ``` + * + * @module + */ + export * from './primitives.ts' export * from './compound.ts' export * from './_generated_.ts' -export { Hash, KeyPair, PrivateKey, PublicKey, Signature } from '@iroha/core/crypto' +export { Hash, KeyPair, PrivateKey, PublicKey, Signature } from '../crypto/mod.ts' +export type { Variant, VariantUnit } from '../util.ts' diff --git a/packages/core/data-model/primitives.ts b/packages/core/data-model/primitives.ts index 3ee4479c..6bd949c4 100644 --- a/packages/core/data-model/primitives.ts +++ b/packages/core/data-model/primitives.ts @@ -1,6 +1,6 @@ import * as scale from '@scale-codec/core' -import { GenCodec, structCodec } from '../codec.ts' -import { type CodecContainer, defineCodec, type IsZero, type Ord, ordCompare } from '../traits.ts' +import { type CodecContainer, defineCodec, GenCodec, structCodec } from '../codec.ts' +import { type IsZero, type Ord, ordCompare, type OrdKnown } from '../traits.ts' import { type CompareFn, toSortedSet } from '../util.ts' export type U8 = number @@ -105,8 +105,17 @@ export const Vec: { }, } +/** + * "Sorted vector". + * + * Represented as a plain array. The codec ensures that the entries are encoded in a deterministic manner, + * by sorting and deduplicating items. + */ export type BTreeSet = Vec +/** + * Codec factories for {@link BTreeSet:type} + */ export const BTreeSet: { with | string>(type: GenCodec): GenCodec> withCmp(codec: GenCodec, compare: CompareFn): GenCodec> @@ -128,13 +137,44 @@ export interface MapEntry { } /** - * Being represented as a plain array, its codec ensures that - * the entries are encoded in a deterministic manner, sorting and deduplicating items. + * "Sorted map". + * + * Represented as a plain array. The codec ensures that the entries are encoded in a deterministic manner, by sorting and deduplicating items. + * + * Items comparison is based on their keys. + * + * @example + * ```ts + * import { getCodec } from '../codec.ts' + * import { assertEquals } from '@std/assert/equals' + * + * const map1: BTreeMap = [ + * { key: 'a', value: 5 }, + * { key: 'c', value: 2 }, + * { key: 'b', value: 3 } + * ] + * + * const map2: BTreeMap = [ + * { key: 'c', value: 2 }, + * { key: 'a', value: 2 }, + * { key: 'a', value: 5 }, + * { key: 'b', value: 3 } + * ] + * + * const codec = BTreeMap.with(getCodec(String), getCodec(U8)) + * + * assertEquals(codec.encode(map1), codec.encode(map2)) + * assertEquals(codec.decode(codec.encode(map1)), [ + * { key: 'a', value: 5 }, + * { key: 'b', value: 3 }, + * { key: 'c', value: 2 } + * ]) + * ``` */ export type BTreeMap = Array> export const BTreeMap = { - with: , V>(key: GenCodec, value: GenCodec): GenCodec> => { + with: | OrdKnown, V>(key: GenCodec, value: GenCodec): GenCodec> => { return BTreeMap.withCmp(key, value, (a, b) => ordCompare(a.key, b.key)) }, withCmp: ( diff --git a/packages/core/data-model/schema/json.ts b/packages/core/data-model/schema/json.ts index d428a7d1..f9ca4c6e 100644 --- a/packages/core/data-model/schema/json.ts +++ b/packages/core/data-model/schema/json.ts @@ -1 +1,15 @@ -export { default } from '../../../../prep/iroha/schema.json' with { type: 'json' } +/** + * The Iroha's `schema.json` itself. + * + * Data model types are generated based on this schema. It could be used as a reference. + * + * @module + */ + +import { default as schema } from './schema.json' with { type: 'json' } +import type { Schema } from './mod.ts' + +type Test = true +type A = Test + +export default schema diff --git a/packages/core/data-model/schema/mod.ts b/packages/core/data-model/schema/mod.ts index d85b44cf..82a0e0fc 100644 --- a/packages/core/data-model/schema/mod.ts +++ b/packages/core/data-model/schema/mod.ts @@ -1,3 +1,9 @@ +/** + * Types describing Iroha's `schema.json` file. + * + * @module + */ + export interface Schema { [type: string]: SchemaTypeDefinition } diff --git a/packages/core/deno.jsonc b/packages/core/deno.jsonc index a5c85d39..2bbfc1a3 100644 --- a/packages/core/deno.jsonc +++ b/packages/core/deno.jsonc @@ -1,8 +1,9 @@ { "name": "@iroha/core", - "version": "0.0.0", + "version": "0.1.0", "exports": { ".": "./mod.ts", + "./codec": "./codec.ts", "./data-model": "./data-model/mod.ts", "./data-model/schema": "./data-model/schema/mod.ts", "./data-model/schema-json": "./data-model/schema/json.ts", diff --git a/packages/core/mod.ts b/packages/core/mod.ts index 0daa7f23..b0865ec5 100644 --- a/packages/core/mod.ts +++ b/packages/core/mod.ts @@ -1,12 +1,116 @@ -import * as crypto from '@iroha/core/crypto' -import { getCodec } from './traits.ts' -import * as types from '@iroha/core/data-model' +/** + * Core components of the [Iroha](https://github.com/hyperledger-iroha/iroha) JavaScript SDK. + * + * It includes Iroha Data Model types and codecs, `iroha_crypto` WASM interface, and + * utilities such as building/signing transactions/queries. + * + * It consists of the following modules: + * + * - `@iroha/core` - shared utilities + * - `@iroha/core/data-model` - the data model + * - `@iroha/core/crypto` - `iroha_crypto` WASM interface + * - `@iroha/core/codec` - lower-level utilities to work with the codec + * + * ### Install `iroha_crypto` WASM + * + * > [!IMPORTANT] + * > Make sure to install the `iroha_crypto` WASM **before using utilities that are dependant on it**. + * > + * > The simplest way to do so in Deno/Node.js is the following: + * > + * > ```ts ignore + * > import '@iroha/crypto-target-node/install' + * > ``` + * > + * > See the `@iroha/core/crypto` module for details. + * + * ### Iroha Compatibility + * + * Versions compatibility between this package and Iroha: + * + * | Iroha version | `@iroha/core` version | + * | --: | :-- | + * | `2.0.0-rc.1.x` | `0.1.0` | + * | `2.0.0-pre-rc.20.x` and before | the legacy SDK | + * + * The legacy SDK is the previous iteration on SDK that is no longer maintained. + * It is still available on Iroha Nexus NPM registry (https://nexus.iroha.tech/repository/npm-group/). + * Its source code could be found on the [`iroha-2-pre-rc`](https://github.com/hyperledger-iroha/iroha-javascript/tree/iroha-2-pre-rc) branch. + * + * @example Building and signing a transaction + * ```ts + * import '@iroha/crypto-target-node/install' + * + * import * as types from '@iroha/core/data-model' + * import { buildTransactionPayload, signTransaction } from '@iroha/core' + * + * const kp = types.KeyPair.random() + * + * const account = new types.AccountId(kp.publicKey(), new types.Name('wonderland')) + * + * const payload = buildTransactionPayload( + * types.Executable.Instructions([ + * types.InstructionBox.SetKeyValue.Domain({ + * object: new types.Name('wonderland'), + * key: new types.Name('foo'), + * value: types.Json.fromValue(['bar', 'baz']), + * }), + * ]), + * { + * chain: '000-000', + * authority: account, + * }, + * ) + * + * const signed: types.SignedTransaction = signTransaction(payload, kp.privateKey()) + * ``` + * + * @example Parsing & encoding an asset definition id + * ```ts + * import { getCodec } from '@iroha/core' + * import * as types from '@iroha/core/data-model' + * import { encodeHex } from '@std/encoding/hex' + * import { assertEquals } from '@std/assert/equals' + * + * const asset = types.AssetDefinitionId.parse("rose#wonderland") + * assertEquals(asset.name.value, 'rose') + * assertEquals(asset.domain.value, 'wonderland') + * assertEquals(asset.toString(), 'rose#wonderland') + * + * const encoded: Uint8Array = getCodec(types.AssetDefinitionId).encode(asset) + * assertEquals(encodeHex(encoded), '28776f6e6465726c616e6410726f7365') + * ``` + * + * @example Parsing an account id + * ```ts + * import '@iroha/crypto-target-node/install' + * import { getCodec } from '@iroha/core' + * import * as types from '@iroha/core/data-model' + * import { encodeHex } from '@std/encoding/hex' + * import { assertEquals } from '@std/assert/equals' + * + * const raw = "ed0120B23E14F659B91736AAB980B6ADDCE4B1DB8A138AB0267E049C082A744471714E@wonderland" + * + * const account = types.AccountId.parse(raw) + * assertEquals(account.signatory.algorithm, 'ed25519') + * assertEquals(account.domain.value, 'wonderland') + * assertEquals(account.toString(), raw) + * ``` + * + * Note that this example requires WASM installation, because account id contains a public key. + * + * @module + */ + +import * as crypto from './crypto/mod.ts' +import { getCodec } from './codec.ts' +import * as types from './data-model/mod.ts' export * from './query.ts' export * from './transaction.ts' -export * from './codec.ts' export * from './util.ts' export * from './traits.ts' +export { getCodec } /** * The one that is used for e.g. {@link types.TransactionEventFilter} diff --git a/packages/core/query.ts b/packages/core/query.ts index 9ec2ee86..222ee082 100644 --- a/packages/core/query.ts +++ b/packages/core/query.ts @@ -1,4 +1,4 @@ -import * as types from '@iroha/core/data-model' +import * as types from './data-model/mod.ts' import type { VariantUnit } from './util.ts' /** diff --git a/packages/core/traits.ts b/packages/core/traits.ts index 7ef1a2c7..cc577047 100644 --- a/packages/core/traits.ts +++ b/packages/core/traits.ts @@ -1,28 +1,3 @@ -import type { GenCodec } from './codec.ts' - -export const SYMBOL_CODEC = '$codec' - -/** - * Extracts codec from its container. - */ -export function getCodec(type: CodecContainer): GenCodec { - return type[SYMBOL_CODEC] -} - -/** - * Wraps a codec into {@link CodecContainer}. - */ -export function defineCodec(codec: GenCodec): CodecContainer { - return { [SYMBOL_CODEC]: codec } -} - -/** - * A value that contains a codec under a "special" key ({@link SYMBOL_CODEC}). - */ -export interface CodecContainer { - [SYMBOL_CODEC]: GenCodec -} - /** * Ordering "trait". Tells how to compare values of the same type with each other. */ diff --git a/packages/core/transaction.ts b/packages/core/transaction.ts index 11041d12..e53bab25 100644 --- a/packages/core/transaction.ts +++ b/packages/core/transaction.ts @@ -1,4 +1,4 @@ -import * as dm from '@iroha/core/data-model' +import * as dm from './data-model/mod.ts' export interface TransactionPayloadParams { chain: dm.ChainId diff --git a/packages/crypto-target-node/CHANGELOG.md b/packages/crypto-target-node/CHANGELOG.md deleted file mode 100644 index ba6e02bc..00000000 --- a/packages/crypto-target-node/CHANGELOG.md +++ /dev/null @@ -1,200 +0,0 @@ -# @iroha2/crypto-target-node - -## 2.0.0 - -### Major Changes - -- 787a198: **Breaking:** Complete rewrite of crypto WASM, and major update of the surrounding API. - - - Now WASM is made from the original `iroha_crypto` from Iroha 2 repo. As one of the outcomes, binary blob size is - reduced from 2mb to 600kb. - - Remove `KeyGenConfiguration`. Use `KeyPair.deriveFromSeed`, `KeyPair.deriveFromPrivateKey`, and `KeyPair.random` - instead. - - Normalise API across `PublicKey`, `PrivateKey`, `KeyPair`, and `Signature` classes (JSON methods, raw conversion - methods etc.) - - Introduce `Bytes` utility to accept binary input either as `Bytes.array([1, 2, 3])` or `Bytes.hex('001122')` - - Export more types¡ - - See the [issue](https://github.com/hyperledger/iroha-javascript/issues/186) for related context. - -### Patch Changes - -- Updated dependencies [787a198] - - @iroha2/crypto-core@2.0.0 - - @iroha2/data-model@7.1.0 - -## 1.1.1 - -### Patch Changes - -- Updated dependencies [e0459fa] -- Updated dependencies [e0459fa] -- Updated dependencies [e0459fa] -- Updated dependencies [e0459fa] -- Updated dependencies [e0459fa] - - @iroha2/data-model@7.0.0 - - @iroha2/crypto-core@1.1.1 - -## 1.1.0 - -### Minor Changes - -- 40516f1: **refactor**: combine new `Algorithm` type and codec from `data-model` with the crypto's `Algorithm` type, - which is simply a string. Add `Algorithm.toDataModel` and `Algorithm.fromDataModel` methods. - -### Patch Changes - -- Updated dependencies [40516f1] -- Updated dependencies [40516f1] - - @iroha2/data-model@6.0.0 - - @iroha2/crypto-core@1.1.0 - -## 1.0.1 - -### Patch Changes - -- Updated dependencies [d1e5f68] -- Updated dependencies [3ff768d] -- Updated dependencies [d1e5f68] - - @iroha2/data-model@5.0.0 - - @iroha2/crypto-core@1.0.1 - -## 1.0.0 - -### Major Changes - -- ddfeeac: **feature**: re-write WASM and provide high-level wrappers around it. - - #### What the braking change is - - Each target now provides high-level wrappers around raw `wasm-pack` artifacts. These wrappers provide a - better-designed interface with features like global `.free()`-objects tracking and integration with - `@iroha2/data-model`. - - Moreover, the WASM itself is re-written and now provides more flexibility, such as working with JSON and HEX - representations out of the box. - - Here you can see how `@iroha2/crypto-core` and `@iroha2/crypto-target-*` are connected: - - ```ts - import { cryptoTypes, IrohaCryptoInterface } from '@iroha2/crypto-core' - import { crypto } from '@iroha2/crypto-target-node' - - // each target exports `crypto`, which is the `IrohaCryptoInterface` type from - // the core library - const cryptoAsserted: IrohaCryptoInterface = crypto - - // the core library exports `cryptoTypes` namespace which contains all the types - // used in crypto you might need - const hash: cryptoTypes.Hash = crypto.Hash.hash('hex', '00ff') - ``` - - `@iroha2/crypto-core` re-exports `@iroha2/crypto-util`, a new library which contains (for now) only utilities to work - with `.free()` tracking: - - ```ts - import { FREE_HEAP, freeScope } from '@iroha2/crypto-util' - import { crypto } from '@iroha2/crypto-target-web' - - const keyPair = freeScope((scope) => { - const pair = crypto.KeyGenConfiguration - // Create a configuration object that you can later `.free()` manually. - // It is automatically attached to the scope it is created within, - // so when the scope is over, everything attached to it will be freed. - .default() - .useSeed('hex', 'ff') - // Create a new `.free()` object: a key pair - .generate() - - // to use the key pair (and nothing else) out of scope, - // you need to "untrack" it - scope.forget(pair) - - return pair - }) - - // inspect the heap in order to determine if there are memory leaks - if (FREE_HEAP.size > 1) { - console.log('Something went wrong, I guess?') - } - ``` - - #### Why the change was made - - Codegen of `wasm_bindgen` is very limited. This change is made to provide a better quality and safer API over crypto - WASM. - - #### How a consumer should update their code - - Unfortunately, the code should be updated completely. Here are some major points you should note. - - `IrohaCryptoInterface` type from the core package is still the same as `crypto` export from target packages, but the - content of the type is completely different. - - Previously, types such as `Hash`, `Signature`, `PublicKey` were separate exports from the core library. Now they are - contained within the `cryptoTypes` namespace: - - ```ts - // doesn't work anymore - // import { Hash } from '@iroha2/crypto-core' - - import { cryptoTypes } from '@iroha2/crypto-core' - - type Hash = cryptoTypes.Hash - ``` - -### Patch Changes - -- Updated dependencies [ddfeeac] - - @iroha2/crypto-core@1.0.0 - - @iroha2/data-model@4.1.0 - -## 0.4.0 - -### Minor Changes - -- bd03fc0: **fix!**: use `createRequire()` in ESM entry, and plain `require()` in CJS - -## 0.3.0 - -### Minor Changes - -- a99d219: **fix!**: define `exports` field; use `*.cjs` extension for `require()` imports and `*.mjs` for `import` - -## 0.2.0 - -### Minor Changes - -- 6f6163f: **BREAKING:** - - - Package contents are moved to `dist` directory; - - Compiled WASM is moved to `dist/wasm` directory. - - Compiled WASM is renamed to `crypto` (from `wasm_pack_output`) - - **How to migrate:** - - - If you have imported WASMs like this: - - ```ts - import rawWasm from '@iroha2/crypto-target-*/wasm_pack_output_bg.wasm' - ``` - - then now you should change import path: - - ```ts - import rawWasm from '@iroha2/crypto-target-*/dist/wasm/crypto_bg.wasm' - ``` - -### Patch Changes - -- 49c8451: fix: make `@iroha2/crypto-core` a prod dependency, not a dev -- 6f6163f: rebuild WASM -- 49c8451: chore: include only necessary files into `files` field in the `package.json` -- Updated dependencies [49c8451] - - @iroha2/crypto-core@0.1.1 - -## 0.1.1 - -### Patch Changes - -- 98d3638: recompile wasms diff --git a/packages/crypto-target-node/README.md b/packages/crypto-target-node/README.md deleted file mode 100644 index 5ac62216..00000000 --- a/packages/crypto-target-node/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# `@iroha2/crypto-target-node` - -The `@iroha2/crypto-target-node` package contains a crypto WASM compiled with the `node` target. This package provides -crypto interface for the Node.js environment. - -## Usage - -```ts -// commonjs style -const { crypto } = require('@iroha2/crypto-target-node') - -// esm style -import { crypto } from '@iroha2/crypto-target-node' -``` - -See [`@iroha2/crypto-core`](https://github.com/hyperledger/iroha-javascript/tree/iroha2/packages/crypto/packages/core) -package for details. diff --git a/packages/crypto-target-node/deno.jsonc b/packages/crypto-target-node/deno.jsonc index e4edcaef..c7858f92 100644 --- a/packages/crypto-target-node/deno.jsonc +++ b/packages/crypto-target-node/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@iroha/crypto-target-node", - "version": "0.0.0", + "version": "0.1.0", "exports": { ".": "./mod.ts", "./install": "./install.ts" diff --git a/packages/crypto-target-node/install.ts b/packages/crypto-target-node/install.ts index 4057e331..7338eb8e 100644 --- a/packages/crypto-target-node/install.ts +++ b/packages/crypto-target-node/install.ts @@ -1,3 +1,9 @@ +/** + * Entrypoint that sets the WASM in `@iroha/core/crypto`. + * + * @module + */ + import { install } from './mod.ts' install() diff --git a/packages/crypto-target-node/mod.ts b/packages/crypto-target-node/mod.ts index e939c50c..6553ddcf 100644 --- a/packages/crypto-target-node/mod.ts +++ b/packages/crypto-target-node/mod.ts @@ -1,5 +1,22 @@ /** - * @module @iroha/crypto-target-node + * `iroha_crypto` WASM built for Node.js target. + * + * Works with Deno too, but requires `--allow-read` permission. + * + * @example + * ```ts + * import { setWASM } from '@iroha/core/crypto' + * import { wasmPkg } from '@iroha/crypto-target-node' + * + * setWASM(wasmPkg) + * ``` + * + * @example + * ```ts + * import '@iroha/crypto-target-node/install' + * ``` + * + * @module */ import { setWASM } from '@iroha/core/crypto' @@ -8,6 +25,9 @@ import wasmPkg from './wasm-target/iroha_crypto.js' export { wasmPkg } +/** + * Shortcut to `setWASM(wasmPkg)`. + */ export function install() { setWASM(wasmPkg) } diff --git a/packages/crypto-target-web/deno.jsonc b/packages/crypto-target-web/deno.jsonc index ce9799b6..5aecfb1d 100644 --- a/packages/crypto-target-web/deno.jsonc +++ b/packages/crypto-target-web/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@iroha/crypto-target-web", - "version": "0.0.0", + "version": "0.1.0", "exports": { ".": "./mod.ts", "./install": "./install.ts" diff --git a/packages/crypto-target-web/install.ts b/packages/crypto-target-web/install.ts index e0c663f1..da5e76e8 100644 --- a/packages/crypto-target-web/install.ts +++ b/packages/crypto-target-web/install.ts @@ -1,3 +1,9 @@ +/** + * Entrypoint that sets the WASM in `@iroha/core/crypto`. + * + * @module + */ + import { install } from './mod.ts' await install() diff --git a/packages/crypto-target-web/mod.ts b/packages/crypto-target-web/mod.ts index cb942755..8148a5da 100644 --- a/packages/crypto-target-web/mod.ts +++ b/packages/crypto-target-web/mod.ts @@ -1,5 +1,23 @@ /** - * @module @iroha/crypto-target-web + * `iroha_crypto` WASM built for Web target (native Browser ESModule environment). + * + * @example + * ```ts + * import { setWASM } from '@iroha/core/crypto' + * import { wasmPkg, init } from '@iroha/crypto-target-web' + * + * // this is necessary + * await init() + * + * setWASM(wasmPkg) + * ``` + * + * @example + * ```ts + * import '@iroha/crypto-target-web/install' + * ``` + * + * @module */ import { setWASM } from '@iroha/core/crypto' @@ -7,6 +25,9 @@ import { setWASM } from '@iroha/core/crypto' import * as wasmPkg from './wasm-target/iroha_crypto.js' import init from './wasm-target/iroha_crypto.js' +/** + * Shortcut to initialise and install the WASM. + */ export async function install() { await init() setWASM(wasmPkg) diff --git a/tests/browser/src/client.ts b/tests/browser/src/client.ts index 57990c2e..67621819 100644 --- a/tests/browser/src/client.ts +++ b/tests/browser/src/client.ts @@ -1,7 +1,6 @@ import { ACCOUNT_KEY_PAIR, CHAIN, DOMAIN } from '@iroha/test-configuration' import { Client } from '@iroha/client' -import { adapter as WS } from '@iroha/client/web-socket/native' // it must resolve first, before using core crypto exports import './setup-crypto.ts' @@ -19,7 +18,6 @@ export const client = new Client({ // proxified with vite toriiBaseURL: new URL(`http://${HOST}/torii`), - ws: WS, chain: CHAIN, accountDomain: DOMAIN, accountKeyPair: keyPair, diff --git a/tests/node/tests/codec-compat.spec.ts b/tests/node/tests/codec-compat.spec.ts index 501e5455..70440520 100644 --- a/tests/node/tests/codec-compat.spec.ts +++ b/tests/node/tests/codec-compat.spec.ts @@ -1,5 +1,5 @@ import type { Except, JsonValue } from 'type-fest' -import { type CodecContainer, defineCodec, getCodec } from '@iroha/core' +import { type CodecContainer, defineCodec, getCodec } from '@iroha/core/codec' import * as dm from '@iroha/core/data-model' import type SCHEMA from '@iroha/core/data-model/schema-json' import { irohaCodecToScale } from 'iroha-build-utils' diff --git a/tests/node/tests/util.ts b/tests/node/tests/util.ts index 5e31f26b..24de1b1e 100644 --- a/tests/node/tests/util.ts +++ b/tests/node/tests/util.ts @@ -1,7 +1,7 @@ import { onTestFinished } from 'vitest' import uniquePort from 'get-port' import { Client } from '../../../packages/client/mod.ts' -import { adapter as WS } from '../../../packages/client/web-socket/node.ts' +import WS from '@iroha/client-web-socket-node' import { ACCOUNT_KEY_PAIR, CHAIN, DOMAIN } from '@iroha/test-configuration' import { createGenesis } from '@iroha/test-configuration/node' import { Bytes, KeyPair, PrivateKey, PublicKey } from '@iroha/core/crypto'