diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef26ca03..5381b382 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,3 +57,16 @@ You can add this to `.git/hooks/pre-commit`: #!/bin/sh deno task ok ``` + +## Preview Docs + +You can `cd` into a package directory (e.g. `packages/core`) and run these commands in parallel (requires `watchexec` +and `jq` tools): + +```shell +watchexec -e ts deno doc --html $(cat deno.json* | deno run npm:json5 | jq -r '.exports | .[]') +``` + +```shell +deno run -A npm:vite serve docs +``` diff --git a/README.md b/README.md index 1e41038a..607af6ae 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ 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, + authority: new types.AccountId(kp.publicKey(), new types.DomainId('wonderland')), + authorityPrivateKey: kp.privateKey(), }) async function test() { diff --git a/deno.lock b/deno.lock index f790c294..7e9bdd58 100644 --- a/deno.lock +++ b/deno.lock @@ -50,6 +50,7 @@ "npm:h3@^1.15.0": "1.15.0", "npm:immutable@^5.0.3": "5.0.3", "npm:jake@^10.9.2": "10.9.2", + "npm:json5@*": "2.2.3", "npm:listhen@^1.9.0": "1.9.0", "npm:npm-run-all@^4.1.5": "4.1.5", "npm:p-defer@^4.0.1": "4.0.1", @@ -1819,6 +1820,9 @@ "json-stringify-safe@5.0.1": { "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "json5@2.2.3": { + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, "jsonfile@6.1.0": { "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": [ diff --git a/packages/client/api-ws.ts b/packages/client/api-ws.ts index 85420288..0a1a2b78 100644 --- a/packages/client/api-ws.ts +++ b/packages/client/api-ws.ts @@ -6,6 +6,9 @@ import type { SocketEmitMapBase } from './util.ts' import { setupWebSocket } from './util.ts' import { type IsomorphicWebSocketAdapter, nativeWS } from './web-socket/mod.ts' +/** + * Lower-level client + */ export class WebSocketAPI { public readonly toriiBaseURL: URL public readonly adapter: IsomorphicWebSocketAdapter diff --git a/packages/client/api.ts b/packages/client/api.ts index 4cff3649..602b4519 100644 --- a/packages/client/api.ts +++ b/packages/client/api.ts @@ -16,7 +16,7 @@ import { import { urlJoinPath } from './util.ts' /** - * Peer information returned from {@link ApiTelemetry.peers} + * Peer information returned from {@link TelemetryAPI.peers} */ export interface PeerJson { /** @@ -78,17 +78,26 @@ export class HttpTransport { } } +/** + * Lower-level client to interact with Iroha HTTP APIs. + * + * It is separated from {@linkcode WebSocketAPI}. + * + * It is lower-level in a sense that, for example, {@linkcode MainAPI#transaction} accepts an already signed transaction + * and simply "fire and forget"s it, while {@linkcode Client#transaction} helps to construct a transaction, submit it, + * and verify that it is accepted. + */ export class MainAPI { /** * Works only if Iroha is compiled with `telemetry` feature flag. */ - public readonly telemetry: ApiTelemetry + public readonly telemetry: TelemetryAPI private readonly http: HttpTransport public constructor(http: HttpTransport) { this.http = http - this.telemetry = new ApiTelemetry(http) + this.telemetry = new TelemetryAPI(http) } public async health(): Promise { @@ -178,7 +187,7 @@ export class QueryValidationError extends Error { } // TODO: handle errors with a hint that Iroha might be not compiled with the needed features -export class ApiTelemetry { +export class TelemetryAPI { private readonly http: HttpTransport public constructor(http: HttpTransport) { diff --git a/packages/client/client.ts b/packages/client/client.ts index 2286c11f..8a45b20b 100644 --- a/packages/client/client.ts +++ b/packages/client/client.ts @@ -1,4 +1,4 @@ -import type { KeyPair, PrivateKey } from '@iroha/core/crypto' +import type { PrivateKey } from '@iroha/core/crypto' import * as types from '@iroha/core/data-model' import type { Except } from 'type-fest' import defer from 'p-defer' @@ -31,9 +31,18 @@ export interface CreateClientParams { * The base URL of **Torii**, Iroha API Gateway. */ toriiBaseURL: URL + /** + * Chain ID. + */ chain: string - accountDomain: types.DomainId - accountKeyPair: KeyPair + /** + * Authority on which behalf to sign transactions and queries. + */ + authority: types.AccountId + /** + * The private key of {@linkcode CreateClientParams.authority}. + */ + authorityPrivateKey: types.PrivateKey } export interface SubmitParams { @@ -62,6 +71,20 @@ export class TransactionExpiredError extends Error { } } +/** + * All-in-one Iroha client. + * + * Through it, it is possible to perform all different kinds of interactions with Iroha, e.g. + * signing and submitting transactions and queries or listening to events through WebSockets. + * + * It is possible to use each layer of functionality separately, through lower-level layers: + * + * - {@linkcode MainAPI} + * - {@linkcode WebSocketAPI} + * + * It could be useful if e.g. you don't need to submit transactions (which requires an account with a key pair), + * but only want to check Iroha status. + */ export class Client { public readonly params: CreateClientParams @@ -83,21 +106,18 @@ export class Client { const http = new HttpTransport(params.toriiBaseURL, params.fetch) this.api = new MainAPI(http) - const executor = new QueryExecutor(this.api, this.authority(), this.authorityPrivateKey()) + const executor = new QueryExecutor(this.api, this.authority, this.authorityPrivateKey) this.find = new FindAPI(executor) this.socket = new WebSocketAPI(params.toriiBaseURL, params.ws) } - public authority(): types.AccountId { - return new types.AccountId( - this.params.accountKeyPair.publicKey(), - this.params.accountDomain, - ) + public get authority(): types.AccountId { + return this.params.authority } - public authorityPrivateKey(): PrivateKey { - return this.params.accountKeyPair.privateKey() + public get authorityPrivateKey(): PrivateKey { + return this.params.authorityPrivateKey } /** @@ -114,10 +134,10 @@ export class Client { const tx = signTransaction( buildTransactionPayload(executable, { chain: this.params.chain, - authority: this.authority(), + authority: this.authority, ...params, }), - this.params.accountKeyPair.privateKey(), + this.authorityPrivateKey, ) return new TransactionHandle(tx, this) diff --git a/packages/client/mod.ts b/packages/client/mod.ts index aa02c471..ca20e21d 100644 --- a/packages/client/mod.ts +++ b/packages/client/mod.ts @@ -3,43 +3,171 @@ * * The primary functionality is exposed via the {@linkcode Client}. * - * @example Constructing the client + * @example Construct a client with a random account * ```ts - * 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 domain = new types.Name('wonderland') + * const account = new types.AccountId(kp.publicKey(), domain) * * 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 + * toriiBaseURL: new URL('http://localhost:8080'), + * authority: account, + * authorityPrivateKey: kp.privateKey() * }) * ``` * - * @example Querying data + * @example Construct a client from `iroha`'s TOML configuration + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * import * as TOML from 'jsr:@std/toml' + * + * const configRaw = ` + * chain = "00000000-0000-0000-0000-000000000000" + * torii_url = "http://127.0.0.1:8080/" + * + * [account] + * domain = "wonderland" + * public_key = "ed0120CE7FA46C9DCE7EA4B125E2E36BDB63EA33073E7590AC92816AE1E861B7048B03" + * private_key = "802620CCF31D85E3B32A4BEA59987CE0C78E3B8E2DB93881468AB2435FE45D5C9DCD53" + * `; + * const config = TOML.parse(configRaw) as Record + * + * const client = new Client({ + * chain: config.chain, + * toriiBaseURL: new URL(config.torii_url), + * authority: types.AccountId.parse(`${config.account.public_key}@${config.account.domain}`), + * authorityPrivateKey: types.PrivateKey.fromMultihash(config.account.private_key) + * }) + * ``` + * + * @example Register a new domain, account, and numeric asset * ```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(); + * const newDomain: types.NewDomain = { + * id: new types.Name('test'), + * logo: null, + * metadata: [] + * } + * + * const newAccount: types.NewAccount = { + * id: new types.AccountId(types.KeyPair.random().publicKey(), newDomain.id), + * metadata: [], + * } + * + * const newAsset: types.NewAssetDefinition = { + * id: new types.AssetDefinitionId(new types.Name('time'), newDomain.id), + * type: types.AssetType.Numeric({ scale: 1 }), + * mintable: types.Mintable.Infinitely, + * logo: null, + * metadata: [], + * } + * + * await client.transaction( + * types.Executable.Instructions([ + * types.InstructionBox.Register.Domain(newDomain), + * types.InstructionBox.Register.Account(newAccount), + * types.InstructionBox.Register.AssetDefinition(newAsset), + * ]), + * ).submit({ verify: true }) + * } + * ``` + * + * @example Mint an asset + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * async function mint100(client: Client, definition: types.AssetDefinitionId, account: types.AccountId) { + * await client.transaction(types.Executable.Instructions([ + * types.InstructionBox.Mint.Asset({ + * object: { scale: 0n, mantissa: 100n }, + * destination: new types.AssetId(account, definition), + * }), + * ])).submit({ verify: true }) + * } + * ``` * - * // find all accounts in a domain ending with `land` + * This function mints 100 of _something_ of the given asset `definition` to the given `account`. + * + * @example List domains, accounts, and assets + * + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * async function list(client: Client) { + * for (const domain of await client.find.domains().executeAll()) { + * console.log('Domain with ID:', domain.id.value) + * // => Domain with ID: wonderland + * // => Domain with ID: looking_glass + * // .. + * } + * + * for (const account of await client.find.accounts().executeAll()) { + * console.log('Account with signatory', account.id.signatory.multihash(), '@ domain', account.id.domain.value) + * // => Account with signatory ed0120CE7FA46C9DCE7EA4B125E2E36BDB63EA33073E7590AC92816AE1E861B7048B03 @ domain wonderland + * // .. + * } + * + * for (const asset of await client.find.assets().executeAll()) { + * console.log('Asset', asset.id.toString()) + * if (asset.value.kind === 'Numeric') { + * console.log(' Numeric:', asset.value.value.mantissa, asset.value.value.scale) + * } else { + * for (const { key, value } of asset.value.value) { + * console.log(` Metadata: key="${key.value}" value=${value.asJsonString()}`) + * } + * } + * // => Asset rose##ed0120CE7FA46C9DCE7EA4B125E2E36BDB63EA33073E7590AC92816AE1E861B7048B03@wonderland + * // => Numeric: 42n 0n + * // => Asset registry#wonderland#ed0120CE7FA46C9DCE7EA4B125E2E36BDB63EA33073E7590AC92816AE1E861B7048B03@looking_glass + * // => Metadata: key="foo" value=['foo', 'bar'] + * // => Metadata: key="bar" value={"whatever":"whichever"} + * // ... + * } + * } + * ``` + * + * @example Filter and paginate query results + * + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * async function test(client: Client) { * const accounts: types.Account[] = await client.find * .accounts({ * predicate: types.CompoundPredicate.Atom( * types.AccountProjectionPredicate.Id.Domain.Name.Atom.EndsWith('land') - * ) + * ), + * offset: 10, + * limit: new types.NonZero(50), * }) * .executeAll() + * } + * ``` + * + * This example finds all accounts whose domain name ends with `land`. + * + * > [!IMPORTANT] + * > Current selectors and predicates implementation is not very intuitive and easy to use, and it most probably will change (see + * > [tracking issue](https://github.com/hyperledger-iroha/iroha-javascript/issues/213)). * + * @example Use query selectors + * + * ```ts + * import { Client } from '@iroha/client' + * import * as types from '@iroha/core/data-model' + * + * async function test(client: Client) { * // use selectors and pagination * const items: [types.Hash, types.AccountId][] = await client.find * .transactions({ @@ -47,40 +175,20 @@ * 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: [], - * }), - * ]), - * ) + * This example finds all transaction and retrieves them as tuples of their block hash and authority id. * - * // could be used to watch for events - * const hash: types.Hash = txHandle.hash; + * Note that resulting types are inferred automatically based on the selectors you pass. * - * // submit and wait until the transaction is committed - * await txHandle.submit({ verify: true }) - * } - * ``` + * > [!IMPORTANT] + * > Current selectors and predicates implementation is not very intuitive and easy to use, and it most probably will change (see + * > [tracking issue](https://github.com/hyperledger-iroha/iroha-javascript/issues/213)). * - * @example Using lower-level API utilitites + * @example Make lower-level API calls * ```ts * import { MainAPI, HttpTransport } from '@iroha/client' * import { assertEquals } from '@std/assert/equals' @@ -96,6 +204,8 @@ * } * ``` * + * This example shows that you don't need to use {@linkcode Client} to make such simple API calls. + * * @module */ diff --git a/tests/browser/src/client.ts b/tests/browser/src/client.ts index 8613eb0c..78af382c 100644 --- a/tests/browser/src/client.ts +++ b/tests/browser/src/client.ts @@ -1,11 +1,6 @@ import { ACCOUNT_KEY_PAIR, CHAIN, DOMAIN } from '@iroha/test-configuration' import { Client } from '@iroha/client' -import { KeyPair, PrivateKey, PublicKey } from '@iroha/core/crypto' - -const keyPair = KeyPair.fromParts( - PublicKey.fromMultihash(ACCOUNT_KEY_PAIR.publicKey), - PrivateKey.fromMultihash(ACCOUNT_KEY_PAIR.privateKey), -) +import * as types from '@iroha/core/data-model' const HOST = globalThis.location.host @@ -14,6 +9,6 @@ export const client = new Client({ toriiBaseURL: new URL(`http://${HOST}/torii`), chain: CHAIN, - accountDomain: DOMAIN, - accountKeyPair: keyPair, + authority: new types.AccountId(ACCOUNT_KEY_PAIR.publicKey(), DOMAIN), + authorityPrivateKey: ACCOUNT_KEY_PAIR.privateKey(), }) diff --git a/tests/node/tests/client-misc.spec.ts b/tests/node/tests/client-misc.spec.ts index dc5c49f9..86c9901f 100644 --- a/tests/node/tests/client-misc.spec.ts +++ b/tests/node/tests/client-misc.spec.ts @@ -604,7 +604,7 @@ describe('Roles & Permission', () => { const permissions = await client.find .permissionsByAccountId({ - id: new dm.AccountId(dm.PublicKey.fromMultihash(ACCOUNT_KEY_PAIR.publicKey), DOMAIN), + id: new dm.AccountId(ACCOUNT_KEY_PAIR.publicKey(), DOMAIN), }) .executeAll() diff --git a/tests/node/tests/util.ts b/tests/node/tests/util.ts index 24de1b1e..b109712f 100644 --- a/tests/node/tests/util.ts +++ b/tests/node/tests/util.ts @@ -4,8 +4,8 @@ import { Client } from '../../../packages/client/mod.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' -import type * as dm from '@iroha/core/data-model' +import { Bytes, KeyPair } from '@iroha/core/crypto' +import * as dm from '@iroha/core/data-model' import * as TestPeer from '@iroha/test-peer' import { delay } from '@std/async' @@ -17,13 +17,6 @@ async function waitForGenesisCommitted(f: () => Promise) { } } -function getAccountKeyPair() { - const accountPublicKey = PublicKey.fromMultihash(ACCOUNT_KEY_PAIR.publicKey) - const accountPrivateKey = PrivateKey.fromMultihash(ACCOUNT_KEY_PAIR.privateKey) - const accountKeyPair = KeyPair.fromParts(accountPublicKey, accountPrivateKey) - return accountKeyPair -} - async function uniquePortsPair() { return { api: await uniquePort(), @@ -72,8 +65,8 @@ export async function useNetwork(params: { const client = new Client({ ws: WS, toriiBaseURL: new URL(`http://localhost:${ports.api}`), - accountKeyPair: getAccountKeyPair(), - accountDomain: DOMAIN, + authority: new dm.AccountId(ACCOUNT_KEY_PAIR.publicKey(), DOMAIN), + authorityPrivateKey: ACCOUNT_KEY_PAIR.privateKey(), chain: CHAIN, }) diff --git a/tests/support/test-configuration/deno.jsonc b/tests/support/test-configuration/deno.jsonc index ac69ab8a..85a9f563 100644 --- a/tests/support/test-configuration/deno.jsonc +++ b/tests/support/test-configuration/deno.jsonc @@ -6,5 +6,10 @@ }, "imports": { "tempy": "npm:tempy@^3.1.0" + }, + "lint": { + "rules": { + "exclude": ["no-slow-types"] + } } } diff --git a/tests/support/test-configuration/mod.ts b/tests/support/test-configuration/mod.ts index 698769f4..fb0c90c4 100644 --- a/tests/support/test-configuration/mod.ts +++ b/tests/support/test-configuration/mod.ts @@ -1,22 +1,22 @@ -import * as dm from '@iroha/core/data-model' +import * as types from '@iroha/core/data-model' -export const DOMAIN: dm.DomainId = new dm.Name('wonderland') +export const DOMAIN: types.DomainId = new types.Name('wonderland') -export const ACCOUNT_KEY_PAIR = { - publicKey: 'ed0120B23E14F659B91736AAB980B6ADDCE4B1DB8A138AB0267E049C082A744471714E', - privateKey: '802620E28031CC65994ADE240E32FCFD0405DF30A47BDD6ABAF76C8C3C5A4F3DE96F75', -} as const +export const ACCOUNT_KEY_PAIR = types.KeyPair.fromParts( + types.PublicKey.fromMultihash('ed0120B23E14F659B91736AAB980B6ADDCE4B1DB8A138AB0267E049C082A744471714E'), + types.PrivateKey.fromMultihash('802620E28031CC65994ADE240E32FCFD0405DF30A47BDD6ABAF76C8C3C5A4F3DE96F75'), +) -export const GENESIS_KEY_PAIR = { - publicKey: 'ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4', - privateKey: '80262082B3BDE54AEBECA4146257DA0DE8D59D8E46D5FE34887DCD8072866792FCB3AD', -} as const +export const GENESIS_KEY_PAIR = types.KeyPair.fromParts( + types.PublicKey.fromMultihash('ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4'), + types.PrivateKey.fromMultihash('80262082B3BDE54AEBECA4146257DA0DE8D59D8E46D5FE34887DCD8072866792FCB3AD'), +) export const CHAIN = '00000000-0000-0000-0000-000000000000' export const PEER_CONFIG_BASE = { chain: CHAIN, genesis: { - public_key: GENESIS_KEY_PAIR.publicKey, + public_key: GENESIS_KEY_PAIR.publicKey().multihash(), }, } as const diff --git a/tests/support/test-configuration/node.ts b/tests/support/test-configuration/node.ts index 9710a64c..6a925264 100644 --- a/tests/support/test-configuration/node.ts +++ b/tests/support/test-configuration/node.ts @@ -20,8 +20,8 @@ export async function createGenesis(params: { */ topology: PublicKey[] }): Promise { - const alice = dm.AccountId.parse(`${ACCOUNT_KEY_PAIR.publicKey}@${DOMAIN.value}`) - const genesis = dm.AccountId.parse(`${GENESIS_KEY_PAIR.publicKey}@genesis`) + const alice = dm.AccountId.parse(`${ACCOUNT_KEY_PAIR.publicKey().multihash()}@${DOMAIN.value}`) + const genesis = dm.AccountId.parse(`${GENESIS_KEY_PAIR.publicKey().multihash()}@genesis`) const instructionsJson = await irohaCodecToJson( 'Vec', @@ -63,9 +63,9 @@ async function signGenesisWithKagami(json: unknown): Promise { `sign`, path.join(dir, 'genesis.json'), `--public-key`, - GENESIS_KEY_PAIR.publicKey, + GENESIS_KEY_PAIR.publicKey().multihash(), `--private-key`, - GENESIS_KEY_PAIR.privateKey, + GENESIS_KEY_PAIR.privateKey().multihash(), // '--out-file', // path.join(dir, 'genesis.scale'), ], { stdio: ['ignore', 'pipe', 'inherit'] })