diff --git a/Cargo.toml b/Cargo.toml index 61c8bfa6..9a6c9d73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,29 +8,42 @@ rust-version = "1.85" [workspace] resolver = "2" -members = ["audit-trail-rs", "examples", "notarization-rs"] -exclude = ["bindings/wasm/notarization_wasm", "bindings/wasm/audit_trail_wasm"] +members = ["audit-trail-rs", "examples", "notarization-rs", "poi-rs"] +exclude = [ + "bindings/wasm/notarization_wasm", + "bindings/wasm/audit_trail_wasm", + "bindings/wasm/poi_wasm", +] [workspace.dependencies] anyhow = "1.0" async-trait = "0.1" bcs = "0.1" chrono = { version = "0.4", default-features = false } +clap = { version = "4.6.1", features = ["derive"] } hyper = "1" +iota-grpc-client = { git = "https://github.com/iotaledger/iota-rust-sdk.git", package = "iota-sdk-grpc-client", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339" } +iota-grpc-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", package = "iota-sdk-grpc-types", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339" } iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v1.27.0" } -iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "ce81341ac3fdb7204df112182c68319f26d5896b", default-features = false } +iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339", default-features = false } +iota-types = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436" } iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "iota_interaction" } iota_interaction_rust = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "iota_interaction_rust" } iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "iota_interaction_ts" } product_common = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.23", default-features = false, package = "product_common" } +reqwest = { version = "0.12.28", default-features = false, features = ["rustls-tls"] } secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.3.0", default-features = false } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde-aux = { version = "4.7.0", default-features = false } serde_json = { version = "1.0", default-features = false } sha2 = { version = "0.10", default-features = false } strum = { version = "0.27", default-features = false, features = ["std", "derive"] } +tempfile = "3.27.0" thiserror = { version = "2.0", default-features = false } tokio = { version = "1.52.2", default-features = false, features = ["macros", "sync", "rt", "process"] } +[patch."https://github.com/MystenLabs/fastcrypto"] +fastcrypto = "=0.1.11" + [profile.release.package.iota_interaction_ts] opt-level = 's' diff --git a/bindings/wasm/build/node.js b/bindings/wasm/build/node.js index 95c58bd3..2ad06faf 100644 --- a/bindings/wasm/build/node.js +++ b/bindings/wasm/build/node.js @@ -4,6 +4,7 @@ const { lintAll } = require("./lints"); const generatePackage = require("./utils/generatePackage"); const artifact = process.argv[2]; +const skipFetchPolyfill = process.argv.includes("--skip-fetch-polyfill"); const RELEASE_FOLDER = path.join(__dirname, "..", artifact, "node"); const entryFilePathNode = path.join(RELEASE_FOLDER, `${artifact}.js`); @@ -12,10 +13,11 @@ console.log(`[build/node.js] Processing entryFile '${entryFilePathNode}' for art lintAll(entryFileNode); -// Add node-fetch polyfill (https://github.com/seanmonstar/reqwest/issues/910). -let changedFileNode = entryFileNode.replace( - "let imports = {};", - `if (!globalThis.fetch) { +if (!skipFetchPolyfill) { + // Add node-fetch polyfill (https://github.com/seanmonstar/reqwest/issues/910). + const changedFileNode = entryFileNode.replace( + "let imports = {};", + `if (!globalThis.fetch) { const fetch = require('node-fetch') globalThis.Headers = fetch.Headers globalThis.Request = fetch.Request @@ -23,15 +25,16 @@ let changedFileNode = entryFileNode.replace( globalThis.fetch = fetch } let imports = {};`, -); + ); -fs.writeFileSync( - entryFilePathNode, - changedFileNode, -); -console.log( - `[build/node.js] Added node-fetch polyfill to entryFile '${entryFilePathNode}'. Starting generatePackage().`, -); + fs.writeFileSync( + entryFilePathNode, + changedFileNode, + ); + console.log(`[build/node.js] Added node-fetch polyfill to entryFile '${entryFilePathNode}'.`); +} else { + console.log(`[build/node.js] Skipped node-fetch polyfill for artifact '${artifact}'.`); +} // Generate `package.json`. const newPackage = generatePackage({ diff --git a/bindings/wasm/poi_wasm/.cargo/config.toml b/bindings/wasm/poi_wasm/.cargo/config.toml new file mode 100644 index 00000000..18a827be --- /dev/null +++ b/bindings/wasm/poi_wasm/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/bindings/wasm/poi_wasm/.gitignore b/bindings/wasm/poi_wasm/.gitignore new file mode 100644 index 00000000..a706174e --- /dev/null +++ b/bindings/wasm/poi_wasm/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.cache/ +target/ +node/ diff --git a/bindings/wasm/poi_wasm/Cargo.toml b/bindings/wasm/poi_wasm/Cargo.toml new file mode 100644 index 00000000..d13d0c83 --- /dev/null +++ b/bindings/wasm/poi_wasm/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "poi_wasm" +version = "0.1.0-alpha" +authors = ["IOTA Stiftung"] +edition = "2024" +homepage = "https://www.iota.org" +keywords = ["iota", "proof", "inclusion", "wasm"] +license = "Apache-2.0" +publish = false +readme = "README.md" +repository = "https://github.com/iotaledger/notarization" +rust-version = "1.85" +description = "Node.js WASM bindings for the IOTA Proof of Inclusion Package." + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +async-trait = { version = "0.1", default-features = false } +bcs = "0.1.6" +console_error_panic_hook = "0.1" +fastcrypto = "=0.1.11" +iota-sdk-types = { git = "https://github.com/iotaledger/iota-rust-sdk.git", rev = "b77fcd5ac5fedb3dfbc77ba7d183140e43512339", default-features = false, features = ["serde"] } +iota-types = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436" } +js-sys = "=0.3.85" +poi-rs = { path = "../../../poi-rs", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +thiserror = { version = "2.0", default-features = false } +wasm-bindgen = "=0.2.108" +wasm-bindgen-futures = "=0.4.58" + +[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] +getrandom = { version = "0.3", default-features = false, features = ["wasm_js"] } + +[profile.release] +lto = true +opt-level = "s" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] } diff --git a/bindings/wasm/poi_wasm/README.md b/bindings/wasm/poi_wasm/README.md new file mode 100644 index 00000000..3576686c --- /dev/null +++ b/bindings/wasm/poi_wasm/README.md @@ -0,0 +1,139 @@ +# Proof of Inclusion Node.js Package + +This package generates a typed Node.js client for IOTA's `LedgerService` and +connects it to `poi-rs` compiled as WebAssembly. + +The generated client uses: + +- protobuf definitions pinned to the same `iota-rust-sdk` revision as the Rust + workspace; +- Protobuf-ES generated messages and service descriptors; +- ConnectRPC's native Node.js gRPC transport over HTTP/2. + +## Schema workflow + +[`grpc/iota-schema.lock.json`](grpc/iota-schema.lock.json) records the approved +repository, exact Git revision and SHA-256 digest of the committed Buf image. +Normal generation does not access the network. + +To intentionally download a different upstream schema: + +```sh +npm run grpc:schema:update -- +``` + +To regenerate the TypeScript client from the committed schema image: + +```sh +npm run grpc:generate +``` + +Review the lock file, Buf image and generated TypeScript changes together. + +## Client creation + +```ts +import { PoiClient } from "@iota/poi-wasm"; + +const mainnet = PoiClient.mainnet(); +const testnet = PoiClient.testnet(); +const devnet = PoiClient.devnet(); +const custom = new PoiClient("http://localhost:9000"); +``` + +No network is selected implicitly. The named constructors use the public IOTA +gRPC endpoints. Construct `PoiClient` with an explicit endpoint for private +nodes, archives, local networks, or alternative endpoints. + +## Proof construction + +```ts +import { PoiClient } from "@iota/poi-wasm"; + +const client = PoiClient.testnet(); +const proof = await client + .proof() + .transaction(transactionDigest) + .build(); + +console.log(proof.toJSON()); +``` + +The same builder also exposes `object(objectId)` and +`event(transactionDigest, eventSequence)`. All 64-bit values use JavaScript +`bigint`. + +The serialized proof records the targets explicitly selected by the caller. +Its checkpoint summary and checkpoint contents are sibling fields, while the +required transaction proof contains the transaction, effects, and optional +event evidence. Object targets contain the selected object values; event +targets contain event IDs whose contents are selected from the authenticated +transaction event list. + +`PoiClient` hides the generated protobuf client, gRPC transport, and +JavaScript/WASM source adapter. The adapter passes only opaque BCS bytes and +checkpoint sequence numbers into WASM. Rust decodes those values into existing +IOTA domain types and delegates target resolution and proof construction to +`poi-rs`. + +## Verification + +```ts +import { CommitteeResolution } from "@iota/poi-wasm"; + +const verifier = client.verifier(CommitteeResolution.trustedNode()); +await verifier.verify(proof); +``` + +The verifier asks the client's node for the committee governing the proof +checkpoint epoch. Rust validates the returned committee representation and +performs proof verification locally with `poi-rs`. + +This mode places the node inside the caller's trust boundary. It does not +authenticate committee lineage from genesis. To authenticate committee +lineage from an already trusted committee: + +```ts +import { readFile } from "node:fs/promises"; + +const trustedGenesisBlob = await readFile("genesis.blob"); +const resolution = CommitteeResolution.fromGenesis(trustedGenesisBlob); +const verifier = client.verifier(resolution); + +await verifier.verify(proof); +``` + +`CommitteeResolution.fromGenesis()` decodes the BCS-encoded IOTA genesis blob +and extracts its committee in Rust. Callers that already possess an extracted +trusted committee can use `CommitteeResolution.anchored(committee)` instead. +`Committee.fromJSON()` accepts the Rust +`Committee` fields `epoch` and `voting_rights`, validates public keys, rejects +duplicate authorities, requires total voting power to equal 10,000, and +reconstructs the committee's derived lookup state. + +The verifier fetches the certified checkpoint in each epoch-close proof, +verifies it with the current committee, and only then accepts and caches the +next committee. The node supplies evidence but is not trusted to choose the +committee. + +Retain the verifier when checking multiple proofs so its authenticated +committee cache is reused. `CommitteeResolver.resolve(epoch)` and +`Proof.verify(committee)` remain available for callers that need the +lower-level committee or offline-verification APIs. + +## Package verification + +```sh +npm install +npm run verify +``` + +Verification regenerates the Node.js protobuf client from the committed schema +image, builds `poi-rs` for `wasm32-unknown-unknown`, type-checks the TypeScript +boundary, and runs the tests. The tests use an in-memory generated service +implementation and do not require a running IOTA node. To query a live +endpoint with the development diagnostic: + +```sh +npm run example:service-info -- https://grpc.testnet.iota.cafe:443 +``` diff --git a/bindings/wasm/poi_wasm/examples/service-info.ts b/bindings/wasm/poi_wasm/examples/service-info.ts new file mode 100644 index 00000000..a7f9e5a2 --- /dev/null +++ b/bindings/wasm/poi_wasm/examples/service-info.ts @@ -0,0 +1,20 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { createIotaGrpcClient } from "../lib/client.js"; + +const endpoint = process.argv[2] ?? "https://grpc.testnet.iota.cafe:443"; +const client = createIotaGrpcClient(endpoint); +const serviceInfo = await client.getServiceInfo({ + readMask: { paths: ["chain_id"] }, +}); +const chainIdentifier = serviceInfo.chainId?.digest; + +if (!chainIdentifier) { + throw new Error("getServiceInfo returned no chain identifier"); +} + +console.log({ + endpoint, + chainIdentifier: Buffer.from(chainIdentifier).toString("hex"), +}); diff --git a/bindings/wasm/poi_wasm/grpc/buf.gen.yaml b/bindings/wasm/poi_wasm/grpc/buf.gen.yaml new file mode 100644 index 00000000..63d2c2c2 --- /dev/null +++ b/bindings/wasm/poi_wasm/grpc/buf.gen.yaml @@ -0,0 +1,13 @@ +version: v2 +clean: true + +plugins: + - local: protoc-gen-es + out: lib/grpc/generated + opt: + - target=ts + - import_extension=js + include_imports: true + +inputs: + - binary_image: grpc/iota-ledger.binpb diff --git a/bindings/wasm/poi_wasm/grpc/iota-ledger.binpb b/bindings/wasm/poi_wasm/grpc/iota-ledger.binpb new file mode 100644 index 00000000..9962a2a2 Binary files /dev/null and b/bindings/wasm/poi_wasm/grpc/iota-ledger.binpb differ diff --git a/bindings/wasm/poi_wasm/grpc/iota-schema.lock.json b/bindings/wasm/poi_wasm/grpc/iota-schema.lock.json new file mode 100644 index 00000000..1adedfb7 --- /dev/null +++ b/bindings/wasm/poi_wasm/grpc/iota-schema.lock.json @@ -0,0 +1,9 @@ +{ + "repository": "https://github.com/iotaledger/iota-rust-sdk", + "revision": "b77fcd5ac5fedb3dfbc77ba7d183140e43512339", + "protoRoot": "crates/iota-sdk-grpc-types/proto", + "entrypoints": [ + "iota/grpc/v1/ledger_service.proto" + ], + "imageSha256": "sha256:8ed56353f07bbf508442cab56c592b2cc34fe39f696f3bc815ee01cff9dd6186" +} diff --git a/bindings/wasm/poi_wasm/lib/client.ts b/bindings/wasm/poi_wasm/lib/client.ts new file mode 100644 index 00000000..527b3a97 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/client.ts @@ -0,0 +1,43 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { createClient, type Client, type Transport } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; + +import { LedgerService } from "./grpc/generated/iota/grpc/v1/ledger_service_pb.js"; + +const DEFAULT_TIMEOUT_MS = 30_000; +const DEFAULT_READ_MAX_BYTES = 128 * 1024 * 1024; + +export type IotaGrpcClient = Client; + +export interface IotaGrpcClientOptions { + defaultTimeoutMs?: number; + readMaxBytes?: number; + transport?: Transport; +} + +/** + * Creates a Node.js gRPC client from the generated IOTA LedgerService + * descriptor. + */ +export function createIotaGrpcClient( + endpoint: string, + options: IotaGrpcClientOptions = {}, +): IotaGrpcClient { + const baseUrl = endpoint.trim().replace(/\/+$/, ""); + + if (!baseUrl) { + throw new Error("IOTA gRPC endpoint must not be empty"); + } + + const transport = + options.transport ?? + createGrpcTransport({ + baseUrl, + defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS, + readMaxBytes: options.readMaxBytes ?? DEFAULT_READ_MAX_BYTES, + }); + + return createClient(LedgerService, transport); +} diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/google/rpc/status_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/google/rpc/status_pb.ts new file mode 100644 index 00000000..e48529c5 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/google/rpc/status_pb.ts @@ -0,0 +1,76 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file google/rpc/status.proto (package google.rpc, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Any } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_any } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file google/rpc/status.proto. + */ +export const file_google_rpc_status: GenFile = /*@__PURE__*/ + fileDesc("Chdnb29nbGUvcnBjL3N0YXR1cy5wcm90bxIKZ29vZ2xlLnJwYyJOCgZTdGF0dXMSDAoEY29kZRgBIAEoBRIPCgdtZXNzYWdlGAIgASgJEiUKB2RldGFpbHMYAyADKAsyFC5nb29nbGUucHJvdG9idWYuQW55QmEKDmNvbS5nb29nbGUucnBjQgtTdGF0dXNQcm90b1ABWjdnb29nbGUuZ29sYW5nLm9yZy9nZW5wcm90by9nb29nbGVhcGlzL3JwYy9zdGF0dXM7c3RhdHVz+AEBogIDUlBDYgZwcm90bzM", [file_google_protobuf_any]); + +/** + * The `Status` type defines a logical error model that is suitable for + * different programming environments, including REST APIs and RPC APIs. It is + * used by [gRPC](https://github.com/grpc). Each `Status` message contains + * three pieces of data: error code, error message, and error details. + * + * You can find out more about this error model and how to work with it in the + * [API Design Guide](https://cloud.google.com/apis/design/errors). + * + * @generated from message google.rpc.Status + */ +export type Status = Message<"google.rpc.Status"> & { + /** + * The status code, which should be an enum value of + * [google.rpc.Code][google.rpc.Code]. + * + * @generated from field: int32 code = 1; + */ + code: number; + + /** + * A developer-facing error message, which should be in English. Any + * user-facing error message should be localized and sent in the + * [google.rpc.Status.details][google.rpc.Status.details] field, or localized + * by the client. + * + * @generated from field: string message = 2; + */ + message: string; + + /** + * A list of messages that carry the error details. There is a common set of + * message types for APIs to use. + * + * @generated from field: repeated google.protobuf.Any details = 3; + */ + details: Any[]; +}; + +/** + * Describes the message google.rpc.Status. + * Use `create(StatusSchema)` to create a new message. + */ +export const StatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_rpc_status, 0); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/options_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/options_pb.ts new file mode 100644 index 00000000..89877de6 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/options_pb.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/options.proto (package iota.grpc, syntax proto3) +/* eslint-disable */ + +import type { GenExtension, GenFile } from "@bufbuild/protobuf/codegenv2"; +import { extDesc, fileDesc } from "@bufbuild/protobuf/codegenv2"; +import type { FieldOptions, MessageOptions } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_descriptor } from "@bufbuild/protobuf/wkt"; + +/** + * Describes the file iota/grpc/options.proto. + */ +export const file_iota_grpc_options: GenFile = /*@__PURE__*/ + fileDesc("Chdpb3RhL2dycGMvb3B0aW9ucy5wcm90bxIJaW90YS5ncnBjOlEKEW1lc3NhZ2VfYWNjZXNzb3JzEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGNCGAyABKAlSEG1lc3NhZ2VBY2Nlc3NvcnOIAQE6WgoWZmllbGRfbWFza190cmFuc3BhcmVudBIfLmdvb2dsZS5wcm90b2J1Zi5NZXNzYWdlT3B0aW9ucxjShgMgASgIUhRmaWVsZE1hc2tUcmFuc3BhcmVudIgBATpLCg9maWVsZF9hY2Nlc3NvcnMSHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGNGGAyABKAlSDmZpZWxkQWNjZXNzb3JziAEBYgZwcm90bzM", [file_google_protobuf_descriptor]); + +/** + * Default accessor types to generate for all fields in this message. + * Valid values: getter, getter_opt, set, with, mut, mut_opt, all + * Example: "with" generates with_field() for all fields + * Field-level field_accessors annotations override this default. + * + * @generated from extension: optional string message_accessors = 50000; + */ +export const message_accessors: GenExtension = /*@__PURE__*/ + extDesc(file_iota_grpc_options, 0); + +/** + * When true, parent fields pointing to this message will skip this wrapper + * layer in field_info (read_mask paths) and FieldPathBuilders. The wrapper + * must contain exactly one repeated or map field. + * + * @generated from extension: optional bool field_mask_transparent = 50002; + */ +export const field_mask_transparent: GenExtension = /*@__PURE__*/ + extDesc(file_iota_grpc_options, 1); + +/** + * Comma-separated list of accessor types to generate for this field. + * Valid values: getter, getter_opt, set, with, mut, mut_opt, all + * Example: "set,with" generates set_field() and with_field() + * Overrides message-level message_accessors option. + * + * @generated from extension: optional string field_accessors = 50001; + */ +export const field_accessors: GenExtension = /*@__PURE__*/ + extDesc(file_iota_grpc_options, 2); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/bcs_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/bcs_pb.ts new file mode 100644 index 00000000..0bee6d34 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/bcs_pb.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/bcs.proto (package iota.grpc.v1.bcs, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/bcs.proto. + */ +export const file_iota_grpc_v1_bcs: GenFile = /*@__PURE__*/ + fileDesc("ChZpb3RhL2dycGMvdjEvYmNzLnByb3RvEhBpb3RhLmdycGMudjEuYmNzIiEKB0Jjc0RhdGESDAoEZGF0YRgBIAEoDDoIgrUYBHdpdGhiBnByb3RvMw", [file_iota_grpc_options]); + +/** + * BCS-serialized data container + * + * @generated from message iota.grpc.v1.bcs.BcsData + */ +export type BcsData = Message<"iota.grpc.v1.bcs.BcsData"> & { + /** + * @generated from field: bytes data = 1; + */ + data: Uint8Array; +}; + +/** + * Describes the message iota.grpc.v1.bcs.BcsData. + * Use `create(BcsDataSchema)` to create a new message. + */ +export const BcsDataSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_bcs, 0); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/checkpoint_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/checkpoint_pb.ts new file mode 100644 index 00000000..900f4c14 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/checkpoint_pb.ts @@ -0,0 +1,140 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/checkpoint.proto (package iota.grpc.v1.checkpoint, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { BcsData } from "./bcs_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { ValidatorAggregatedSignature } from "./signatures_pb.js"; +import { file_iota_grpc_v1_signatures } from "./signatures_pb.js"; +import type { Digest } from "./types_pb.js"; +import { file_iota_grpc_v1_types } from "./types_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/checkpoint.proto. + */ +export const file_iota_grpc_v1_checkpoint: GenFile = /*@__PURE__*/ + fileDesc("Ch1pb3RhL2dycGMvdjEvY2hlY2twb2ludC5wcm90bxIXaW90YS5ncnBjLnYxLmNoZWNrcG9pbnQijgEKEUNoZWNrcG9pbnRTdW1tYXJ5Ei8KBmRpZ2VzdBgBIAEoCzIaLmlvdGEuZ3JwYy52MS50eXBlcy5EaWdlc3RIAIgBARIrCgNiY3MYAiABKAsyGS5pb3RhLmdycGMudjEuYmNzLkJjc0RhdGFIAYgBAToIgrUYBHdpdGhCCQoHX2RpZ2VzdEIGCgRfYmNzIo8BChJDaGVja3BvaW50Q29udGVudHMSLwoGZGlnZXN0GAEgASgLMhouaW90YS5ncnBjLnYxLnR5cGVzLkRpZ2VzdEgAiAEBEisKA2JjcxgCIAEoCzIZLmlvdGEuZ3JwYy52MS5iY3MuQmNzRGF0YUgBiAEBOgiCtRgEd2l0aEIJCgdfZGlnZXN0QgYKBF9iY3MixAIKCkNoZWNrcG9pbnQSHAoPc2VxdWVuY2VfbnVtYmVyGAEgASgESACIAQESQAoHc3VtbWFyeRgCIAEoCzIqLmlvdGEuZ3JwYy52MS5jaGVja3BvaW50LkNoZWNrcG9pbnRTdW1tYXJ5SAGIAQESQgoIY29udGVudHMYAyABKAsyKy5pb3RhLmdycGMudjEuY2hlY2twb2ludC5DaGVja3BvaW50Q29udGVudHNIAogBARJNCglzaWduYXR1cmUYBCABKAsyNS5pb3RhLmdycGMudjEuc2lnbmF0dXJlcy5WYWxpZGF0b3JBZ2dyZWdhdGVkU2lnbmF0dXJlSAOIAQE6CIK1GAR3aXRoQhIKEF9zZXF1ZW5jZV9udW1iZXJCCgoIX3N1bW1hcnlCCwoJX2NvbnRlbnRzQgwKCl9zaWduYXR1cmViBnByb3RvMw", [file_iota_grpc_options, file_iota_grpc_v1_bcs, file_iota_grpc_v1_signatures, file_iota_grpc_v1_types]); + +/** + * A header for a checkpoint on the IOTA blockchain. + * + * On the IOTA network, checkpoints define the history of the blockchain. They are quite similar to + * the concept of blocks used by other blockchains like Bitcoin or Ethereum. The IOTA blockchain, + * however, forms checkpoints after transaction execution has already happened to provide a + * certified history of the chain, instead of being formed before execution. + * + * Checkpoints commit to a variety of state, including but not limited to: + * - The hash of the previous checkpoint. + * - The set of transaction digests, their corresponding effects digests, as well as the set of + * user signatures that authorized its execution. + * - The objects produced by a transaction. + * - The set of live objects that make up the current state of the chain. + * - On epoch transitions, the next validator committee. + * + * `CheckpointSummary`s themselves don't directly include all of the previous information but they + * are the top-level type by which all the information is committed to transitively via cryptographic + * hashes included in the summary. `CheckpointSummary`s are signed and certified by a quorum of + * the validator committee in a given epoch to allow verification of the chain's state. + * + * @generated from message iota.grpc.v1.checkpoint.CheckpointSummary + */ +export type CheckpointSummary = Message<"iota.grpc.v1.checkpoint.CheckpointSummary"> & { + /** + * The digest of this CheckpointSummary. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 1; + */ + digest?: Digest | undefined; + + /** + * This CheckpointSummary serialized as BCS. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 2; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.checkpoint.CheckpointSummary. + * Use `create(CheckpointSummarySchema)` to create a new message. + */ +export const CheckpointSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_checkpoint, 0); + +/** + * The committed to contents of a checkpoint. + * + * @generated from message iota.grpc.v1.checkpoint.CheckpointContents + */ +export type CheckpointContents = Message<"iota.grpc.v1.checkpoint.CheckpointContents"> & { + /** + * The digest of this CheckpointContents. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 1; + */ + digest?: Digest | undefined; + + /** + * This CheckpointContents serialized as BCS. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 2; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.checkpoint.CheckpointContents. + * Use `create(CheckpointContentsSchema)` to create a new message. + */ +export const CheckpointContentsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_checkpoint, 1); + +/** + * @generated from message iota.grpc.v1.checkpoint.Checkpoint + */ +export type Checkpoint = Message<"iota.grpc.v1.checkpoint.Checkpoint"> & { + /** + * The height of this checkpoint. + * + * @generated from field: optional uint64 sequence_number = 1; + */ + sequenceNumber?: bigint | undefined; + + /** + * The `CheckpointSummary` for this checkpoint. + * + * @generated from field: optional iota.grpc.v1.checkpoint.CheckpointSummary summary = 2; + */ + summary?: CheckpointSummary | undefined; + + /** + * The `CheckpointContents` for this checkpoint. + * + * @generated from field: optional iota.grpc.v1.checkpoint.CheckpointContents contents = 3; + */ + contents?: CheckpointContents | undefined; + + /** + * An aggregated quorum signature from the validator committee that + * certified this checkpoint. + * + * @generated from field: optional iota.grpc.v1.signatures.ValidatorAggregatedSignature signature = 4; + */ + signature?: ValidatorAggregatedSignature | undefined; +}; + +/** + * Describes the message iota.grpc.v1.checkpoint.Checkpoint. + * Use `create(CheckpointSchema)` to create a new message. + */ +export const CheckpointSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_checkpoint, 2); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/epoch_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/epoch_pb.ts new file mode 100644 index 00000000..f5467b65 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/epoch_pb.ts @@ -0,0 +1,283 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/epoch.proto (package iota.grpc.v1.epoch, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { BcsData } from "./bcs_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { Checkpoint } from "./checkpoint_pb.js"; +import { file_iota_grpc_v1_checkpoint } from "./checkpoint_pb.js"; +import type { TransactionEffects, TransactionEvents } from "./transaction_pb.js"; +import { file_iota_grpc_v1_transaction } from "./transaction_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/epoch.proto. + */ +export const file_iota_grpc_v1_epoch: GenFile = /*@__PURE__*/ + fileDesc("Chhpb3RhL2dycGMvdjEvZXBvY2gucHJvdG8SEmlvdGEuZ3JwYy52MS5lcG9jaCJsChhWYWxpZGF0b3JDb21taXR0ZWVNZW1iZXISFwoKcHVibGljX2tleRgBIAEoDEgAiAEBEhMKBndlaWdodBgCIAEoBEgBiAEBOgiCtRgEd2l0aEINCgtfcHVibGljX2tleUIJCgdfd2VpZ2h0ImQKGVZhbGlkYXRvckNvbW1pdHRlZU1lbWJlcnMSPQoHbWVtYmVycxgBIAMoCzIsLmlvdGEuZ3JwYy52MS5lcG9jaC5WYWxpZGF0b3JDb21taXR0ZWVNZW1iZXI6CIK1GAR3aXRoIo0BChJWYWxpZGF0b3JDb21taXR0ZWUSEgoFZXBvY2gYASABKARIAIgBARJDCgdtZW1iZXJzGAIgASgLMi0uaW90YS5ncnBjLnYxLmVwb2NoLlZhbGlkYXRvckNvbW1pdHRlZU1lbWJlcnNIAYgBAToIgrUYBHdpdGhCCAoGX2Vwb2NoQgoKCF9tZW1iZXJzIpYBChRQcm90b2NvbEZlYXR1cmVGbGFncxJCCgVmbGFncxgBIAMoCzIzLmlvdGEuZ3JwYy52MS5lcG9jaC5Qcm90b2NvbEZlYXR1cmVGbGFncy5GbGFnc0VudHJ5GiwKCkZsYWdzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgIOgI4AToMgrUYBHdpdGiQtRgBIqEBChJQcm90b2NvbEF0dHJpYnV0ZXMSSgoKYXR0cmlidXRlcxgBIAMoCzI2LmlvdGEuZ3JwYy52MS5lcG9jaC5Qcm90b2NvbEF0dHJpYnV0ZXMuQXR0cmlidXRlc0VudHJ5GjEKD0F0dHJpYnV0ZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBOgyCtRgEd2l0aJC1GAEi9gEKDlByb3RvY29sQ29uZmlnEh0KEHByb3RvY29sX3ZlcnNpb24YASABKARIAIgBARJECg1mZWF0dXJlX2ZsYWdzGAIgASgLMiguaW90YS5ncnBjLnYxLmVwb2NoLlByb3RvY29sRmVhdHVyZUZsYWdzSAGIAQESPwoKYXR0cmlidXRlcxgDIAEoCzImLmlvdGEuZ3JwYy52MS5lcG9jaC5Qcm90b2NvbEF0dHJpYnV0ZXNIAogBAToIgrUYBHdpdGhCEwoRX3Byb3RvY29sX3ZlcnNpb25CEAoOX2ZlYXR1cmVfZmxhZ3NCDQoLX2F0dHJpYnV0ZXMijQUKBUVwb2NoEhIKBWVwb2NoGAEgASgESACIAQESPgoJY29tbWl0dGVlGAIgASgLMiYuaW90YS5ncnBjLnYxLmVwb2NoLlZhbGlkYXRvckNvbW1pdHRlZUgBiAEBEjgKEGJjc19zeXN0ZW1fc3RhdGUYAyABKAsyGS5pb3RhLmdycGMudjEuYmNzLkJjc0RhdGFIAogBARIdChBmaXJzdF9jaGVja3BvaW50GAQgASgESAOIAQESHAoPbGFzdF9jaGVja3BvaW50GAUgASgESASIAQESLgoFc3RhcnQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAWIAQESLAoDZW5kGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgGiAEBEiAKE3JlZmVyZW5jZV9nYXNfcHJpY2UYCCABKARIB4gBARJACg9wcm90b2NvbF9jb25maWcYCSABKAsyIi5pb3RhLmdycGMudjEuZXBvY2guUHJvdG9jb2xDb25maWdICIgBARJDChFlcG9jaF9jbG9zZV9wcm9vZhgKIAEoCzIjLmlvdGEuZ3JwYy52MS5lcG9jaC5FcG9jaENsb3NlUHJvb2ZICYgBAToIgrUYBHdpdGhCCAoGX2Vwb2NoQgwKCl9jb21taXR0ZWVCEwoRX2Jjc19zeXN0ZW1fc3RhdGVCEwoRX2ZpcnN0X2NoZWNrcG9pbnRCEgoQX2xhc3RfY2hlY2twb2ludEIICgZfc3RhcnRCBgoEX2VuZEIWChRfcmVmZXJlbmNlX2dhc19wcmljZUISChBfcHJvdG9jb2xfY29uZmlnQhQKEl9lcG9jaF9jbG9zZV9wcm9vZiKxAwoPRXBvY2hDbG9zZVByb29mEjwKCmNoZWNrcG9pbnQYASABKAsyIy5pb3RhLmdycGMudjEuY2hlY2twb2ludC5DaGVja3BvaW50SACIAQESWwogZW5kX29mX2Vwb2NoX3RyYW5zYWN0aW9uX2VmZmVjdHMYAiABKAsyLC5pb3RhLmdycGMudjEudHJhbnNhY3Rpb24uVHJhbnNhY3Rpb25FZmZlY3RzSAGIAQESWQofZW5kX29mX2Vwb2NoX3RyYW5zYWN0aW9uX2V2ZW50cxgDIAEoCzIrLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5UcmFuc2FjdGlvbkV2ZW50c0gCiAEBEkYKI2Jjc19uZXh0X2Vwb2NoX3N5c3RlbV9zdGF0ZV9vYmplY3RzGAQgAygLMhkuaW90YS5ncnBjLnYxLmJjcy5CY3NEYXRhOgiCtRgEd2l0aEINCgtfY2hlY2twb2ludEIjCiFfZW5kX29mX2Vwb2NoX3RyYW5zYWN0aW9uX2VmZmVjdHNCIgogX2VuZF9vZl9lcG9jaF90cmFuc2FjdGlvbl9ldmVudHNiBnByb3RvMw", [file_google_protobuf_timestamp, file_iota_grpc_options, file_iota_grpc_v1_bcs, file_iota_grpc_v1_checkpoint, file_iota_grpc_v1_transaction]); + +/** + * A member of a validator committee. + * + * @generated from message iota.grpc.v1.epoch.ValidatorCommitteeMember + */ +export type ValidatorCommitteeMember = Message<"iota.grpc.v1.epoch.ValidatorCommitteeMember"> & { + /** + * The 96-byte Bls12381 public key for this validator. + * + * @generated from field: optional bytes public_key = 1; + */ + publicKey?: Uint8Array | undefined; + + /** + * voting weight this validator possesses. + * + * @generated from field: optional uint64 weight = 2; + */ + weight?: bigint | undefined; +}; + +/** + * Describes the message iota.grpc.v1.epoch.ValidatorCommitteeMember. + * Use `create(ValidatorCommitteeMemberSchema)` to create a new message. + */ +export const ValidatorCommitteeMemberSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 0); + +/** + * @generated from message iota.grpc.v1.epoch.ValidatorCommitteeMembers + */ +export type ValidatorCommitteeMembers = Message<"iota.grpc.v1.epoch.ValidatorCommitteeMembers"> & { + /** + * @generated from field: repeated iota.grpc.v1.epoch.ValidatorCommitteeMember members = 1; + */ + members: ValidatorCommitteeMember[]; +}; + +/** + * Describes the message iota.grpc.v1.epoch.ValidatorCommitteeMembers. + * Use `create(ValidatorCommitteeMembersSchema)` to create a new message. + */ +export const ValidatorCommitteeMembersSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 1); + +/** + * The validator set for a particular epoch. + * + * @generated from message iota.grpc.v1.epoch.ValidatorCommittee + */ +export type ValidatorCommittee = Message<"iota.grpc.v1.epoch.ValidatorCommittee"> & { + /** + * The epoch where this committee governs. + * + * @generated from field: optional uint64 epoch = 1; + */ + epoch?: bigint | undefined; + + /** + * The committee members. + * + * @generated from field: optional iota.grpc.v1.epoch.ValidatorCommitteeMembers members = 2; + */ + members?: ValidatorCommitteeMembers | undefined; +}; + +/** + * Describes the message iota.grpc.v1.epoch.ValidatorCommittee. + * Use `create(ValidatorCommitteeSchema)` to create a new message. + */ +export const ValidatorCommitteeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 2); + +/** + * @generated from message iota.grpc.v1.epoch.ProtocolFeatureFlags + */ +export type ProtocolFeatureFlags = Message<"iota.grpc.v1.epoch.ProtocolFeatureFlags"> & { + /** + * @generated from field: map flags = 1; + */ + flags: { [key: string]: boolean }; +}; + +/** + * Describes the message iota.grpc.v1.epoch.ProtocolFeatureFlags. + * Use `create(ProtocolFeatureFlagsSchema)` to create a new message. + */ +export const ProtocolFeatureFlagsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 3); + +/** + * @generated from message iota.grpc.v1.epoch.ProtocolAttributes + */ +export type ProtocolAttributes = Message<"iota.grpc.v1.epoch.ProtocolAttributes"> & { + /** + * @generated from field: map attributes = 1; + */ + attributes: { [key: string]: string }; +}; + +/** + * Describes the message iota.grpc.v1.epoch.ProtocolAttributes. + * Use `create(ProtocolAttributesSchema)` to create a new message. + */ +export const ProtocolAttributesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 4); + +/** + * @generated from message iota.grpc.v1.epoch.ProtocolConfig + */ +export type ProtocolConfig = Message<"iota.grpc.v1.epoch.ProtocolConfig"> & { + /** + * @generated from field: optional uint64 protocol_version = 1; + */ + protocolVersion?: bigint | undefined; + + /** + * @generated from field: optional iota.grpc.v1.epoch.ProtocolFeatureFlags feature_flags = 2; + */ + featureFlags?: ProtocolFeatureFlags | undefined; + + /** + * @generated from field: optional iota.grpc.v1.epoch.ProtocolAttributes attributes = 3; + */ + attributes?: ProtocolAttributes | undefined; +}; + +/** + * Describes the message iota.grpc.v1.epoch.ProtocolConfig. + * Use `create(ProtocolConfigSchema)` to create a new message. + */ +export const ProtocolConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 5); + +/** + * @generated from message iota.grpc.v1.epoch.Epoch + */ +export type Epoch = Message<"iota.grpc.v1.epoch.Epoch"> & { + /** + * @generated from field: optional uint64 epoch = 1; + */ + epoch?: bigint | undefined; + + /** + * The committee governing this epoch. + * + * @generated from field: optional iota.grpc.v1.epoch.ValidatorCommittee committee = 2; + */ + committee?: ValidatorCommittee | undefined; + + /** + * Snapshot of IOTA's SystemState (`0x3::iota_system::SystemState`) at the + * beginning of the epoch, for past epochs, or the current state for the + * current epoch. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs_system_state = 3; + */ + bcsSystemState?: BcsData | undefined; + + /** + * @generated from field: optional uint64 first_checkpoint = 4; + */ + firstCheckpoint?: bigint | undefined; + + /** + * @generated from field: optional uint64 last_checkpoint = 5; + */ + lastCheckpoint?: bigint | undefined; + + /** + * @generated from field: optional google.protobuf.Timestamp start = 6; + */ + start?: Timestamp | undefined; + + /** + * @generated from field: optional google.protobuf.Timestamp end = 7; + */ + end?: Timestamp | undefined; + + /** + * Reference gas price denominated in NANOS + * + * @generated from field: optional uint64 reference_gas_price = 8; + */ + referenceGasPrice?: bigint | undefined; + + /** + * @generated from field: optional iota.grpc.v1.epoch.ProtocolConfig protocol_config = 9; + */ + protocolConfig?: ProtocolConfig | undefined; + + /** + * Proof of how this epoch closed: the certified closing checkpoint, the + * epoch-change transaction's effects and events, and the system-state + * objects it wrote for the next epoch's start state. + * + * Absent until the epoch closes. + * + * @generated from field: optional iota.grpc.v1.epoch.EpochCloseProof epoch_close_proof = 10; + */ + epochCloseProof?: EpochCloseProof | undefined; +}; + +/** + * Describes the message iota.grpc.v1.epoch.Epoch. + * Use `create(EpochSchema)` to create a new message. + */ +export const EpochSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 6); + +/** + * @generated from message iota.grpc.v1.epoch.EpochCloseProof + */ +export type EpochCloseProof = Message<"iota.grpc.v1.epoch.EpochCloseProof"> & { + /** + * The certified checkpoint that closed the epoch. + * + * @generated from field: optional iota.grpc.v1.checkpoint.Checkpoint checkpoint = 1; + */ + checkpoint?: Checkpoint | undefined; + + /** + * Effects of the epoch-change transaction (the last transaction of the + * closing checkpoint). + * + * @generated from field: optional iota.grpc.v1.transaction.TransactionEffects end_of_epoch_transaction_effects = 2; + */ + endOfEpochTransactionEffects?: TransactionEffects | undefined; + + /** + * Events emitted by the epoch-change transaction. Empty on safe-mode + * boundaries, which mutate the system state without emitting events. + * + * @generated from field: optional iota.grpc.v1.transaction.TransactionEvents end_of_epoch_transaction_events = 3; + */ + endOfEpochTransactionEvents?: TransactionEvents | undefined; + + /** + * Raw BCS bytes of the system-state wrapper object (`0x5`) and its inner + * state object, as written by this epoch boundary — byte-for-byte as + * originally written (not wrapped, unlike `Object.bcs` elsewhere in this + * API), so their digests can be verified against the written-object + * digests in `end_of_epoch_transaction_effects`. + * + * @generated from field: repeated iota.grpc.v1.bcs.BcsData bcs_next_epoch_system_state_objects = 4; + */ + bcsNextEpochSystemStateObjects: BcsData[]; +}; + +/** + * Describes the message iota.grpc.v1.epoch.EpochCloseProof. + * Use `create(EpochCloseProofSchema)` to create a new message. + */ +export const EpochCloseProofSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_epoch, 7); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/event_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/event_pb.ts new file mode 100644 index 00000000..e779fa85 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/event_pb.ts @@ -0,0 +1,109 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/event.proto (package iota.grpc.v1.event, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Value } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_struct } from "@bufbuild/protobuf/wkt"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { BcsData } from "./bcs_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { Address, ObjectId } from "./types_pb.js"; +import { file_iota_grpc_v1_types } from "./types_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/event.proto. + */ +export const file_iota_grpc_v1_event: GenFile = /*@__PURE__*/ + fileDesc("Chhpb3RhL2dycGMvdjEvZXZlbnQucHJvdG8SEmlvdGEuZ3JwYy52MS5ldmVudCKeAwoFRXZlbnQSKwoDYmNzGAEgASgLMhkuaW90YS5ncnBjLnYxLmJjcy5CY3NEYXRhSACIAQESNQoKcGFja2FnZV9pZBgCIAEoCzIcLmlvdGEuZ3JwYy52MS50eXBlcy5PYmplY3RJZEgBiAEBEhMKBm1vZHVsZRgDIAEoCUgCiAEBEjAKBnNlbmRlchgEIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5BZGRyZXNzSAOIAQESFwoKZXZlbnRfdHlwZRgFIAEoCUgEiAEBEjQKDGJjc19jb250ZW50cxgGIAEoCzIZLmlvdGEuZ3JwYy52MS5iY3MuQmNzRGF0YUgFiAEBEjIKDWpzb25fY29udGVudHMYByABKAsyFi5nb29nbGUucHJvdG9idWYuVmFsdWVIBogBAToIgrUYBHdpdGhCBgoEX2Jjc0INCgtfcGFja2FnZV9pZEIJCgdfbW9kdWxlQgkKB19zZW5kZXJCDQoLX2V2ZW50X3R5cGVCDwoNX2Jjc19jb250ZW50c0IQCg5fanNvbl9jb250ZW50cyJBCgZFdmVudHMSKQoGZXZlbnRzGAEgAygLMhkuaW90YS5ncnBjLnYxLmV2ZW50LkV2ZW50OgyCtRgEd2l0aJC1GAFiBnByb3RvMw", [file_google_protobuf_struct, file_iota_grpc_options, file_iota_grpc_v1_bcs, file_iota_grpc_v1_types]); + +/** + * An event. + * + * @generated from message iota.grpc.v1.event.Event + */ +export type Event = Message<"iota.grpc.v1.event.Event"> & { + /** + * This Event serialized as BCS. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 1; + */ + bcs?: BcsData | undefined; + + /** + * Package ID of the top-level function invoked by a `MoveCall` command that triggered this + * event to be emitted. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId package_id = 2; + */ + packageId?: ObjectId | undefined; + + /** + * Module name of the top-level function invoked by a `MoveCall` command that triggered this + * event to be emitted. + * + * @generated from field: optional string module = 3; + */ + module?: string | undefined; + + /** + * Address of the account that sent the transaction where this event was emitted. + * + * @generated from field: optional iota.grpc.v1.types.Address sender = 4; + */ + sender?: Address | undefined; + + /** + * The type of the event emitted. + * + * @generated from field: optional string event_type = 5; + */ + eventType?: string | undefined; + + /** + * BCS serialized bytes of the event. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs_contents = 6; + */ + bcsContents?: BcsData | undefined; + + /** + * JSON rendering of the event. + * + * @generated from field: optional google.protobuf.Value json_contents = 7; + */ + jsonContents?: Value | undefined; +}; + +/** + * Describes the message iota.grpc.v1.event.Event. + * Use `create(EventSchema)` to create a new message. + */ +export const EventSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_event, 0); + +/** + * A list of events. + * + * @generated from message iota.grpc.v1.event.Events + */ +export type Events = Message<"iota.grpc.v1.event.Events"> & { + /** + * @generated from field: repeated iota.grpc.v1.event.Event events = 1; + */ + events: Event[]; +}; + +/** + * Describes the message iota.grpc.v1.event.Events. + * Use `create(EventsSchema)` to create a new message. + */ +export const EventsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_event, 1); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/filter_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/filter_pb.ts new file mode 100644 index 00000000..df7e03fe --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/filter_pb.ts @@ -0,0 +1,668 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/filter.proto (package iota.grpc.v1.filter, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_iota_grpc_options } from "../options_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { Address, ObjectId, ObjectReference } from "./types_pb.js"; +import { file_iota_grpc_v1_types } from "./types_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/filter.proto. + */ +export const file_iota_grpc_v1_filter: GenFile = /*@__PURE__*/ + fileDesc("Chlpb3RhL2dycGMvdjEvZmlsdGVyLnByb3RvEhNpb3RhLmdycGMudjEuZmlsdGVyIk0KDkFsbEV2ZW50RmlsdGVyEjEKB2ZpbHRlcnMYASADKAsyIC5pb3RhLmdycGMudjEuZmlsdGVyLkV2ZW50RmlsdGVyOgiCtRgEd2l0aCJNCg5BbnlFdmVudEZpbHRlchIxCgdmaWx0ZXJzGAEgAygLMiAuaW90YS5ncnBjLnYxLmZpbHRlci5FdmVudEZpbHRlcjoIgrUYBHdpdGgiTAoOTm90RXZlbnRGaWx0ZXISMAoGZmlsdGVyGAEgASgLMiAuaW90YS5ncnBjLnYxLmZpbHRlci5FdmVudEZpbHRlcjoIgrUYBHdpdGgiRwoNQWRkcmVzc0ZpbHRlchIsCgdhZGRyZXNzGAEgASgLMhsuaW90YS5ncnBjLnYxLnR5cGVzLkFkZHJlc3M6CIK1GAR3aXRoIngKGk1vdmVQYWNrYWdlQW5kTW9kdWxlRmlsdGVyEjAKCnBhY2thZ2VfaWQYASABKAsyHC5pb3RhLmdycGMudjEudHlwZXMuT2JqZWN0SWQSEwoGbW9kdWxlGAIgASgJSACIAQE6CIK1GAR3aXRoQgkKB19tb2R1bGUiMwoTTW92ZUV2ZW50VHlwZUZpbHRlchISCgpzdHJ1Y3RfdGFnGAEgASgJOgiCtRgEd2l0aCLrAwoLRXZlbnRGaWx0ZXISMgoDYWxsGAEgASgLMiMuaW90YS5ncnBjLnYxLmZpbHRlci5BbGxFdmVudEZpbHRlckgAEjIKA2FueRgCIAEoCzIjLmlvdGEuZ3JwYy52MS5maWx0ZXIuQW55RXZlbnRGaWx0ZXJIABI3CghuZWdhdGlvbhgDIAEoCzIjLmlvdGEuZ3JwYy52MS5maWx0ZXIuTm90RXZlbnRGaWx0ZXJIABI0CgZzZW5kZXIYBCABKAsyIi5pb3RhLmdycGMudjEuZmlsdGVyLkFkZHJlc3NGaWx0ZXJIABJSChdtb3ZlX3BhY2thZ2VfYW5kX21vZHVsZRgFIAEoCzIvLmlvdGEuZ3JwYy52MS5maWx0ZXIuTW92ZVBhY2thZ2VBbmRNb2R1bGVGaWx0ZXJIABJYCh1tb3ZlX2V2ZW50X3BhY2thZ2VfYW5kX21vZHVsZRgGIAEoCzIvLmlvdGEuZ3JwYy52MS5maWx0ZXIuTW92ZVBhY2thZ2VBbmRNb2R1bGVGaWx0ZXJIABJDCg9tb3ZlX2V2ZW50X3R5cGUYByABKAsyKC5pb3RhLmdycGMudjEuZmlsdGVyLk1vdmVFdmVudFR5cGVGaWx0ZXJIADoIgrUYBHdpdGhCCAoGZmlsdGVyIlkKFEFsbFRyYW5zYWN0aW9uRmlsdGVyEjcKB2ZpbHRlcnMYASADKAsyJi5pb3RhLmdycGMudjEuZmlsdGVyLlRyYW5zYWN0aW9uRmlsdGVyOgiCtRgEd2l0aCJZChRBbnlUcmFuc2FjdGlvbkZpbHRlchI3CgdmaWx0ZXJzGAEgAygLMiYuaW90YS5ncnBjLnYxLmZpbHRlci5UcmFuc2FjdGlvbkZpbHRlcjoIgrUYBHdpdGgiWAoUTm90VHJhbnNhY3Rpb25GaWx0ZXISNgoGZmlsdGVyGAEgASgLMiYuaW90YS5ncnBjLnYxLmZpbHRlci5UcmFuc2FjdGlvbkZpbHRlcjoIgrUYBHdpdGgiVwoWVHJhbnNhY3Rpb25LaW5kc0ZpbHRlchIzCgVraW5kcxgBIAMoDjIkLmlvdGEuZ3JwYy52MS5maWx0ZXIuVHJhbnNhY3Rpb25LaW5kOgiCtRgEd2l0aCJTCg5PYmplY3RJZEZpbHRlchI3CgpvYmplY3RfcmVmGAEgASgLMiMuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdFJlZmVyZW5jZToIgrUYBHdpdGgilwEKFU1vdmVDYWxsQ29tbWFuZEZpbHRlchIwCgpwYWNrYWdlX2lkGAEgASgLMhwuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdElkEhMKBm1vZHVsZRgCIAEoCUgAiAEBEhUKCGZ1bmN0aW9uGAMgASgJSAGIAQE6CIK1GAR3aXRoQgkKB19tb2R1bGVCCwoJX2Z1bmN0aW9uIigKHFRyYW5zZmVyT2JqZWN0c0NvbW1hbmRGaWx0ZXI6CIK1GAR3aXRoIiMKF1NwbGl0Q29pbnNDb21tYW5kRmlsdGVyOgiCtRgEd2l0aCIjChdNZXJnZUNvaW5zQ29tbWFuZEZpbHRlcjoIgrUYBHdpdGgiIAoUUHVibGlzaENvbW1hbmRGaWx0ZXI6CIK1GAR3aXRoIiQKGE1ha2VNb3ZlVmVjQ29tbWFuZEZpbHRlcjoIgrUYBHdpdGgiZgoUVXBncmFkZUNvbW1hbmRGaWx0ZXISNQoKcGFja2FnZV9pZBgBIAEoCzIcLmlvdGEuZ3JwYy52MS50eXBlcy5PYmplY3RJZEgAiAEBOgiCtRgEd2l0aEINCgtfcGFja2FnZV9pZCKBBAoNQ29tbWFuZEZpbHRlchI/Cgltb3ZlX2NhbGwYASABKAsyKi5pb3RhLmdycGMudjEuZmlsdGVyLk1vdmVDYWxsQ29tbWFuZEZpbHRlckgAEk0KEHRyYW5zZmVyX29iamVjdHMYAiABKAsyMS5pb3RhLmdycGMudjEuZmlsdGVyLlRyYW5zZmVyT2JqZWN0c0NvbW1hbmRGaWx0ZXJIABJDCgtzcGxpdF9jb2lucxgDIAEoCzIsLmlvdGEuZ3JwYy52MS5maWx0ZXIuU3BsaXRDb2luc0NvbW1hbmRGaWx0ZXJIABJDCgttZXJnZV9jb2lucxgEIAEoCzIsLmlvdGEuZ3JwYy52MS5maWx0ZXIuTWVyZ2VDb2luc0NvbW1hbmRGaWx0ZXJIABI8CgdwdWJsaXNoGAUgASgLMikuaW90YS5ncnBjLnYxLmZpbHRlci5QdWJsaXNoQ29tbWFuZEZpbHRlckgAEkYKDW1ha2VfbW92ZV92ZWMYBiABKAsyLS5pb3RhLmdycGMudjEuZmlsdGVyLk1ha2VNb3ZlVmVjQ29tbWFuZEZpbHRlckgAEjwKB3VwZ3JhZGUYByABKAsyKS5pb3RhLmdycGMudjEuZmlsdGVyLlVwZ3JhZGVDb21tYW5kRmlsdGVySAA6CIK1GAR3aXRoQggKBmZpbHRlciIyChVFeGVjdXRpb25TdGF0dXNGaWx0ZXISDwoHc3VjY2VzcxgBIAEoCDoIgrUYBHdpdGgihAUKEVRyYW5zYWN0aW9uRmlsdGVyEjgKA2FsbBgBIAEoCzIpLmlvdGEuZ3JwYy52MS5maWx0ZXIuQWxsVHJhbnNhY3Rpb25GaWx0ZXJIABI4CgNhbnkYAiABKAsyKS5pb3RhLmdycGMudjEuZmlsdGVyLkFueVRyYW5zYWN0aW9uRmlsdGVySAASPQoIbmVnYXRpb24YAyABKAsyKS5pb3RhLmdycGMudjEuZmlsdGVyLk5vdFRyYW5zYWN0aW9uRmlsdGVySAASSAoRdHJhbnNhY3Rpb25fa2luZHMYBCABKAsyKy5pb3RhLmdycGMudjEuZmlsdGVyLlRyYW5zYWN0aW9uS2luZHNGaWx0ZXJIABJGChBleGVjdXRpb25fc3RhdHVzGAUgASgLMiouaW90YS5ncnBjLnYxLmZpbHRlci5FeGVjdXRpb25TdGF0dXNGaWx0ZXJIABI0CgZzZW5kZXIYBiABKAsyIi5pb3RhLmdycGMudjEuZmlsdGVyLkFkZHJlc3NGaWx0ZXJIABI2CghyZWNlaXZlchgHIAEoCzIiLmlvdGEuZ3JwYy52MS5maWx0ZXIuQWRkcmVzc0ZpbHRlckgAEj4KD2FmZmVjdGVkX29iamVjdBgIIAEoCzIjLmlvdGEuZ3JwYy52MS5maWx0ZXIuT2JqZWN0SWRGaWx0ZXJIABI1Cgdjb21tYW5kGAkgASgLMiIuaW90YS5ncnBjLnYxLmZpbHRlci5Db21tYW5kRmlsdGVySAASMQoFZXZlbnQYCiABKAsyIC5pb3RhLmdycGMudjEuZmlsdGVyLkV2ZW50RmlsdGVySAA6CIK1GAR3aXRoQggKBmZpbHRlciqNAQoPVHJhbnNhY3Rpb25LaW5kEgoKBlNZU1RFTRAAEhAKDFBST0dSQU1NQUJMRRABEgsKB0dFTkVTSVMQAhIgChxDT05TRU5TVVNfQ09NTUlUX1BST0xPR1VFX1YxEAMSEAoMRU5EX09GX0VQT0NIEAUSGwoXUkFORE9NTkVTU19TVEFURV9VUERBVEUQBmIGcHJvdG8z", [file_iota_grpc_options, file_iota_grpc_v1_bcs, file_iota_grpc_v1_types]); + +/** + * Logical AND of several filters. + * + * @generated from message iota.grpc.v1.filter.AllEventFilter + */ +export type AllEventFilter = Message<"iota.grpc.v1.filter.AllEventFilter"> & { + /** + * @generated from field: repeated iota.grpc.v1.filter.EventFilter filters = 1; + */ + filters: EventFilter[]; +}; + +/** + * Describes the message iota.grpc.v1.filter.AllEventFilter. + * Use `create(AllEventFilterSchema)` to create a new message. + */ +export const AllEventFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 0); + +/** + * Logical OR of several filters. + * + * @generated from message iota.grpc.v1.filter.AnyEventFilter + */ +export type AnyEventFilter = Message<"iota.grpc.v1.filter.AnyEventFilter"> & { + /** + * @generated from field: repeated iota.grpc.v1.filter.EventFilter filters = 1; + */ + filters: EventFilter[]; +}; + +/** + * Describes the message iota.grpc.v1.filter.AnyEventFilter. + * Use `create(AnyEventFilterSchema)` to create a new message. + */ +export const AnyEventFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 1); + +/** + * Logical NOT of a filter. + * + * @generated from message iota.grpc.v1.filter.NotEventFilter + */ +export type NotEventFilter = Message<"iota.grpc.v1.filter.NotEventFilter"> & { + /** + * @generated from field: iota.grpc.v1.filter.EventFilter filter = 1; + */ + filter?: EventFilter | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.NotEventFilter. + * Use `create(NotEventFilterSchema)` to create a new message. + */ +export const NotEventFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 2); + +/** + * Filter by address. + * + * @generated from message iota.grpc.v1.filter.AddressFilter + */ +export type AddressFilter = Message<"iota.grpc.v1.filter.AddressFilter"> & { + /** + * @generated from field: iota.grpc.v1.types.Address address = 1; + */ + address?: Address | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.AddressFilter. + * Use `create(AddressFilterSchema)` to create a new message. + */ +export const AddressFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 3); + +/** + * Filter by Move package + module (optional). + * + * @generated from message iota.grpc.v1.filter.MovePackageAndModuleFilter + */ +export type MovePackageAndModuleFilter = Message<"iota.grpc.v1.filter.MovePackageAndModuleFilter"> & { + /** + * @generated from field: iota.grpc.v1.types.ObjectId package_id = 1; + */ + packageId?: ObjectId | undefined; + + /** + * @generated from field: optional string module = 2; + */ + module?: string | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.MovePackageAndModuleFilter. + * Use `create(MovePackageAndModuleFilterSchema)` to create a new message. + */ +export const MovePackageAndModuleFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 4); + +/** + * Filter by the given Move event struct name (struct tag). + * For example, if the event is defined in `0xabcd::MyModule`, and named + * `Foo`, then the struct tag is `0xabcd::MyModule::Foo`. + * + * @generated from message iota.grpc.v1.filter.MoveEventTypeFilter + */ +export type MoveEventTypeFilter = Message<"iota.grpc.v1.filter.MoveEventTypeFilter"> & { + /** + * @generated from field: string struct_tag = 1; + */ + structTag: string; +}; + +/** + * Describes the message iota.grpc.v1.filter.MoveEventTypeFilter. + * Use `create(MoveEventTypeFilterSchema)` to create a new message. + */ +export const MoveEventTypeFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 5); + +/** + * Filter for events. + * + * @generated from message iota.grpc.v1.filter.EventFilter + */ +export type EventFilter = Message<"iota.grpc.v1.filter.EventFilter"> & { + /** + * @generated from oneof iota.grpc.v1.filter.EventFilter.filter + */ + filter: { + /** + * Logical AND of several filters. + * + * @generated from field: iota.grpc.v1.filter.AllEventFilter all = 1; + */ + value: AllEventFilter; + case: "all"; + } | { + /** + * Logical OR of several filters. + * + * @generated from field: iota.grpc.v1.filter.AnyEventFilter any = 2; + */ + value: AnyEventFilter; + case: "any"; + } | { + /** + * Logical NOT of a filter. + * + * @generated from field: iota.grpc.v1.filter.NotEventFilter negation = 3; + */ + value: NotEventFilter; + case: "negation"; + } | { + /** + * Filter by sender address. + * + * @generated from field: iota.grpc.v1.filter.AddressFilter sender = 4; + */ + value: AddressFilter; + case: "sender"; + } | { + /** + * Return events emitted in a specified Move package + module (optional). + * If the event is defined in PackageA::ModuleA but emitted in a tx with PackageB::ModuleB, + * filtering `MovePackageAndModule` by PackageB::ModuleB returns the event. + * Filtering `MoveEventPackageAndModule` by PackageA::ModuleA returns the event too. + * + * @generated from field: iota.grpc.v1.filter.MovePackageAndModuleFilter move_package_and_module = 5; + */ + value: MovePackageAndModuleFilter; + case: "movePackageAndModule"; + } | { + /** + * Return events with the given Move package + module (optional) where the event struct is + * defined. If the event is defined in PackageA::ModuleA but emitted in a tx + * with PackageB::ModuleB, filtering `MoveEventPackageAndModule` by PackageA::ModuleA returns the + * event. Filtering `MovePackageAndModule` by PackageB::ModuleB returns the event too. + * + * @generated from field: iota.grpc.v1.filter.MovePackageAndModuleFilter move_event_package_and_module = 6; + */ + value: MovePackageAndModuleFilter; + case: "moveEventPackageAndModule"; + } | { + /** + * Return events with the given Move event struct name (struct tag). + * For example, if the event is defined in `0xabcd::MyModule`, and named + * `Foo`, then the struct tag is `0xabcd::MyModule::Foo`. + * + * @generated from field: iota.grpc.v1.filter.MoveEventTypeFilter move_event_type = 7; + */ + value: MoveEventTypeFilter; + case: "moveEventType"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.filter.EventFilter. + * Use `create(EventFilterSchema)` to create a new message. + */ +export const EventFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 6); + +/** + * Logical AND of several filters. + * + * @generated from message iota.grpc.v1.filter.AllTransactionFilter + */ +export type AllTransactionFilter = Message<"iota.grpc.v1.filter.AllTransactionFilter"> & { + /** + * @generated from field: repeated iota.grpc.v1.filter.TransactionFilter filters = 1; + */ + filters: TransactionFilter[]; +}; + +/** + * Describes the message iota.grpc.v1.filter.AllTransactionFilter. + * Use `create(AllTransactionFilterSchema)` to create a new message. + */ +export const AllTransactionFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 7); + +/** + * Logical OR of several filters. + * + * @generated from message iota.grpc.v1.filter.AnyTransactionFilter + */ +export type AnyTransactionFilter = Message<"iota.grpc.v1.filter.AnyTransactionFilter"> & { + /** + * @generated from field: repeated iota.grpc.v1.filter.TransactionFilter filters = 1; + */ + filters: TransactionFilter[]; +}; + +/** + * Describes the message iota.grpc.v1.filter.AnyTransactionFilter. + * Use `create(AnyTransactionFilterSchema)` to create a new message. + */ +export const AnyTransactionFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 8); + +/** + * Logical NOT of a filter. + * + * @generated from message iota.grpc.v1.filter.NotTransactionFilter + */ +export type NotTransactionFilter = Message<"iota.grpc.v1.filter.NotTransactionFilter"> & { + /** + * @generated from field: iota.grpc.v1.filter.TransactionFilter filter = 1; + */ + filter?: TransactionFilter | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.NotTransactionFilter. + * Use `create(NotTransactionFilterSchema)` to create a new message. + */ +export const NotTransactionFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 9); + +/** + * Filter by transaction kinds (any of the specified kinds). + * + * @generated from message iota.grpc.v1.filter.TransactionKindsFilter + */ +export type TransactionKindsFilter = Message<"iota.grpc.v1.filter.TransactionKindsFilter"> & { + /** + * @generated from field: repeated iota.grpc.v1.filter.TransactionKind kinds = 1; + */ + kinds: TransactionKind[]; +}; + +/** + * Describes the message iota.grpc.v1.filter.TransactionKindsFilter. + * Use `create(TransactionKindsFilterSchema)` to create a new message. + */ +export const TransactionKindsFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 10); + +/** + * Filter by input object ID. + * + * @generated from message iota.grpc.v1.filter.ObjectIdFilter + */ +export type ObjectIdFilter = Message<"iota.grpc.v1.filter.ObjectIdFilter"> & { + /** + * @generated from field: iota.grpc.v1.types.ObjectReference object_ref = 1; + */ + objectRef?: ObjectReference | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.ObjectIdFilter. + * Use `create(ObjectIdFilterSchema)` to create a new message. + */ +export const ObjectIdFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 11); + +/** + * Filter by move package, module (optional) and function (optional). + * + * @generated from message iota.grpc.v1.filter.MoveCallCommandFilter + */ +export type MoveCallCommandFilter = Message<"iota.grpc.v1.filter.MoveCallCommandFilter"> & { + /** + * @generated from field: iota.grpc.v1.types.ObjectId package_id = 1; + */ + packageId?: ObjectId | undefined; + + /** + * @generated from field: optional string module = 2; + */ + module?: string | undefined; + + /** + * @generated from field: optional string function = 3; + */ + function?: string | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.MoveCallCommandFilter. + * Use `create(MoveCallCommandFilterSchema)` to create a new message. + */ +export const MoveCallCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 12); + +/** + * Match a TransferObjects command. + * + * @generated from message iota.grpc.v1.filter.TransferObjectsCommandFilter + */ +export type TransferObjectsCommandFilter = Message<"iota.grpc.v1.filter.TransferObjectsCommandFilter"> & { +}; + +/** + * Describes the message iota.grpc.v1.filter.TransferObjectsCommandFilter. + * Use `create(TransferObjectsCommandFilterSchema)` to create a new message. + */ +export const TransferObjectsCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 13); + +/** + * Match a SplitCoins command. + * + * @generated from message iota.grpc.v1.filter.SplitCoinsCommandFilter + */ +export type SplitCoinsCommandFilter = Message<"iota.grpc.v1.filter.SplitCoinsCommandFilter"> & { +}; + +/** + * Describes the message iota.grpc.v1.filter.SplitCoinsCommandFilter. + * Use `create(SplitCoinsCommandFilterSchema)` to create a new message. + */ +export const SplitCoinsCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 14); + +/** + * Match a MergeCoins command. + * + * @generated from message iota.grpc.v1.filter.MergeCoinsCommandFilter + */ +export type MergeCoinsCommandFilter = Message<"iota.grpc.v1.filter.MergeCoinsCommandFilter"> & { +}; + +/** + * Describes the message iota.grpc.v1.filter.MergeCoinsCommandFilter. + * Use `create(MergeCoinsCommandFilterSchema)` to create a new message. + */ +export const MergeCoinsCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 15); + +/** + * Match a Publish command. + * + * @generated from message iota.grpc.v1.filter.PublishCommandFilter + */ +export type PublishCommandFilter = Message<"iota.grpc.v1.filter.PublishCommandFilter"> & { +}; + +/** + * Describes the message iota.grpc.v1.filter.PublishCommandFilter. + * Use `create(PublishCommandFilterSchema)` to create a new message. + */ +export const PublishCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 16); + +/** + * Match a MakeMoveVec command. + * + * @generated from message iota.grpc.v1.filter.MakeMoveVecCommandFilter + */ +export type MakeMoveVecCommandFilter = Message<"iota.grpc.v1.filter.MakeMoveVecCommandFilter"> & { +}; + +/** + * Describes the message iota.grpc.v1.filter.MakeMoveVecCommandFilter. + * Use `create(MakeMoveVecCommandFilterSchema)` to create a new message. + */ +export const MakeMoveVecCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 17); + +/** + * Match an Upgrade command. Optionally filter by the specific package being upgraded. + * + * @generated from message iota.grpc.v1.filter.UpgradeCommandFilter + */ +export type UpgradeCommandFilter = Message<"iota.grpc.v1.filter.UpgradeCommandFilter"> & { + /** + * @generated from field: optional iota.grpc.v1.types.ObjectId package_id = 1; + */ + packageId?: ObjectId | undefined; +}; + +/** + * Describes the message iota.grpc.v1.filter.UpgradeCommandFilter. + * Use `create(UpgradeCommandFilterSchema)` to create a new message. + */ +export const UpgradeCommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 18); + +/** + * Filter by command type. + * + * @generated from message iota.grpc.v1.filter.CommandFilter + */ +export type CommandFilter = Message<"iota.grpc.v1.filter.CommandFilter"> & { + /** + * @generated from oneof iota.grpc.v1.filter.CommandFilter.filter + */ + filter: { + /** + * @generated from field: iota.grpc.v1.filter.MoveCallCommandFilter move_call = 1; + */ + value: MoveCallCommandFilter; + case: "moveCall"; + } | { + /** + * @generated from field: iota.grpc.v1.filter.TransferObjectsCommandFilter transfer_objects = 2; + */ + value: TransferObjectsCommandFilter; + case: "transferObjects"; + } | { + /** + * @generated from field: iota.grpc.v1.filter.SplitCoinsCommandFilter split_coins = 3; + */ + value: SplitCoinsCommandFilter; + case: "splitCoins"; + } | { + /** + * @generated from field: iota.grpc.v1.filter.MergeCoinsCommandFilter merge_coins = 4; + */ + value: MergeCoinsCommandFilter; + case: "mergeCoins"; + } | { + /** + * @generated from field: iota.grpc.v1.filter.PublishCommandFilter publish = 5; + */ + value: PublishCommandFilter; + case: "publish"; + } | { + /** + * @generated from field: iota.grpc.v1.filter.MakeMoveVecCommandFilter make_move_vec = 6; + */ + value: MakeMoveVecCommandFilter; + case: "makeMoveVec"; + } | { + /** + * @generated from field: iota.grpc.v1.filter.UpgradeCommandFilter upgrade = 7; + */ + value: UpgradeCommandFilter; + case: "upgrade"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.filter.CommandFilter. + * Use `create(CommandFilterSchema)` to create a new message. + */ +export const CommandFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 19); + +/** + * Filter by transaction execution status. + * Set `success` to `true` to match successful transactions, + * or `false` to match failed transactions (cancelled, execution error, etc.). + * + * @generated from message iota.grpc.v1.filter.ExecutionStatusFilter + */ +export type ExecutionStatusFilter = Message<"iota.grpc.v1.filter.ExecutionStatusFilter"> & { + /** + * @generated from field: bool success = 1; + */ + success: boolean; +}; + +/** + * Describes the message iota.grpc.v1.filter.ExecutionStatusFilter. + * Use `create(ExecutionStatusFilterSchema)` to create a new message. + */ +export const ExecutionStatusFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 20); + +/** + * Filter for transactions. + * + * @generated from message iota.grpc.v1.filter.TransactionFilter + */ +export type TransactionFilter = Message<"iota.grpc.v1.filter.TransactionFilter"> & { + /** + * @generated from oneof iota.grpc.v1.filter.TransactionFilter.filter + */ + filter: { + /** + * Logical AND of several filters. + * + * @generated from field: iota.grpc.v1.filter.AllTransactionFilter all = 1; + */ + value: AllTransactionFilter; + case: "all"; + } | { + /** + * Logical OR of several filters. + * + * @generated from field: iota.grpc.v1.filter.AnyTransactionFilter any = 2; + */ + value: AnyTransactionFilter; + case: "any"; + } | { + /** + * Logical NOT of a filter. + * + * @generated from field: iota.grpc.v1.filter.NotTransactionFilter negation = 3; + */ + value: NotTransactionFilter; + case: "negation"; + } | { + /** + * Filter transactions of any given kind in the filter. + * + * @generated from field: iota.grpc.v1.filter.TransactionKindsFilter transaction_kinds = 4; + */ + value: TransactionKindsFilter; + case: "transactionKinds"; + } | { + /** + * Filter by transaction execution success/failure. + * + * @generated from field: iota.grpc.v1.filter.ExecutionStatusFilter execution_status = 5; + */ + value: ExecutionStatusFilter; + case: "executionStatus"; + } | { + /** + * Filter by sender address. + * + * @generated from field: iota.grpc.v1.filter.AddressFilter sender = 6; + */ + value: AddressFilter; + case: "sender"; + } | { + /** + * Filter by recipient address. The recipient is determined by + * checking the owners of mutated and unwrapped objects. + * + * @generated from field: iota.grpc.v1.filter.AddressFilter receiver = 7; + */ + value: AddressFilter; + case: "receiver"; + } | { + /** + * Filter for transactions that touch this object. + * + * @generated from field: iota.grpc.v1.filter.ObjectIdFilter affected_object = 8; + */ + value: ObjectIdFilter; + case: "affectedObject"; + } | { + /** + * Filter by command type. + * + * @generated from field: iota.grpc.v1.filter.CommandFilter command = 9; + */ + value: CommandFilter; + case: "command"; + } | { + /** + * Filter transactions that contain events matching the given event filter. + * + * @generated from field: iota.grpc.v1.filter.EventFilter event = 10; + */ + value: EventFilter; + case: "event"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.filter.TransactionFilter. + * Use `create(TransactionFilterSchema)` to create a new message. + */ +export const TransactionFilterSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_filter, 21); + +/** + * @generated from enum iota.grpc.v1.filter.TransactionKind + */ +export enum TransactionKind { + /** + * `SYSTEM` can be used to filter for all types of system transactions. + * + * @generated from enum value: SYSTEM = 0; + */ + SYSTEM = 0, + + /** + * @generated from enum value: PROGRAMMABLE = 1; + */ + PROGRAMMABLE = 1, + + /** + * @generated from enum value: GENESIS = 2; + */ + GENESIS = 2, + + /** + * @generated from enum value: CONSENSUS_COMMIT_PROLOGUE_V1 = 3; + */ + CONSENSUS_COMMIT_PROLOGUE_V1 = 3, + + /** + * @generated from enum value: END_OF_EPOCH = 5; + */ + END_OF_EPOCH = 5, + + /** + * @generated from enum value: RANDOMNESS_STATE_UPDATE = 6; + */ + RANDOMNESS_STATE_UPDATE = 6, +} + +/** + * Describes the enum iota.grpc.v1.filter.TransactionKind. + */ +export const TransactionKindSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_iota_grpc_v1_filter, 0); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/ledger_service_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/ledger_service_pb.ts new file mode 100644 index 00000000..d263966a --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/ledger_service_pb.ts @@ -0,0 +1,764 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/ledger_service.proto (package iota.grpc.v1.ledger_service, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { FieldMask, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_field_mask, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Status } from "../../../google/rpc/status_pb.js"; +import { file_google_rpc_status } from "../../../google/rpc/status_pb.js"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { Checkpoint } from "./checkpoint_pb.js"; +import { file_iota_grpc_v1_checkpoint } from "./checkpoint_pb.js"; +import type { Epoch } from "./epoch_pb.js"; +import { file_iota_grpc_v1_epoch } from "./epoch_pb.js"; +import type { Events } from "./event_pb.js"; +import { file_iota_grpc_v1_event } from "./event_pb.js"; +import type { EventFilter, TransactionFilter } from "./filter_pb.js"; +import { file_iota_grpc_v1_filter } from "./filter_pb.js"; +import type { Object$ } from "./object_pb.js"; +import { file_iota_grpc_v1_object } from "./object_pb.js"; +import type { ExecutedTransaction, ExecutedTransactions } from "./transaction_pb.js"; +import { file_iota_grpc_v1_transaction } from "./transaction_pb.js"; +import type { Digest, ObjectReference } from "./types_pb.js"; +import { file_iota_grpc_v1_types } from "./types_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/ledger_service.proto. + */ +export const file_iota_grpc_v1_ledger_service: GenFile = /*@__PURE__*/ + fileDesc("CiFpb3RhL2dycGMvdjEvbGVkZ2VyX3NlcnZpY2UucHJvdG8SG2lvdGEuZ3JwYy52MS5sZWRnZXJfc2VydmljZSJIChBHZXRIZWFsdGhSZXF1ZXN0EhkKDHRocmVzaG9sZF9tcxgBIAEoBEgAiAEBOgiCtRgEd2l0aEIPCg1fdGhyZXNob2xkX21zIrUBChFHZXRIZWFsdGhSZXNwb25zZRInChpleGVjdXRlZF9jaGVja3BvaW50X2hlaWdodBgBIAEoBEgAiAEBEisKHmVzdGltYXRlZF92YWxpZGF0b3JfbGF0ZW5jeV9tcxgCIAEoDUgBiAEBOgiCtRgEd2l0aEIdChtfZXhlY3V0ZWRfY2hlY2twb2ludF9oZWlnaHRCIQofX2VzdGltYXRlZF92YWxpZGF0b3JfbGF0ZW5jeV9tcyJjChVHZXRTZXJ2aWNlSW5mb1JlcXVlc3QSMgoJcmVhZF9tYXNrGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0gAiAEBOgiCtRgEd2l0aEIMCgpfcmVhZF9tYXNrIpQEChZHZXRTZXJ2aWNlSW5mb1Jlc3BvbnNlEjEKCGNoYWluX2lkGAEgASgLMhouaW90YS5ncnBjLnYxLnR5cGVzLkRpZ2VzdEgAiAEBEhIKBWNoYWluGAIgASgJSAGIAQESEgoFZXBvY2gYAyABKARIAogBARInChpleGVjdXRlZF9jaGVja3BvaW50X2hlaWdodBgEIAEoBEgDiAEBEkYKHWV4ZWN1dGVkX2NoZWNrcG9pbnRfdGltZXN0YW1wGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgEiAEBEigKG2xvd2VzdF9hdmFpbGFibGVfY2hlY2twb2ludBgGIAEoBEgFiAEBEjAKI2xvd2VzdF9hdmFpbGFibGVfY2hlY2twb2ludF9vYmplY3RzGAcgASgESAaIAQESEwoGc2VydmVyGAggASgJSAeIAQE6CIK1GAR3aXRoQgsKCV9jaGFpbl9pZEIICgZfY2hhaW5CCAoGX2Vwb2NoQh0KG19leGVjdXRlZF9jaGVja3BvaW50X2hlaWdodEIgCh5fZXhlY3V0ZWRfY2hlY2twb2ludF90aW1lc3RhbXBCHgocX2xvd2VzdF9hdmFpbGFibGVfY2hlY2twb2ludEImCiRfbG93ZXN0X2F2YWlsYWJsZV9jaGVja3BvaW50X29iamVjdHNCCQoHX3NlcnZlciJmCg1PYmplY3RSZXF1ZXN0EjwKCm9iamVjdF9yZWYYASABKAsyIy5pb3RhLmdycGMudjEudHlwZXMuT2JqZWN0UmVmZXJlbmNlSACIAQE6CIK1GAR3aXRoQg0KC19vYmplY3RfcmVmIlgKDk9iamVjdFJlcXVlc3RzEjwKCHJlcXVlc3RzGAEgAygLMiouaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLk9iamVjdFJlcXVlc3Q6CIK1GAR3aXRoIvABChFHZXRPYmplY3RzUmVxdWVzdBJCCghyZXF1ZXN0cxgBIAEoCzIrLmlvdGEuZ3JwYy52MS5sZWRnZXJfc2VydmljZS5PYmplY3RSZXF1ZXN0c0gAiAEBEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tIAYgBARIjChZtYXhfbWVzc2FnZV9zaXplX2J5dGVzGAMgASgNSAKIAQE6CIK1GAR3aXRoQgsKCV9yZXF1ZXN0c0IMCgpfcmVhZF9tYXNrQhkKF19tYXhfbWVzc2FnZV9zaXplX2J5dGVzInYKDE9iamVjdFJlc3VsdBItCgZvYmplY3QYASABKAsyGy5pb3RhLmdycGMudjEub2JqZWN0Lk9iamVjdEgAEiMKBWVycm9yGAIgASgLMhIuZ29vZ2xlLnJwYy5TdGF0dXNIADoIgrUYBHdpdGhCCAoGcmVzdWx0ImwKEkdldE9iamVjdHNSZXNwb25zZRI6CgdvYmplY3RzGAEgAygLMikuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLk9iamVjdFJlc3VsdBIQCghoYXNfbmV4dBgCIAEoCDoIgrUYBHdpdGgiWgoSVHJhbnNhY3Rpb25SZXF1ZXN0Ei8KBmRpZ2VzdBgBIAEoCzIaLmlvdGEuZ3JwYy52MS50eXBlcy5EaWdlc3RIAIgBAToIgrUYBHdpdGhCCQoHX2RpZ2VzdCJiChNUcmFuc2FjdGlvblJlcXVlc3RzEkEKCHJlcXVlc3RzGAEgAygLMi8uaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLlRyYW5zYWN0aW9uUmVxdWVzdDoIgrUYBHdpdGgi+gEKFkdldFRyYW5zYWN0aW9uc1JlcXVlc3QSRwoIcmVxdWVzdHMYASABKAsyMC5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuVHJhbnNhY3Rpb25SZXF1ZXN0c0gAiAEBEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tIAYgBARIjChZtYXhfbWVzc2FnZV9zaXplX2J5dGVzGAMgASgNSAKIAQE6CIK1GAR3aXRoQgsKCV9yZXF1ZXN0c0IMCgpfcmVhZF9tYXNrQhkKF19tYXhfbWVzc2FnZV9zaXplX2J5dGVzIpsBChFUcmFuc2FjdGlvblJlc3VsdBJNChRleGVjdXRlZF90cmFuc2FjdGlvbhgBIAEoCzItLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5FeGVjdXRlZFRyYW5zYWN0aW9uSAASIwoFZXJyb3IYAiABKAsyEi5nb29nbGUucnBjLlN0YXR1c0gAOgiCtRgEd2l0aEIICgZyZXN1bHQiggEKF0dldFRyYW5zYWN0aW9uc1Jlc3BvbnNlEksKE3RyYW5zYWN0aW9uX3Jlc3VsdHMYASADKAsyLi5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuVHJhbnNhY3Rpb25SZXN1bHQSEAoIaGFzX25leHQYAiABKAg6CIK1GAR3aXRoIsADChRHZXRDaGVja3BvaW50UmVxdWVzdBIQCgZsYXRlc3QYASABKAhIABIZCg9zZXF1ZW5jZV9udW1iZXIYAiABKARIABIsCgZkaWdlc3QYAyABKAsyGi5pb3RhLmdycGMudjEudHlwZXMuRGlnZXN0SAASMgoJcmVhZF9tYXNrGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0gBiAEBEkgKE3RyYW5zYWN0aW9uc19maWx0ZXIYBSABKAsyJi5pb3RhLmdycGMudjEuZmlsdGVyLlRyYW5zYWN0aW9uRmlsdGVySAKIAQESPAoNZXZlbnRzX2ZpbHRlchgGIAEoCzIgLmlvdGEuZ3JwYy52MS5maWx0ZXIuRXZlbnRGaWx0ZXJIA4gBARIjChZtYXhfbWVzc2FnZV9zaXplX2J5dGVzGAcgASgNSASIAQE6CIK1GAR3aXRoQg8KDWNoZWNrcG9pbnRfaWRCDAoKX3JlYWRfbWFza0IWChRfdHJhbnNhY3Rpb25zX2ZpbHRlckIQCg5fZXZlbnRzX2ZpbHRlckIZChdfbWF4X21lc3NhZ2Vfc2l6ZV9ieXRlcyLEBAoYU3RyZWFtQ2hlY2twb2ludHNSZXF1ZXN0EiIKFXN0YXJ0X3NlcXVlbmNlX251bWJlchgBIAEoBEgAiAEBEiAKE2VuZF9zZXF1ZW5jZV9udW1iZXIYAiABKARIAYgBARIyCglyZWFkX21hc2sYAyABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrSAKIAQESSAoTdHJhbnNhY3Rpb25zX2ZpbHRlchgEIAEoCzImLmlvdGEuZ3JwYy52MS5maWx0ZXIuVHJhbnNhY3Rpb25GaWx0ZXJIA4gBARI8Cg1ldmVudHNfZmlsdGVyGAUgASgLMiAuaW90YS5ncnBjLnYxLmZpbHRlci5FdmVudEZpbHRlckgEiAEBEh8KEmZpbHRlcl9jaGVja3BvaW50cxgGIAEoCEgFiAEBEiEKFHByb2dyZXNzX2ludGVydmFsX21zGAcgASgNSAaIAQESIwoWbWF4X21lc3NhZ2Vfc2l6ZV9ieXRlcxgIIAEoDUgHiAEBOgiCtRgEd2l0aEIYChZfc3RhcnRfc2VxdWVuY2VfbnVtYmVyQhYKFF9lbmRfc2VxdWVuY2VfbnVtYmVyQgwKCl9yZWFkX21hc2tCFgoUX3RyYW5zYWN0aW9uc19maWx0ZXJCEAoOX2V2ZW50c19maWx0ZXJCFQoTX2ZpbHRlcl9jaGVja3BvaW50c0IXChVfcHJvZ3Jlc3NfaW50ZXJ2YWxfbXNCGQoXX21heF9tZXNzYWdlX3NpemVfYnl0ZXMi/QMKDkNoZWNrcG9pbnREYXRhEjkKCmNoZWNrcG9pbnQYASABKAsyIy5pb3RhLmdycGMudjEuY2hlY2twb2ludC5DaGVja3BvaW50SAASTwoVZXhlY3V0ZWRfdHJhbnNhY3Rpb25zGAIgASgLMi4uaW90YS5ncnBjLnYxLnRyYW5zYWN0aW9uLkV4ZWN1dGVkVHJhbnNhY3Rpb25zSAASLAoGZXZlbnRzGAMgASgLMhouaW90YS5ncnBjLnYxLmV2ZW50LkV2ZW50c0gAEkgKCHByb2dyZXNzGAQgASgLMjQuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkNoZWNrcG9pbnREYXRhLlByb2dyZXNzSAASSwoKZW5kX21hcmtlchgFIAEoCzI1LmlvdGEuZ3JwYy52MS5sZWRnZXJfc2VydmljZS5DaGVja3BvaW50RGF0YS5FbmRNYXJrZXJIABo8CghQcm9ncmVzcxImCh5sYXRlc3Rfc2Nhbm5lZF9zZXF1ZW5jZV9udW1iZXIYASABKAQ6CIK1GAR3aXRoGkcKCUVuZE1hcmtlchIcCg9zZXF1ZW5jZV9udW1iZXIYASABKARIAIgBAToIgrUYBHdpdGhCEgoQX3NlcXVlbmNlX251bWJlcjoIgrUYBHdpdGhCCQoHcGF5bG9hZCJ7Cg9HZXRFcG9jaFJlcXVlc3QSEgoFZXBvY2gYASABKARIAIgBARIyCglyZWFkX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrSAGIAQE6CIK1GAR3aXRoQggKBl9lcG9jaEIMCgpfcmVhZF9tYXNrIlUKEEdldEVwb2NoUmVzcG9uc2USLQoFZXBvY2gYASABKAsyGS5pb3RhLmdycGMudjEuZXBvY2guRXBvY2hIAIgBAToIgrUYBHdpdGhCCAoGX2Vwb2NoMr4GCg1MZWRnZXJTZXJ2aWNlEmoKCUdldEhlYWx0aBItLmlvdGEuZ3JwYy52MS5sZWRnZXJfc2VydmljZS5HZXRIZWFsdGhSZXF1ZXN0Gi4uaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkdldEhlYWx0aFJlc3BvbnNlEnkKDkdldFNlcnZpY2VJbmZvEjIuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkdldFNlcnZpY2VJbmZvUmVxdWVzdBozLmlvdGEuZ3JwYy52MS5sZWRnZXJfc2VydmljZS5HZXRTZXJ2aWNlSW5mb1Jlc3BvbnNlEm8KCkdldE9iamVjdHMSLi5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuR2V0T2JqZWN0c1JlcXVlc3QaLy5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuR2V0T2JqZWN0c1Jlc3BvbnNlMAESfgoPR2V0VHJhbnNhY3Rpb25zEjMuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkdldFRyYW5zYWN0aW9uc1JlcXVlc3QaNC5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuR2V0VHJhbnNhY3Rpb25zUmVzcG9uc2UwARJxCg1HZXRDaGVja3BvaW50EjEuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkdldENoZWNrcG9pbnRSZXF1ZXN0GisuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkNoZWNrcG9pbnREYXRhMAESeQoRU3RyZWFtQ2hlY2twb2ludHMSNS5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuU3RyZWFtQ2hlY2twb2ludHNSZXF1ZXN0GisuaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkNoZWNrcG9pbnREYXRhMAESZwoIR2V0RXBvY2gSLC5pb3RhLmdycGMudjEubGVkZ2VyX3NlcnZpY2UuR2V0RXBvY2hSZXF1ZXN0Gi0uaW90YS5ncnBjLnYxLmxlZGdlcl9zZXJ2aWNlLkdldEVwb2NoUmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_field_mask, file_google_protobuf_timestamp, file_google_rpc_status, file_iota_grpc_options, file_iota_grpc_v1_checkpoint, file_iota_grpc_v1_epoch, file_iota_grpc_v1_event, file_iota_grpc_v1_filter, file_iota_grpc_v1_object, file_iota_grpc_v1_transaction, file_iota_grpc_v1_types]); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetHealthRequest + */ +export type GetHealthRequest = Message<"iota.grpc.v1.ledger_service.GetHealthRequest"> & { + /** + * Optional threshold in milliseconds. The node is considered healthy only if + * the latest executed checkpoint timestamp is within this many milliseconds of + * the current system time. If not provided, the server applies a default + * threshold of 5 seconds. + * + * @generated from field: optional uint64 threshold_ms = 1; + */ + thresholdMs?: bigint | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetHealthRequest. + * Use `create(GetHealthRequestSchema)` to create a new message. + */ +export const GetHealthRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 0); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetHealthResponse + */ +export type GetHealthResponse = Message<"iota.grpc.v1.ledger_service.GetHealthResponse"> & { + /** + * Checkpoint height of the most recently executed checkpoint. + * + * @generated from field: optional uint64 executed_checkpoint_height = 1; + */ + executedCheckpointHeight?: bigint | undefined; + + /** + * Estimated validator latency in milliseconds. + * Reserved for future use. + * + * @generated from field: optional uint32 estimated_validator_latency_ms = 2; + */ + estimatedValidatorLatencyMs?: number | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetHealthResponse. + * Use `create(GetHealthResponseSchema)` to create a new message. + */ +export const GetHealthResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 1); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetServiceInfoRequest + */ +export type GetServiceInfoRequest = Message<"iota.grpc.v1.ledger_service.GetServiceInfoRequest"> & { + /** + * Mask specifying which ServiceInfo fields to read. + * If no mask is specified, defaults to `chain_id,epoch,checkpoint_height`. + * + * @generated from field: optional google.protobuf.FieldMask read_mask = 1; + */ + readMask?: FieldMask | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetServiceInfoRequest. + * Use `create(GetServiceInfoRequestSchema)` to create a new message. + */ +export const GetServiceInfoRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 2); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetServiceInfoResponse + */ +export type GetServiceInfoResponse = Message<"iota.grpc.v1.ledger_service.GetServiceInfoResponse"> & { + /** + * The chain identifier of the chain that this node is on. + * + * The chain identifier is the digest of the genesis checkpoint, the + * checkpoint with sequence number 0. + * + * @generated from field: optional iota.grpc.v1.types.Digest chain_id = 1; + */ + chainId?: Digest | undefined; + + /** + * Human-readable name of the chain that this node is on. + * + * This is intended to be a human-readable name like `mainnet`, `testnet`, and so on. + * + * @generated from field: optional string chain = 2; + */ + chain?: string | undefined; + + /** + * Current epoch of the node based on its highest executed checkpoint. + * + * @generated from field: optional uint64 epoch = 3; + */ + epoch?: bigint | undefined; + + /** + * Checkpoint height of the most recently executed checkpoint. + * + * @generated from field: optional uint64 executed_checkpoint_height = 4; + */ + executedCheckpointHeight?: bigint | undefined; + + /** + * Unix timestamp of the most recently executed checkpoint. + * + * @generated from field: optional google.protobuf.Timestamp executed_checkpoint_timestamp = 5; + */ + executedCheckpointTimestamp?: Timestamp | undefined; + + /** + * The lowest checkpoint for which checkpoints and transaction data are available. + * + * @generated from field: optional uint64 lowest_available_checkpoint = 6; + */ + lowestAvailableCheckpoint?: bigint | undefined; + + /** + * The lowest checkpoint for which object data is available. + * + * @generated from field: optional uint64 lowest_available_checkpoint_objects = 7; + */ + lowestAvailableCheckpointObjects?: bigint | undefined; + + /** + * Software version of the service. Similar to the `server` http header. + * + * @generated from field: optional string server = 8; + */ + server?: string | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetServiceInfoResponse. + * Use `create(GetServiceInfoResponseSchema)` to create a new message. + */ +export const GetServiceInfoResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 3); + +/** + * @generated from message iota.grpc.v1.ledger_service.ObjectRequest + */ +export type ObjectRequest = Message<"iota.grpc.v1.ledger_service.ObjectRequest"> & { + /** + * Required. The `ObjectId` of the requested object. + * If no version is specified, and the object is live, then the latest + * version of the object is returned. + * + * @generated from field: optional iota.grpc.v1.types.ObjectReference object_ref = 1; + */ + objectRef?: ObjectReference | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.ObjectRequest. + * Use `create(ObjectRequestSchema)` to create a new message. + */ +export const ObjectRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 4); + +/** + * @generated from message iota.grpc.v1.ledger_service.ObjectRequests + */ +export type ObjectRequests = Message<"iota.grpc.v1.ledger_service.ObjectRequests"> & { + /** + * @generated from field: repeated iota.grpc.v1.ledger_service.ObjectRequest requests = 1; + */ + requests: ObjectRequest[]; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.ObjectRequests. + * Use `create(ObjectRequestsSchema)` to create a new message. + */ +export const ObjectRequestsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 5); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetObjectsRequest + */ +export type GetObjectsRequest = Message<"iota.grpc.v1.ledger_service.GetObjectsRequest"> & { + /** + * @generated from field: optional iota.grpc.v1.ledger_service.ObjectRequests requests = 1; + */ + requests?: ObjectRequests | undefined; + + /** + * Mask specifying which fields to read. + * If no mask is specified, defaults to `object_id,version,digest`. + * + * @generated from field: optional google.protobuf.FieldMask read_mask = 2; + */ + readMask?: FieldMask | undefined; + + /** + * Optional maximum message size the client can receive (1MB - 128MB) + * If not specified, server uses default chunking threshold (4MB) + * + * @generated from field: optional uint32 max_message_size_bytes = 3; + */ + maxMessageSizeBytes?: number | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetObjectsRequest. + * Use `create(GetObjectsRequestSchema)` to create a new message. + */ +export const GetObjectsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 6); + +/** + * @generated from message iota.grpc.v1.ledger_service.ObjectResult + */ +export type ObjectResult = Message<"iota.grpc.v1.ledger_service.ObjectResult"> & { + /** + * @generated from oneof iota.grpc.v1.ledger_service.ObjectResult.result + */ + result: { + /** + * @generated from field: iota.grpc.v1.object.Object object = 1; + */ + value: Object$; + case: "object"; + } | { + /** + * @generated from field: google.rpc.Status error = 2; + */ + value: Status; + case: "error"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.ObjectResult. + * Use `create(ObjectResultSchema)` to create a new message. + */ +export const ObjectResultSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 7); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetObjectsResponse + */ +export type GetObjectsResponse = Message<"iota.grpc.v1.ledger_service.GetObjectsResponse"> & { + /** + * @generated from field: repeated iota.grpc.v1.ledger_service.ObjectResult objects = 1; + */ + objects: ObjectResult[]; + + /** + * @generated from field: bool has_next = 2; + */ + hasNext: boolean; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetObjectsResponse. + * Use `create(GetObjectsResponseSchema)` to create a new message. + */ +export const GetObjectsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 8); + +/** + * @generated from message iota.grpc.v1.ledger_service.TransactionRequest + */ +export type TransactionRequest = Message<"iota.grpc.v1.ledger_service.TransactionRequest"> & { + /** + * Required. The digest of the requested transaction. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 1; + */ + digest?: Digest | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.TransactionRequest. + * Use `create(TransactionRequestSchema)` to create a new message. + */ +export const TransactionRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 9); + +/** + * @generated from message iota.grpc.v1.ledger_service.TransactionRequests + */ +export type TransactionRequests = Message<"iota.grpc.v1.ledger_service.TransactionRequests"> & { + /** + * @generated from field: repeated iota.grpc.v1.ledger_service.TransactionRequest requests = 1; + */ + requests: TransactionRequest[]; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.TransactionRequests. + * Use `create(TransactionRequestsSchema)` to create a new message. + */ +export const TransactionRequestsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 10); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetTransactionsRequest + */ +export type GetTransactionsRequest = Message<"iota.grpc.v1.ledger_service.GetTransactionsRequest"> & { + /** + * @generated from field: optional iota.grpc.v1.ledger_service.TransactionRequests requests = 1; + */ + requests?: TransactionRequests | undefined; + + /** + * Mask specifying which fields to read. + * If no mask is specified, defaults to `transaction.digest`. + * + * @generated from field: optional google.protobuf.FieldMask read_mask = 2; + */ + readMask?: FieldMask | undefined; + + /** + * Optional maximum message size the client can receive (1MB - 128MB) + * If not specified, server uses default chunking threshold (4MB) + * + * @generated from field: optional uint32 max_message_size_bytes = 3; + */ + maxMessageSizeBytes?: number | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetTransactionsRequest. + * Use `create(GetTransactionsRequestSchema)` to create a new message. + */ +export const GetTransactionsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 11); + +/** + * @generated from message iota.grpc.v1.ledger_service.TransactionResult + */ +export type TransactionResult = Message<"iota.grpc.v1.ledger_service.TransactionResult"> & { + /** + * @generated from oneof iota.grpc.v1.ledger_service.TransactionResult.result + */ + result: { + /** + * @generated from field: iota.grpc.v1.transaction.ExecutedTransaction executed_transaction = 1; + */ + value: ExecutedTransaction; + case: "executedTransaction"; + } | { + /** + * @generated from field: google.rpc.Status error = 2; + */ + value: Status; + case: "error"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.TransactionResult. + * Use `create(TransactionResultSchema)` to create a new message. + */ +export const TransactionResultSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 12); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetTransactionsResponse + */ +export type GetTransactionsResponse = Message<"iota.grpc.v1.ledger_service.GetTransactionsResponse"> & { + /** + * @generated from field: repeated iota.grpc.v1.ledger_service.TransactionResult transaction_results = 1; + */ + transactionResults: TransactionResult[]; + + /** + * @generated from field: bool has_next = 2; + */ + hasNext: boolean; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetTransactionsResponse. + * Use `create(GetTransactionsResponseSchema)` to create a new message. + */ +export const GetTransactionsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 13); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetCheckpointRequest + */ +export type GetCheckpointRequest = Message<"iota.grpc.v1.ledger_service.GetCheckpointRequest"> & { + /** + * @generated from oneof iota.grpc.v1.ledger_service.GetCheckpointRequest.checkpoint_id + */ + checkpointId: { + /** + * If set to true, the latest checkpoint is requested. + * + * @generated from field: bool latest = 1; + */ + value: boolean; + case: "latest"; + } | { + /** + * The sequence number of the requested checkpoint. + * + * @generated from field: uint64 sequence_number = 2; + */ + value: bigint; + case: "sequenceNumber"; + } | { + /** + * The digest of the requested checkpoint. + * + * @generated from field: iota.grpc.v1.types.Digest digest = 3; + */ + value: Digest; + case: "digest"; + } | { case: undefined; value?: undefined }; + + /** + * Mask specifying which fields to read. + * If no mask is specified, defaults to `summary`. + * + * @generated from field: optional google.protobuf.FieldMask read_mask = 4; + */ + readMask?: FieldMask | undefined; + + /** + * if no filter is passed, all transactions are included (if mentioned in the read_mask) + * + * @generated from field: optional iota.grpc.v1.filter.TransactionFilter transactions_filter = 5; + */ + transactionsFilter?: TransactionFilter | undefined; + + /** + * if no filter is passed, all events are included (if mentioned in the read_mask) + * + * @generated from field: optional iota.grpc.v1.filter.EventFilter events_filter = 6; + */ + eventsFilter?: EventFilter | undefined; + + /** + * Optional maximum message size the client can receive (1MB - 128MB) + * If not specified, server uses default chunking threshold (4MB) + * + * @generated from field: optional uint32 max_message_size_bytes = 7; + */ + maxMessageSizeBytes?: number | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetCheckpointRequest. + * Use `create(GetCheckpointRequestSchema)` to create a new message. + */ +export const GetCheckpointRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 14); + +/** + * @generated from message iota.grpc.v1.ledger_service.StreamCheckpointsRequest + */ +export type StreamCheckpointsRequest = Message<"iota.grpc.v1.ledger_service.StreamCheckpointsRequest"> & { + /** + * if no start sequence number is provided, streaming starts from the latest checkpoint + * + * @generated from field: optional uint64 start_sequence_number = 1; + */ + startSequenceNumber?: bigint | undefined; + + /** + * if no end sequence number is provided, streaming continues forever + * + * @generated from field: optional uint64 end_sequence_number = 2; + */ + endSequenceNumber?: bigint | undefined; + + /** + * Mask specifying which fields to read. + * If no mask is specified, defaults to `summary`. + * + * @generated from field: optional google.protobuf.FieldMask read_mask = 3; + */ + readMask?: FieldMask | undefined; + + /** + * if no filter is passed, all transactions are included (if mentioned in the read_mask) + * + * @generated from field: optional iota.grpc.v1.filter.TransactionFilter transactions_filter = 4; + */ + transactionsFilter?: TransactionFilter | undefined; + + /** + * if no filter is passed, all events are included (if mentioned in the read_mask) + * + * @generated from field: optional iota.grpc.v1.filter.EventFilter events_filter = 5; + */ + eventsFilter?: EventFilter | undefined; + + /** + * When true, checkpoints with no matching transactions or events are skipped entirely. + * At least one of transactions_filter or events_filter must be set. + * A Progress message is sent periodically to indicate liveness and scan position. + * + * @generated from field: optional bool filter_checkpoints = 6; + */ + filterCheckpoints?: boolean | undefined; + + /** + * Progress message interval in milliseconds when filter_checkpoints is enabled. + * Defaults to 2000ms. Minimum value is 500ms; lower values are clamped. + * + * @generated from field: optional uint32 progress_interval_ms = 7; + */ + progressIntervalMs?: number | undefined; + + /** + * Optional maximum message size the client can receive (1MB - 128MB) + * If not specified, server uses default chunking threshold (4MB) + * + * @generated from field: optional uint32 max_message_size_bytes = 8; + */ + maxMessageSizeBytes?: number | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.StreamCheckpointsRequest. + * Use `create(StreamCheckpointsRequestSchema)` to create a new message. + */ +export const StreamCheckpointsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 15); + +/** + * @generated from message iota.grpc.v1.ledger_service.CheckpointData + */ +export type CheckpointData = Message<"iota.grpc.v1.ledger_service.CheckpointData"> & { + /** + * @generated from oneof iota.grpc.v1.ledger_service.CheckpointData.payload + */ + payload: { + /** + * @generated from field: iota.grpc.v1.checkpoint.Checkpoint checkpoint = 1; + */ + value: Checkpoint; + case: "checkpoint"; + } | { + /** + * @generated from field: iota.grpc.v1.transaction.ExecutedTransactions executed_transactions = 2; + */ + value: ExecutedTransactions; + case: "executedTransactions"; + } | { + /** + * @generated from field: iota.grpc.v1.event.Events events = 3; + */ + value: Events; + case: "events"; + } | { + /** + * @generated from field: iota.grpc.v1.ledger_service.CheckpointData.Progress progress = 4; + */ + value: CheckpointData_Progress; + case: "progress"; + } | { + /** + * @generated from field: iota.grpc.v1.ledger_service.CheckpointData.EndMarker end_marker = 5; + */ + value: CheckpointData_EndMarker; + case: "endMarker"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.CheckpointData. + * Use `create(CheckpointDataSchema)` to create a new message. + */ +export const CheckpointDataSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 16); + +/** + * @generated from message iota.grpc.v1.ledger_service.CheckpointData.Progress + */ +export type CheckpointData_Progress = Message<"iota.grpc.v1.ledger_service.CheckpointData.Progress"> & { + /** + * The sequence number of the latest scanned checkpoint. + * + * @generated from field: uint64 latest_scanned_sequence_number = 1; + */ + latestScannedSequenceNumber: bigint; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.CheckpointData.Progress. + * Use `create(CheckpointData_ProgressSchema)` to create a new message. + */ +export const CheckpointData_ProgressSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 16, 0); + +/** + * @generated from message iota.grpc.v1.ledger_service.CheckpointData.EndMarker + */ +export type CheckpointData_EndMarker = Message<"iota.grpc.v1.ledger_service.CheckpointData.EndMarker"> & { + /** + * The height of this checkpoint. + * + * @generated from field: optional uint64 sequence_number = 1; + */ + sequenceNumber?: bigint | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.CheckpointData.EndMarker. + * Use `create(CheckpointData_EndMarkerSchema)` to create a new message. + */ +export const CheckpointData_EndMarkerSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 16, 1); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetEpochRequest + */ +export type GetEpochRequest = Message<"iota.grpc.v1.ledger_service.GetEpochRequest"> & { + /** + * The requested epoch. + * If no epoch is provided the current epoch will be returned. + * + * @generated from field: optional uint64 epoch = 1; + */ + epoch?: bigint | undefined; + + /** + * Mask specifying which fields to read. + * If no mask is specified, defaults to `epoch`. + * + * @generated from field: optional google.protobuf.FieldMask read_mask = 2; + */ + readMask?: FieldMask | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetEpochRequest. + * Use `create(GetEpochRequestSchema)` to create a new message. + */ +export const GetEpochRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 17); + +/** + * @generated from message iota.grpc.v1.ledger_service.GetEpochResponse + */ +export type GetEpochResponse = Message<"iota.grpc.v1.ledger_service.GetEpochResponse"> & { + /** + * @generated from field: optional iota.grpc.v1.epoch.Epoch epoch = 1; + */ + epoch?: Epoch | undefined; +}; + +/** + * Describes the message iota.grpc.v1.ledger_service.GetEpochResponse. + * Use `create(GetEpochResponseSchema)` to create a new message. + */ +export const GetEpochResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_ledger_service, 18); + +/** + * @generated from service iota.grpc.v1.ledger_service.LedgerService + */ +export const LedgerService: GenService<{ + /** + * Check the health of the node. + * + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.GetHealth + */ + getHealth: { + methodKind: "unary"; + input: typeof GetHealthRequestSchema; + output: typeof GetHealthResponseSchema; + }, + /** + * Query the service for general information about its current state. + * + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.GetServiceInfo + */ + getServiceInfo: { + methodKind: "unary"; + input: typeof GetServiceInfoRequestSchema; + output: typeof GetServiceInfoResponseSchema; + }, + /** + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.GetObjects + */ + getObjects: { + methodKind: "server_streaming"; + input: typeof GetObjectsRequestSchema; + output: typeof GetObjectsResponseSchema; + }, + /** + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.GetTransactions + */ + getTransactions: { + methodKind: "server_streaming"; + input: typeof GetTransactionsRequestSchema; + output: typeof GetTransactionsResponseSchema; + }, + /** + * Checkpoint operations + * + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.GetCheckpoint + */ + getCheckpoint: { + methodKind: "server_streaming"; + input: typeof GetCheckpointRequestSchema; + output: typeof CheckpointDataSchema; + }, + /** + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.StreamCheckpoints + */ + streamCheckpoints: { + methodKind: "server_streaming"; + input: typeof StreamCheckpointsRequestSchema; + output: typeof CheckpointDataSchema; + }, + /** + * @generated from rpc iota.grpc.v1.ledger_service.LedgerService.GetEpoch + */ + getEpoch: { + methodKind: "unary"; + input: typeof GetEpochRequestSchema; + output: typeof GetEpochResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_iota_grpc_v1_ledger_service, 0); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/object_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/object_pb.ts new file mode 100644 index 00000000..4e330148 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/object_pb.ts @@ -0,0 +1,70 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/object.proto (package iota.grpc.v1.object, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { BcsData } from "./bcs_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { ObjectReference } from "./types_pb.js"; +import { file_iota_grpc_v1_types } from "./types_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/object.proto. + */ +export const file_iota_grpc_v1_object: GenFile = /*@__PURE__*/ + fileDesc("Chlpb3RhL2dycGMvdjEvb2JqZWN0LnByb3RvEhNpb3RhLmdycGMudjEub2JqZWN0IogBCgZPYmplY3QSOwoJcmVmZXJlbmNlGAEgASgLMiMuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdFJlZmVyZW5jZUgAiAEBEisKA2JjcxgCIAEoCzIZLmlvdGEuZ3JwYy52MS5iY3MuQmNzRGF0YUgBiAEBQgwKCl9yZWZlcmVuY2VCBgoEX2JjcyI9CgdPYmplY3RzEiwKB29iamVjdHMYASADKAsyGy5pb3RhLmdycGMudjEub2JqZWN0Lk9iamVjdDoEkLUYAWIGcHJvdG8z", [file_iota_grpc_options, file_iota_grpc_v1_bcs, file_iota_grpc_v1_types]); + +/** + * An object on the IOTA blockchain. + * + * @generated from message iota.grpc.v1.object.Object + */ +export type Object$ = Message<"iota.grpc.v1.object.Object"> & { + /** + * Reference to this object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectReference reference = 1; + */ + reference?: ObjectReference | undefined; + + /** + * This Object serialized as BCS. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 2; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.object.Object. + * Use `create(ObjectSchema)` to create a new message. + */ +export const ObjectSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_object, 0); + +/** + * A list of objects. + * + * @generated from message iota.grpc.v1.object.Objects + */ +export type Objects = Message<"iota.grpc.v1.object.Objects"> & { + /** + * @generated from field: repeated iota.grpc.v1.object.Object objects = 1; + */ + objects: Object$[]; +}; + +/** + * Describes the message iota.grpc.v1.object.Objects. + * Use `create(ObjectsSchema)` to create a new message. + */ +export const ObjectsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_object, 1); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/signatures_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/signatures_pb.ts new file mode 100644 index 00000000..979bbd50 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/signatures_pb.ts @@ -0,0 +1,83 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/signatures.proto (package iota.grpc.v1.signatures, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { BcsData } from "./bcs_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/signatures.proto. + */ +export const file_iota_grpc_v1_signatures: GenFile = /*@__PURE__*/ + fileDesc("Ch1pb3RhL2dycGMvdjEvc2lnbmF0dXJlcy5wcm90bxIXaW90YS5ncnBjLnYxLnNpZ25hdHVyZXMiTgoNVXNlclNpZ25hdHVyZRIrCgNiY3MYASABKAsyGS5pb3RhLmdycGMudjEuYmNzLkJjc0RhdGFIAIgBAToIgrUYBHdpdGhCBgoEX2JjcyJaCg5Vc2VyU2lnbmF0dXJlcxI6CgpzaWduYXR1cmVzGAEgAygLMiYuaW90YS5ncnBjLnYxLnNpZ25hdHVyZXMuVXNlclNpZ25hdHVyZToMgrUYBHdpdGiQtRgBIl0KHFZhbGlkYXRvckFnZ3JlZ2F0ZWRTaWduYXR1cmUSKwoDYmNzGAEgASgLMhkuaW90YS5ncnBjLnYxLmJjcy5CY3NEYXRhSACIAQE6CIK1GAR3aXRoQgYKBF9iY3NiBnByb3RvMw", [file_iota_grpc_options, file_iota_grpc_v1_bcs]); + +/** + * A signature from a user. + * + * @generated from message iota.grpc.v1.signatures.UserSignature + */ +export type UserSignature = Message<"iota.grpc.v1.signatures.UserSignature"> & { + /** + * This signature serialized as as BCS. + * + * When provided as input this will support both the form that is length + * prefixed as well as not length prefixed. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 1; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.signatures.UserSignature. + * Use `create(UserSignatureSchema)` to create a new message. + */ +export const UserSignatureSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_signatures, 0); + +/** + * A list of user signatures. + * + * @generated from message iota.grpc.v1.signatures.UserSignatures + */ +export type UserSignatures = Message<"iota.grpc.v1.signatures.UserSignatures"> & { + /** + * @generated from field: repeated iota.grpc.v1.signatures.UserSignature signatures = 1; + */ + signatures: UserSignature[]; +}; + +/** + * Describes the message iota.grpc.v1.signatures.UserSignatures. + * Use `create(UserSignaturesSchema)` to create a new message. + */ +export const UserSignaturesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_signatures, 1); + +/** + * / An aggregated signature from multiple validators. + * + * @generated from message iota.grpc.v1.signatures.ValidatorAggregatedSignature + */ +export type ValidatorAggregatedSignature = Message<"iota.grpc.v1.signatures.ValidatorAggregatedSignature"> & { + /** + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 1; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.signatures.ValidatorAggregatedSignature. + * Use `create(ValidatorAggregatedSignatureSchema)` to create a new message. + */ +export const ValidatorAggregatedSignatureSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_signatures, 2); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/transaction_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/transaction_pb.ts new file mode 100644 index 00000000..1975f2c3 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/transaction_pb.ts @@ -0,0 +1,676 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/transaction.proto (package iota.grpc.v1.transaction, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { BcsData } from "./bcs_pb.js"; +import { file_iota_grpc_v1_bcs } from "./bcs_pb.js"; +import type { Events } from "./event_pb.js"; +import { file_iota_grpc_v1_event } from "./event_pb.js"; +import type { Objects } from "./object_pb.js"; +import { file_iota_grpc_v1_object } from "./object_pb.js"; +import type { UserSignatures } from "./signatures_pb.js"; +import { file_iota_grpc_v1_signatures } from "./signatures_pb.js"; +import type { Address, Digest, ObjectId, Owner, TypeTag } from "./types_pb.js"; +import { file_iota_grpc_v1_types } from "./types_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/transaction.proto. + */ +export const file_iota_grpc_v1_transaction: GenFile = /*@__PURE__*/ + fileDesc("Ch5pb3RhL2dycGMvdjEvdHJhbnNhY3Rpb24ucHJvdG8SGGlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbiKIAQoLVHJhbnNhY3Rpb24SLwoGZGlnZXN0GAEgASgLMhouaW90YS5ncnBjLnYxLnR5cGVzLkRpZ2VzdEgAiAEBEisKA2JjcxgCIAEoCzIZLmlvdGEuZ3JwYy52MS5iY3MuQmNzRGF0YUgBiAEBOgiCtRgEd2l0aEIJCgdfZGlnZXN0QgYKBF9iY3MijwEKElRyYW5zYWN0aW9uRWZmZWN0cxIvCgZkaWdlc3QYASABKAsyGi5pb3RhLmdycGMudjEudHlwZXMuRGlnZXN0SACIAQESKwoDYmNzGAIgASgLMhkuaW90YS5ncnBjLnYxLmJjcy5CY3NEYXRhSAGIAQE6CIK1GAR3aXRoQgkKB19kaWdlc3RCBgoEX2JjcyKVAQoRVHJhbnNhY3Rpb25FdmVudHMSLwoGZGlnZXN0GAEgASgLMhouaW90YS5ncnBjLnYxLnR5cGVzLkRpZ2VzdEgAiAEBEi8KBmV2ZW50cxgCIAEoCzIaLmlvdGEuZ3JwYy52MS5ldmVudC5FdmVudHNIAYgBAToIgrUYBHdpdGhCCQoHX2RpZ2VzdEIJCgdfZXZlbnRzIpcGChNFeGVjdXRlZFRyYW5zYWN0aW9uEj8KC3RyYW5zYWN0aW9uGAEgASgLMiUuaW90YS5ncnBjLnYxLnRyYW5zYWN0aW9uLlRyYW5zYWN0aW9uSACIAQESQAoKc2lnbmF0dXJlcxgCIAEoCzInLmlvdGEuZ3JwYy52MS5zaWduYXR1cmVzLlVzZXJTaWduYXR1cmVzSAGIAQESQgoHZWZmZWN0cxgDIAEoCzIsLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5UcmFuc2FjdGlvbkVmZmVjdHNIAogBARJACgZldmVudHMYBCABKAsyKy5pb3RhLmdycGMudjEudHJhbnNhY3Rpb24uVHJhbnNhY3Rpb25FdmVudHNIA4gBARIXCgpjaGVja3BvaW50GAUgASgESASIAQESMgoJdGltZXN0YW1wGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgFiAEBEjgKDWlucHV0X29iamVjdHMYByABKAsyHC5pb3RhLmdycGMudjEub2JqZWN0Lk9iamVjdHNIBogBARI5Cg5vdXRwdXRfb2JqZWN0cxgIIAEoCzIcLmlvdGEuZ3JwYy52MS5vYmplY3QuT2JqZWN0c0gHiAEBEkYKD2JhbGFuY2VfY2hhbmdlcxgJIAEoCzIoLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5CYWxhbmNlQ2hhbmdlc0gIiAEBEkQKDm9iamVjdF9jaGFuZ2VzGAogASgLMicuaW90YS5ncnBjLnYxLnRyYW5zYWN0aW9uLk9iamVjdENoYW5nZXNICYgBAToIgrUYBHdpdGhCDgoMX3RyYW5zYWN0aW9uQg0KC19zaWduYXR1cmVzQgoKCF9lZmZlY3RzQgkKB19ldmVudHNCDQoLX2NoZWNrcG9pbnRCDAoKX3RpbWVzdGFtcEIQCg5faW5wdXRfb2JqZWN0c0IRCg9fb3V0cHV0X29iamVjdHNCEgoQX2JhbGFuY2VfY2hhbmdlc0IRCg9fb2JqZWN0X2NoYW5nZXMitQEKDUJhbGFuY2VDaGFuZ2USLQoFb3duZXIYASABKAsyGS5pb3RhLmdycGMudjEudHlwZXMuT3duZXJIAIgBARIzCgljb2luX3R5cGUYAiABKAsyGy5pb3RhLmdycGMudjEudHlwZXMuVHlwZVRhZ0gBiAEBEhMKBmFtb3VudBgDIAEoDEgCiAEBOgiCtRgEd2l0aEIICgZfb3duZXJCDAoKX2NvaW5fdHlwZUIJCgdfYW1vdW50ImAKDkJhbGFuY2VDaGFuZ2VzEkAKD2JhbGFuY2VfY2hhbmdlcxgBIAMoCzInLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5CYWxhbmNlQ2hhbmdlOgyCtRgEd2l0aJC1GAEi1gEKFU9iamVjdENoYW5nZVB1Ymxpc2hlZBI1CgpwYWNrYWdlX2lkGAEgASgLMhwuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdElkSACIAQESFAoHdmVyc2lvbhgCIAEoBEgBiAEBEi8KBmRpZ2VzdBgDIAEoCzIaLmlvdGEuZ3JwYy52MS50eXBlcy5EaWdlc3RIAogBARIPCgdtb2R1bGVzGAQgAygJOgiCtRgEd2l0aEINCgtfcGFja2FnZV9pZEIKCghfdmVyc2lvbkIJCgdfZGlnZXN0IrIDChNPYmplY3RDaGFuZ2VNdXRhdGVkEjAKBnNlbmRlchgBIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5BZGRyZXNzSACIAQESLQoFb3duZXIYAiABKAsyGS5pb3RhLmdycGMudjEudHlwZXMuT3duZXJIAYgBARI1CgtvYmplY3RfdHlwZRgDIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5UeXBlVGFnSAKIAQESNAoJb2JqZWN0X2lkGAQgASgLMhwuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdElkSAOIAQESFAoHdmVyc2lvbhgFIAEoBEgEiAEBEh0KEHByZXZpb3VzX3ZlcnNpb24YBiABKARIBYgBARIvCgZkaWdlc3QYByABKAsyGi5pb3RhLmdycGMudjEudHlwZXMuRGlnZXN0SAaIAQE6CIK1GAR3aXRoQgkKB19zZW5kZXJCCAoGX293bmVyQg4KDF9vYmplY3RfdHlwZUIMCgpfb2JqZWN0X2lkQgoKCF92ZXJzaW9uQhMKEV9wcmV2aW91c192ZXJzaW9uQgkKB19kaWdlc3QiiQIKE09iamVjdENoYW5nZURlbGV0ZWQSMAoGc2VuZGVyGAEgASgLMhsuaW90YS5ncnBjLnYxLnR5cGVzLkFkZHJlc3NIAIgBARI1CgtvYmplY3RfdHlwZRgCIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5UeXBlVGFnSAGIAQESNAoJb2JqZWN0X2lkGAMgASgLMhwuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdElkSAKIAQESFAoHdmVyc2lvbhgEIAEoBEgDiAEBOgiCtRgEd2l0aEIJCgdfc2VuZGVyQg4KDF9vYmplY3RfdHlwZUIMCgpfb2JqZWN0X2lkQgoKCF92ZXJzaW9uIokCChNPYmplY3RDaGFuZ2VXcmFwcGVkEjAKBnNlbmRlchgBIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5BZGRyZXNzSACIAQESNQoLb2JqZWN0X3R5cGUYAiABKAsyGy5pb3RhLmdycGMudjEudHlwZXMuVHlwZVRhZ0gBiAEBEjQKCW9iamVjdF9pZBgDIAEoCzIcLmlvdGEuZ3JwYy52MS50eXBlcy5PYmplY3RJZEgCiAEBEhQKB3ZlcnNpb24YBCABKARIA4gBAToIgrUYBHdpdGhCCQoHX3NlbmRlckIOCgxfb2JqZWN0X3R5cGVCDAoKX29iamVjdF9pZEIKCghfdmVyc2lvbiKAAwoVT2JqZWN0Q2hhbmdlVW53cmFwcGVkEjAKBnNlbmRlchgBIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5BZGRyZXNzSACIAQESLQoFb3duZXIYAiABKAsyGS5pb3RhLmdycGMudjEudHlwZXMuT3duZXJIAYgBARI1CgtvYmplY3RfdHlwZRgDIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5UeXBlVGFnSAKIAQESNAoJb2JqZWN0X2lkGAQgASgLMhwuaW90YS5ncnBjLnYxLnR5cGVzLk9iamVjdElkSAOIAQESFAoHdmVyc2lvbhgFIAEoBEgEiAEBEi8KBmRpZ2VzdBgGIAEoCzIaLmlvdGEuZ3JwYy52MS50eXBlcy5EaWdlc3RIBYgBAToIgrUYBHdpdGhCCQoHX3NlbmRlckIICgZfb3duZXJCDgoMX29iamVjdF90eXBlQgwKCl9vYmplY3RfaWRCCgoIX3ZlcnNpb25CCQoHX2RpZ2VzdCL+AgoTT2JqZWN0Q2hhbmdlQ3JlYXRlZBIwCgZzZW5kZXIYASABKAsyGy5pb3RhLmdycGMudjEudHlwZXMuQWRkcmVzc0gAiAEBEi0KBW93bmVyGAIgASgLMhkuaW90YS5ncnBjLnYxLnR5cGVzLk93bmVySAGIAQESNQoLb2JqZWN0X3R5cGUYAyABKAsyGy5pb3RhLmdycGMudjEudHlwZXMuVHlwZVRhZ0gCiAEBEjQKCW9iamVjdF9pZBgEIAEoCzIcLmlvdGEuZ3JwYy52MS50eXBlcy5PYmplY3RJZEgDiAEBEhQKB3ZlcnNpb24YBSABKARIBIgBARIvCgZkaWdlc3QYBiABKAsyGi5pb3RhLmdycGMudjEudHlwZXMuRGlnZXN0SAWIAQE6CIK1GAR3aXRoQgkKB19zZW5kZXJCCAoGX293bmVyQg4KDF9vYmplY3RfdHlwZUIMCgpfb2JqZWN0X2lkQgoKCF92ZXJzaW9uQgkKB19kaWdlc3QitAMKDE9iamVjdENoYW5nZRJECglwdWJsaXNoZWQYASABKAsyLy5pb3RhLmdycGMudjEudHJhbnNhY3Rpb24uT2JqZWN0Q2hhbmdlUHVibGlzaGVkSAASQAoHbXV0YXRlZBgCIAEoCzItLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5PYmplY3RDaGFuZ2VNdXRhdGVkSAASQAoHZGVsZXRlZBgDIAEoCzItLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5PYmplY3RDaGFuZ2VEZWxldGVkSAASQAoHd3JhcHBlZBgEIAEoCzItLmlvdGEuZ3JwYy52MS50cmFuc2FjdGlvbi5PYmplY3RDaGFuZ2VXcmFwcGVkSAASRAoJdW53cmFwcGVkGAUgASgLMi8uaW90YS5ncnBjLnYxLnRyYW5zYWN0aW9uLk9iamVjdENoYW5nZVVud3JhcHBlZEgAEkAKB2NyZWF0ZWQYBiABKAsyLS5pb3RhLmdycGMudjEudHJhbnNhY3Rpb24uT2JqZWN0Q2hhbmdlQ3JlYXRlZEgAOgiCtRgEd2l0aEIGCgRraW5kIl0KDU9iamVjdENoYW5nZXMSPgoOb2JqZWN0X2NoYW5nZXMYASADKAsyJi5pb3RhLmdycGMudjEudHJhbnNhY3Rpb24uT2JqZWN0Q2hhbmdlOgyCtRgEd2l0aJC1GAEibgoURXhlY3V0ZWRUcmFuc2FjdGlvbnMSTAoVZXhlY3V0ZWRfdHJhbnNhY3Rpb25zGAEgAygLMi0uaW90YS5ncnBjLnYxLnRyYW5zYWN0aW9uLkV4ZWN1dGVkVHJhbnNhY3Rpb246CIK1GAR3aXRoYgZwcm90bzM", [file_google_protobuf_timestamp, file_iota_grpc_options, file_iota_grpc_v1_bcs, file_iota_grpc_v1_event, file_iota_grpc_v1_object, file_iota_grpc_v1_signatures, file_iota_grpc_v1_types]); + +/** + * A transaction. + * + * @generated from message iota.grpc.v1.transaction.Transaction + */ +export type Transaction = Message<"iota.grpc.v1.transaction.Transaction"> & { + /** + * The digest of this Transaction. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 1; + */ + digest?: Digest | undefined; + + /** + * This Transaction serialized as BCS. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 2; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.Transaction. + * Use `create(TransactionSchema)` to create a new message. + */ +export const TransactionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 0); + +/** + * The effects of executing a transaction. + * + * @generated from message iota.grpc.v1.transaction.TransactionEffects + */ +export type TransactionEffects = Message<"iota.grpc.v1.transaction.TransactionEffects"> & { + /** + * The digest of this TransactionEffects. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 1; + */ + digest?: Digest | undefined; + + /** + * This TransactionEffects serialized as BCS. + * + * @generated from field: optional iota.grpc.v1.bcs.BcsData bcs = 2; + */ + bcs?: BcsData | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.TransactionEffects. + * Use `create(TransactionEffectsSchema)` to create a new message. + */ +export const TransactionEffectsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 1); + +/** + * iota.grpc.v1.event.Events emitted during the successful execution of a transaction. + * + * @generated from message iota.grpc.v1.transaction.TransactionEvents + */ +export type TransactionEvents = Message<"iota.grpc.v1.transaction.TransactionEvents"> & { + /** + * The digest of this TransactionEvents. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 1; + */ + digest?: Digest | undefined; + + /** + * List of events emitted by a transaction. + * + * @generated from field: optional iota.grpc.v1.event.Events events = 2; + */ + events?: Events | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.TransactionEvents. + * Use `create(TransactionEventsSchema)` to create a new message. + */ +export const TransactionEventsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 2); + +/** + * A transaction that has been executed, along with its signatures, effects, events and objects. + * + * @generated from message iota.grpc.v1.transaction.ExecutedTransaction + */ +export type ExecutedTransaction = Message<"iota.grpc.v1.transaction.ExecutedTransaction"> & { + /** + * The transaction itself. + * + * @generated from field: optional iota.grpc.v1.transaction.Transaction transaction = 1; + */ + transaction?: Transaction | undefined; + + /** + * List of user signatures that are used to authorize the + * execution of this transaction. + * + * @generated from field: optional iota.grpc.v1.signatures.UserSignatures signatures = 2; + */ + signatures?: UserSignatures | undefined; + + /** + * The `TransactionEffects` for this transaction. + * + * @generated from field: optional iota.grpc.v1.transaction.TransactionEffects effects = 3; + */ + effects?: TransactionEffects | undefined; + + /** + * The `TransactionEvents` for this transaction. + * + * This field might be empty, even if it was explicitly requested, if the + * transaction didn't produce any events. + * `iota.types.TransactionEffects.events_digest` is populated if the + * transaction produced any events. + * + * @generated from field: optional iota.grpc.v1.transaction.TransactionEvents events = 4; + */ + events?: TransactionEvents | undefined; + + /** + * The sequence number for the checkpoint that includes this transaction. + * + * @generated from field: optional uint64 checkpoint = 5; + */ + checkpoint?: bigint | undefined; + + /** + * The Unix timestamp of the checkpoint that includes this transaction. + * + * @generated from field: optional google.protobuf.Timestamp timestamp = 6; + */ + timestamp?: Timestamp | undefined; + + /** + * Set of input objects used by this transaction. + * + * The returned set is always complete: if the serving node no longer has + * one of the objects (e.g. pruned), requesting this field fails with + * `FAILED_PRECONDITION` instead of returning a silently shortened list. + * Narrow the read mask, or fetch objects individually via `GetObjects` + * for best-effort retrieval. + * + * @generated from field: optional iota.grpc.v1.object.Objects input_objects = 7; + */ + inputObjects?: Objects | undefined; + + /** + * Set of output objects produced by this transaction. + * + * The returned set is always complete: if the serving node no longer has + * one of the objects (e.g. pruned), requesting this field fails with + * `FAILED_PRECONDITION` instead of returning a silently shortened list. + * Narrow the read mask, or fetch objects individually via `GetObjects` + * for best-effort retrieval. + * + * @generated from field: optional iota.grpc.v1.object.Objects output_objects = 8; + */ + outputObjects?: Objects | undefined; + + /** + * The balance changes caused by this transaction. + * + * Derived from the transaction's effects and input/output objects. If the + * serving node no longer has a required object (e.g. pruned), requesting + * this field fails with `FAILED_PRECONDITION` instead of returning a + * silently wrong result; retry without this field in the read mask. + * + * @generated from field: optional iota.grpc.v1.transaction.BalanceChanges balance_changes = 9; + */ + balanceChanges?: BalanceChanges | undefined; + + /** + * The object changes caused by this transaction. + * + * Derived from the transaction's effects and input/output objects. If the + * serving node no longer has a required object (e.g. pruned), requesting + * this field fails with `FAILED_PRECONDITION` instead of returning a + * silently incomplete result; retry without this field in the read mask. + * + * @generated from field: optional iota.grpc.v1.transaction.ObjectChanges object_changes = 10; + */ + objectChanges?: ObjectChanges | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ExecutedTransaction. + * Use `create(ExecutedTransactionSchema)` to create a new message. + */ +export const ExecutedTransactionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 3); + +/** + * The delta, or change, in balance of a particular coin type for an owner. + * + * @generated from message iota.grpc.v1.transaction.BalanceChange + */ +export type BalanceChange = Message<"iota.grpc.v1.transaction.BalanceChange"> & { + /** + * The owner whose balance changed. + * + * @generated from field: optional iota.grpc.v1.types.Owner owner = 1; + */ + owner?: Owner | undefined; + + /** + * The type of the coin, e.g. `0x2::iota::IOTA`. + * + * @generated from field: optional iota.grpc.v1.types.TypeTag coin_type = 2; + */ + coinType?: TypeTag | undefined; + + /** + * The amount the balance changed by: a 128-bit signed integer in + * big-endian two's-complement encoding (exactly 16 bytes, + * `i128::to_be_bytes`). A negative amount means the net flow of value is + * away from the owner. + * + * @generated from field: optional bytes amount = 3; + */ + amount?: Uint8Array | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.BalanceChange. + * Use `create(BalanceChangeSchema)` to create a new message. + */ +export const BalanceChangeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 4); + +/** + * A list of balance changes. + * + * @generated from message iota.grpc.v1.transaction.BalanceChanges + */ +export type BalanceChanges = Message<"iota.grpc.v1.transaction.BalanceChanges"> & { + /** + * @generated from field: repeated iota.grpc.v1.transaction.BalanceChange balance_changes = 1; + */ + balanceChanges: BalanceChange[]; +}; + +/** + * Describes the message iota.grpc.v1.transaction.BalanceChanges. + * Use `create(BalanceChangesSchema)` to create a new message. + */ +export const BalanceChangesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 5); + +/** + * A package was published. + * + * @generated from message iota.grpc.v1.transaction.ObjectChangePublished + */ +export type ObjectChangePublished = Message<"iota.grpc.v1.transaction.ObjectChangePublished"> & { + /** + * The ID of the published package. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId package_id = 1; + */ + packageId?: ObjectId | undefined; + + /** + * The version of the published package. + * + * @generated from field: optional uint64 version = 2; + */ + version?: bigint | undefined; + + /** + * The digest of the published package. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 3; + */ + digest?: Digest | undefined; + + /** + * The set of modules in the published package. + * + * @generated from field: repeated string modules = 4; + */ + modules: string[]; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChangePublished. + * Use `create(ObjectChangePublishedSchema)` to create a new message. + */ +export const ObjectChangePublishedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 6); + +/** + * An object was mutated. + * + * @generated from message iota.grpc.v1.transaction.ObjectChangeMutated + */ +export type ObjectChangeMutated = Message<"iota.grpc.v1.transaction.ObjectChangeMutated"> & { + /** + * The sender of the transaction. + * + * @generated from field: optional iota.grpc.v1.types.Address sender = 1; + */ + sender?: Address | undefined; + + /** + * The owner of the object. + * + * @generated from field: optional iota.grpc.v1.types.Owner owner = 2; + */ + owner?: Owner | undefined; + + /** + * The type of the object, e.g. `0x2::coin::Coin<0x2::iota::IOTA>`. + * + * @generated from field: optional iota.grpc.v1.types.TypeTag object_type = 3; + */ + objectType?: TypeTag | undefined; + + /** + * The ID of the object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId object_id = 4; + */ + objectId?: ObjectId | undefined; + + /** + * The version of the object. + * + * @generated from field: optional uint64 version = 5; + */ + version?: bigint | undefined; + + /** + * The version of the object before it was mutated. + * + * @generated from field: optional uint64 previous_version = 6; + */ + previousVersion?: bigint | undefined; + + /** + * The digest of the object. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 7; + */ + digest?: Digest | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChangeMutated. + * Use `create(ObjectChangeMutatedSchema)` to create a new message. + */ +export const ObjectChangeMutatedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 7); + +/** + * An object was deleted. + * + * @generated from message iota.grpc.v1.transaction.ObjectChangeDeleted + */ +export type ObjectChangeDeleted = Message<"iota.grpc.v1.transaction.ObjectChangeDeleted"> & { + /** + * The sender of the transaction. + * + * @generated from field: optional iota.grpc.v1.types.Address sender = 1; + */ + sender?: Address | undefined; + + /** + * The type of the object, e.g. `0x2::coin::Coin<0x2::iota::IOTA>`. + * + * @generated from field: optional iota.grpc.v1.types.TypeTag object_type = 2; + */ + objectType?: TypeTag | undefined; + + /** + * The ID of the object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId object_id = 3; + */ + objectId?: ObjectId | undefined; + + /** + * The version the object was deleted at. + * + * @generated from field: optional uint64 version = 4; + */ + version?: bigint | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChangeDeleted. + * Use `create(ObjectChangeDeletedSchema)` to create a new message. + */ +export const ObjectChangeDeletedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 8); + +/** + * An object was wrapped inside another object. + * + * @generated from message iota.grpc.v1.transaction.ObjectChangeWrapped + */ +export type ObjectChangeWrapped = Message<"iota.grpc.v1.transaction.ObjectChangeWrapped"> & { + /** + * The sender of the transaction. + * + * @generated from field: optional iota.grpc.v1.types.Address sender = 1; + */ + sender?: Address | undefined; + + /** + * The type of the object, e.g. `0x2::coin::Coin<0x2::iota::IOTA>`. + * + * @generated from field: optional iota.grpc.v1.types.TypeTag object_type = 2; + */ + objectType?: TypeTag | undefined; + + /** + * The ID of the object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId object_id = 3; + */ + objectId?: ObjectId | undefined; + + /** + * The version the object was wrapped at. + * + * @generated from field: optional uint64 version = 4; + */ + version?: bigint | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChangeWrapped. + * Use `create(ObjectChangeWrappedSchema)` to create a new message. + */ +export const ObjectChangeWrappedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 9); + +/** + * An object was unwrapped from inside another object. + * + * @generated from message iota.grpc.v1.transaction.ObjectChangeUnwrapped + */ +export type ObjectChangeUnwrapped = Message<"iota.grpc.v1.transaction.ObjectChangeUnwrapped"> & { + /** + * The sender of the transaction. + * + * @generated from field: optional iota.grpc.v1.types.Address sender = 1; + */ + sender?: Address | undefined; + + /** + * The owner of the object. + * + * @generated from field: optional iota.grpc.v1.types.Owner owner = 2; + */ + owner?: Owner | undefined; + + /** + * The type of the object, e.g. `0x2::coin::Coin<0x2::iota::IOTA>`. + * + * @generated from field: optional iota.grpc.v1.types.TypeTag object_type = 3; + */ + objectType?: TypeTag | undefined; + + /** + * The ID of the object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId object_id = 4; + */ + objectId?: ObjectId | undefined; + + /** + * The version of the object. + * + * @generated from field: optional uint64 version = 5; + */ + version?: bigint | undefined; + + /** + * The digest of the object. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 6; + */ + digest?: Digest | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChangeUnwrapped. + * Use `create(ObjectChangeUnwrappedSchema)` to create a new message. + */ +export const ObjectChangeUnwrappedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 10); + +/** + * A new object was created. + * + * @generated from message iota.grpc.v1.transaction.ObjectChangeCreated + */ +export type ObjectChangeCreated = Message<"iota.grpc.v1.transaction.ObjectChangeCreated"> & { + /** + * The sender of the transaction. + * + * @generated from field: optional iota.grpc.v1.types.Address sender = 1; + */ + sender?: Address | undefined; + + /** + * The owner of the object. + * + * @generated from field: optional iota.grpc.v1.types.Owner owner = 2; + */ + owner?: Owner | undefined; + + /** + * The type of the object, e.g. `0x2::coin::Coin<0x2::iota::IOTA>`. + * + * @generated from field: optional iota.grpc.v1.types.TypeTag object_type = 3; + */ + objectType?: TypeTag | undefined; + + /** + * The ID of the object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId object_id = 4; + */ + objectId?: ObjectId | undefined; + + /** + * The version of the object. + * + * @generated from field: optional uint64 version = 5; + */ + version?: bigint | undefined; + + /** + * The digest of the object. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 6; + */ + digest?: Digest | undefined; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChangeCreated. + * Use `create(ObjectChangeCreatedSchema)` to create a new message. + */ +export const ObjectChangeCreatedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 11); + +/** + * A change to an object caused by executing a transaction. + * + * @generated from message iota.grpc.v1.transaction.ObjectChange + */ +export type ObjectChange = Message<"iota.grpc.v1.transaction.ObjectChange"> & { + /** + * @generated from oneof iota.grpc.v1.transaction.ObjectChange.kind + */ + kind: { + /** + * @generated from field: iota.grpc.v1.transaction.ObjectChangePublished published = 1; + */ + value: ObjectChangePublished; + case: "published"; + } | { + /** + * @generated from field: iota.grpc.v1.transaction.ObjectChangeMutated mutated = 2; + */ + value: ObjectChangeMutated; + case: "mutated"; + } | { + /** + * @generated from field: iota.grpc.v1.transaction.ObjectChangeDeleted deleted = 3; + */ + value: ObjectChangeDeleted; + case: "deleted"; + } | { + /** + * @generated from field: iota.grpc.v1.transaction.ObjectChangeWrapped wrapped = 4; + */ + value: ObjectChangeWrapped; + case: "wrapped"; + } | { + /** + * @generated from field: iota.grpc.v1.transaction.ObjectChangeUnwrapped unwrapped = 5; + */ + value: ObjectChangeUnwrapped; + case: "unwrapped"; + } | { + /** + * @generated from field: iota.grpc.v1.transaction.ObjectChangeCreated created = 6; + */ + value: ObjectChangeCreated; + case: "created"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChange. + * Use `create(ObjectChangeSchema)` to create a new message. + */ +export const ObjectChangeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 12); + +/** + * A list of object changes. + * + * @generated from message iota.grpc.v1.transaction.ObjectChanges + */ +export type ObjectChanges = Message<"iota.grpc.v1.transaction.ObjectChanges"> & { + /** + * @generated from field: repeated iota.grpc.v1.transaction.ObjectChange object_changes = 1; + */ + objectChanges: ObjectChange[]; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ObjectChanges. + * Use `create(ObjectChangesSchema)` to create a new message. + */ +export const ObjectChangesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 13); + +/** + * @generated from message iota.grpc.v1.transaction.ExecutedTransactions + */ +export type ExecutedTransactions = Message<"iota.grpc.v1.transaction.ExecutedTransactions"> & { + /** + * @generated from field: repeated iota.grpc.v1.transaction.ExecutedTransaction executed_transactions = 1; + */ + executedTransactions: ExecutedTransaction[]; +}; + +/** + * Describes the message iota.grpc.v1.transaction.ExecutedTransactions. + * Use `create(ExecutedTransactionsSchema)` to create a new message. + */ +export const ExecutedTransactionsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_transaction, 14); + diff --git a/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/types_pb.ts b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/types_pb.ts new file mode 100644 index 00000000..bc44b8db --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/grpc/generated/iota/grpc/v1/types_pb.ts @@ -0,0 +1,296 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.13.0 with parameter "target=ts,import_extension=js" +// @generated from file iota/grpc/v1/types.proto (package iota.grpc.v1.types, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_iota_grpc_options } from "../options_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file iota/grpc/v1/types.proto. + */ +export const file_iota_grpc_v1_types: GenFile = /*@__PURE__*/ + fileDesc("Chhpb3RhL2dycGMvdjEvdHlwZXMucHJvdG8SEmlvdGEuZ3JwYy52MS50eXBlcyIkCgdBZGRyZXNzEg8KB2FkZHJlc3MYASABKAw6CIK1GAR3aXRoIicKCE9iamVjdElkEhEKCW9iamVjdF9pZBgBIAEoDDoIgrUYBHdpdGgiIgoGRGlnZXN0Eg4KBmRpZ2VzdBgBIAEoDDoIgrUYBHdpdGgivQEKD09iamVjdFJlZmVyZW5jZRI0CglvYmplY3RfaWQYASABKAsyHC5pb3RhLmdycGMudjEudHlwZXMuT2JqZWN0SWRIAIgBARIUCgd2ZXJzaW9uGAIgASgESAGIAQESLwoGZGlnZXN0GAMgASgLMhouaW90YS5ncnBjLnYxLnR5cGVzLkRpZ2VzdEgCiAEBOgiCtRgEd2l0aEIMCgpfb2JqZWN0X2lkQgoKCF92ZXJzaW9uQgkKB19kaWdlc3QirAEKBU93bmVyEjQKDWFkZHJlc3Nfb3duZXIYASABKAsyGy5pb3RhLmdycGMudjEudHlwZXMuQWRkcmVzc0gAEjQKDG9iamVjdF9vd25lchgCIAEoCzIcLmlvdGEuZ3JwYy52MS50eXBlcy5PYmplY3RJZEgAEhAKBnNoYXJlZBgDIAEoBEgAEhMKCWltbXV0YWJsZRgEIAEoCEgAOgiCtRgEd2l0aEIGCgRraW5kIkAKDVR5cGVUYWdWZWN0b3ISLwoKaW5uZXJfdHlwZRgBIAEoCzIbLmlvdGEuZ3JwYy52MS50eXBlcy5UeXBlVGFnIiMKDVR5cGVUYWdTdHJ1Y3QSEgoKc3RydWN0X3RhZxgBIAEoCSK7AgoHVHlwZVRhZxISCghib29sX3RhZxgBIAEoCEgAEhAKBnU4X3RhZxgCIAEoCEgAEhEKB3UxNl90YWcYAyABKAhIABIRCgd1MzJfdGFnGAQgASgISAASEQoHdTY0X3RhZxgFIAEoCEgAEhIKCHUxMjhfdGFnGAYgASgISAASEgoIdTI1Nl90YWcYByABKAhIABIVCgthZGRyZXNzX3RhZxgIIAEoCEgAEhQKCnNpZ25lcl90YWcYCSABKAhIABI3Cgp2ZWN0b3JfdGFnGAogASgLMiEuaW90YS5ncnBjLnYxLnR5cGVzLlR5cGVUYWdWZWN0b3JIABI3CgpzdHJ1Y3RfdGFnGAsgASgLMiEuaW90YS5ncnBjLnYxLnR5cGVzLlR5cGVUYWdTdHJ1Y3RIAEIKCgh0eXBlX3RhZyI6CghUeXBlVGFncxIuCgl0eXBlX3RhZ3MYASADKAsyGy5pb3RhLmdycGMudjEudHlwZXMuVHlwZVRhZ2IGcHJvdG8z", [file_iota_grpc_options]); + +/** + * 32-byte address type for IOTA account addresses. + * + * @generated from message iota.grpc.v1.types.Address + */ +export type Address = Message<"iota.grpc.v1.types.Address"> & { + /** + * @generated from field: bytes address = 1; + */ + address: Uint8Array; +}; + +/** + * Describes the message iota.grpc.v1.types.Address. + * Use `create(AddressSchema)` to create a new message. + */ +export const AddressSchema: GenMessage
= /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 0); + +/** + * 32-byte identifier for on-chain objects and packages. + * + * @generated from message iota.grpc.v1.types.ObjectId + */ +export type ObjectId = Message<"iota.grpc.v1.types.ObjectId"> & { + /** + * @generated from field: bytes object_id = 1; + */ + objectId: Uint8Array; +}; + +/** + * Describes the message iota.grpc.v1.types.ObjectId. + * Use `create(ObjectIdSchema)` to create a new message. + */ +export const ObjectIdSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 1); + +/** + * Generic 32-byte digest (used for transactions, objects, etc.) + * + * @generated from message iota.grpc.v1.types.Digest + */ +export type Digest = Message<"iota.grpc.v1.types.Digest"> & { + /** + * @generated from field: bytes digest = 1; + */ + digest: Uint8Array; +}; + +/** + * Describes the message iota.grpc.v1.types.Digest. + * Use `create(DigestSchema)` to create a new message. + */ +export const DigestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 2); + +/** + * Reference to an object. + * + * @generated from message iota.grpc.v1.types.ObjectReference + */ +export type ObjectReference = Message<"iota.grpc.v1.types.ObjectReference"> & { + /** + * The object id of this object. + * + * @generated from field: optional iota.grpc.v1.types.ObjectId object_id = 1; + */ + objectId?: ObjectId | undefined; + + /** + * The version of this object. + * + * @generated from field: optional uint64 version = 2; + */ + version?: bigint | undefined; + + /** + * The digest of this object. + * + * @generated from field: optional iota.grpc.v1.types.Digest digest = 3; + */ + digest?: Digest | undefined; +}; + +/** + * Describes the message iota.grpc.v1.types.ObjectReference. + * Use `create(ObjectReferenceSchema)` to create a new message. + */ +export const ObjectReferenceSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 3); + +/** + * Ownership information for an object. + * + * @generated from message iota.grpc.v1.types.Owner + */ +export type Owner = Message<"iota.grpc.v1.types.Owner"> & { + /** + * @generated from oneof iota.grpc.v1.types.Owner.kind + */ + kind: { + /** + * Object is exclusively owned by a single address, and is mutable. + * + * @generated from field: iota.grpc.v1.types.Address address_owner = 1; + */ + value: Address; + case: "addressOwner"; + } | { + /** + * Object is exclusively owned by a single object, and is mutable. + * + * @generated from field: iota.grpc.v1.types.ObjectId object_owner = 2; + */ + value: ObjectId; + case: "objectOwner"; + } | { + /** + * Object is shared and can be used by any address. The value is the + * version at which the object became shared. + * + * @generated from field: uint64 shared = 3; + */ + value: bigint; + case: "shared"; + } | { + /** + * Object is immutable, and hence ownership doesn't matter. + * + * @generated from field: bool immutable = 4; + */ + value: boolean; + case: "immutable"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.types.Owner. + * Use `create(OwnerSchema)` to create a new message. + */ +export const OwnerSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 4); + +/** + * @generated from message iota.grpc.v1.types.TypeTagVector + */ +export type TypeTagVector = Message<"iota.grpc.v1.types.TypeTagVector"> & { + /** + * @generated from field: iota.grpc.v1.types.TypeTag inner_type = 1; + */ + innerType?: TypeTag | undefined; +}; + +/** + * Describes the message iota.grpc.v1.types.TypeTagVector. + * Use `create(TypeTagVectorSchema)` to create a new message. + */ +export const TypeTagVectorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 5); + +/** + * @generated from message iota.grpc.v1.types.TypeTagStruct + */ +export type TypeTagStruct = Message<"iota.grpc.v1.types.TypeTagStruct"> & { + /** + * @generated from field: string struct_tag = 1; + */ + structTag: string; +}; + +/** + * Describes the message iota.grpc.v1.types.TypeTagStruct. + * Use `create(TypeTagStructSchema)` to create a new message. + */ +export const TypeTagStructSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 6); + +/** + * @generated from message iota.grpc.v1.types.TypeTag + */ +export type TypeTag = Message<"iota.grpc.v1.types.TypeTag"> & { + /** + * @generated from oneof iota.grpc.v1.types.TypeTag.type_tag + */ + typeTag: { + /** + * @generated from field: bool bool_tag = 1; + */ + value: boolean; + case: "boolTag"; + } | { + /** + * @generated from field: bool u8_tag = 2; + */ + value: boolean; + case: "u8Tag"; + } | { + /** + * @generated from field: bool u16_tag = 3; + */ + value: boolean; + case: "u16Tag"; + } | { + /** + * @generated from field: bool u32_tag = 4; + */ + value: boolean; + case: "u32Tag"; + } | { + /** + * @generated from field: bool u64_tag = 5; + */ + value: boolean; + case: "u64Tag"; + } | { + /** + * @generated from field: bool u128_tag = 6; + */ + value: boolean; + case: "u128Tag"; + } | { + /** + * @generated from field: bool u256_tag = 7; + */ + value: boolean; + case: "u256Tag"; + } | { + /** + * @generated from field: bool address_tag = 8; + */ + value: boolean; + case: "addressTag"; + } | { + /** + * @generated from field: bool signer_tag = 9; + */ + value: boolean; + case: "signerTag"; + } | { + /** + * @generated from field: iota.grpc.v1.types.TypeTagVector vector_tag = 10; + */ + value: TypeTagVector; + case: "vectorTag"; + } | { + /** + * @generated from field: iota.grpc.v1.types.TypeTagStruct struct_tag = 11; + */ + value: TypeTagStruct; + case: "structTag"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message iota.grpc.v1.types.TypeTag. + * Use `create(TypeTagSchema)` to create a new message. + */ +export const TypeTagSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 7); + +/** + * @generated from message iota.grpc.v1.types.TypeTags + */ +export type TypeTags = Message<"iota.grpc.v1.types.TypeTags"> & { + /** + * @generated from field: repeated iota.grpc.v1.types.TypeTag type_tags = 1; + */ + typeTags: TypeTag[]; +}; + +/** + * Describes the message iota.grpc.v1.types.TypeTags. + * Use `create(TypeTagsSchema)` to create a new message. + */ +export const TypeTagsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_iota_grpc_v1_types, 8); + diff --git a/bindings/wasm/poi_wasm/lib/index.ts b/bindings/wasm/poi_wasm/lib/index.ts new file mode 100644 index 00000000..e8b08131 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/index.ts @@ -0,0 +1,11 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export { PoiClient, type PoiClientOptions } from "./poi-client.js"; +export { + Committee, + CommitteeResolution, + CommitteeResolver, + Proof, + type ProofBuilder, +} from "../node/poi_wasm.js"; diff --git a/bindings/wasm/poi_wasm/lib/ledger-source.ts b/bindings/wasm/poi_wasm/lib/ledger-source.ts new file mode 100644 index 00000000..9bcffdee --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/ledger-source.ts @@ -0,0 +1,264 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { Status } from "./grpc/generated/google/rpc/status_pb.js"; +import type { ExecutedTransaction } from "./grpc/generated/iota/grpc/v1/transaction_pb.js"; +import { + createIotaGrpcClient, + type IotaGrpcClient, + type IotaGrpcClientOptions, +} from "./client.js"; +import type { + CheckpointEvidence, + CheckpointSummaryEvidence, + Committee, + LedgerSource as LedgerSourceContract, + TransactionEvidence, +} from "./source-types.js"; + +const CHAIN_IDENTIFIER_FIELDS = ["chain_id"]; +const COMMITTEE_FIELDS = ["committee"]; +const CURRENT_EPOCH_FIELDS = ["epoch"]; +const EPOCH_CLOSE_SUMMARY_FIELDS = ["epoch_close_proof.checkpoint"]; +const OBJECT_PROOF_FIELDS = ["bcs"]; +const TRANSACTION_PROOF_FIELDS = [ + "transaction.bcs", + "signatures", + "effects.bcs", + "events.digest", + "events.events.bcs", + "checkpoint", +]; +const CHECKPOINT_PROOF_FIELDS = [ + "checkpoint.summary.bcs", + "checkpoint.signature", + "checkpoint.contents.bcs", +]; + +/** + * Internal implementation of the ledger reads required by Proof of Inclusion. + * + * The generated client owns gRPC and protobuf. This class narrows its responses + * to BCS evidence that can cross the JavaScript/WASM boundary. + */ +export class LedgerSource implements LedgerSourceContract { + readonly #client: IotaGrpcClient; + + public constructor(endpoint: string, options: IotaGrpcClientOptions = {}) { + this.#client = createIotaGrpcClient(endpoint, options); + } + + public async chainIdentifier(): Promise { + const response = await this.#client.getServiceInfo({ + readMask: { paths: CHAIN_IDENTIFIER_FIELDS }, + }); + + return response.chainId!.digest!; + } + + public async transaction( + digest: Uint8Array, + ): Promise { + let transaction: ExecutedTransaction | undefined; + + for await (const response of this.#client.getTransactions({ + requests: { + requests: [{ digest: { digest } }], + }, + readMask: { paths: TRANSACTION_PROOF_FIELDS }, + })) { + for (const result of response.transactionResults) { + if (result.result.case === "error") { + throw statusError("getTransactions", result.result.value); + } + + if (result.result.case !== "executedTransaction") { + throw new Error( + "getTransactions returned a result without a transaction or error", + ); + } + + if (transaction) { + throw new Error( + "getTransactions returned more than one transaction for one digest", + ); + } + + transaction = result.result.value; + } + } + + if (!transaction) { + return undefined; + } + + const signatures = transaction.signatures!; + const transactionEvents = transaction.events?.events?.events; + + return { + transactionBcs: transaction.transaction!.bcs!.data!, + signaturesBcs: signatures.signatures.map( + (signature) => signature.bcs!.data!, + ), + effectsBcs: transaction.effects!.bcs!.data!, + eventsBcs: transactionEvents?.map((event) => event.bcs!.data!), + checkpointSequenceNumber: transaction.checkpoint!, + }; + } + + public async object( + objectId: Uint8Array, + version?: bigint, + ): Promise { + let objectBcs: Uint8Array | undefined; + + for await (const response of this.#client.getObjects({ + requests: { + requests: [ + { + objectRef: { + objectId: { objectId }, + version, + }, + }, + ], + }, + readMask: { paths: OBJECT_PROOF_FIELDS }, + })) { + for (const result of response.objects) { + if (result.result.case === "error") { + throw statusError("getObjects", result.result.value); + } + + if (result.result.case !== "object") { + throw new Error( + "getObjects returned a result without an object or error", + ); + } + + if (objectBcs) { + throw new Error("getObjects returned more than one object for one ID"); + } + + objectBcs = result.result.value.bcs!.data!; + } + } + + return objectBcs; + } + + public async checkpoint( + sequenceNumber: bigint, + ): Promise { + let checkpoint: CheckpointEvidence | undefined; + let reachedEnd = false; + + for await (const response of this.#client.getCheckpoint({ + checkpointId: { + case: "sequenceNumber", + value: sequenceNumber, + }, + readMask: { paths: CHECKPOINT_PROOF_FIELDS }, + })) { + if (response.payload.case === "checkpoint") { + if (checkpoint) { + throw new Error( + "getCheckpoint returned more than one checkpoint for one sequence number", + ); + } + + const value = response.payload.value; + + if ( + value.sequenceNumber !== undefined && + value.sequenceNumber !== sequenceNumber + ) { + throw new Error( + `getCheckpoint returned sequence number ${value.sequenceNumber}, expected ${sequenceNumber}`, + ); + } + + checkpoint = { + summaryBcs: value.summary!.bcs!.data!, + signatureBcs: value.signature!.bcs!.data!, + contentsBcs: value.contents!.bcs!.data!, + }; + } else if (response.payload.case === "endMarker") { + const returnedSequenceNumber = response.payload.value.sequenceNumber; + + if ( + returnedSequenceNumber !== undefined && + returnedSequenceNumber !== sequenceNumber + ) { + throw new Error( + `getCheckpoint ended at sequence number ${returnedSequenceNumber}, expected ${sequenceNumber}`, + ); + } + + reachedEnd = true; + } + } + + if (!checkpoint) { + throw new Error( + `getCheckpoint returned no checkpoint for sequence number ${sequenceNumber}`, + ); + } + + if (!reachedEnd) { + throw new Error( + `getCheckpoint did not finish sequence number ${sequenceNumber}`, + ); + } + + return checkpoint; + } + + public async committee(epoch: bigint): Promise { + const response = await this.#client.getEpoch({ + epoch, + readMask: { paths: COMMITTEE_FIELDS }, + }); + const committee = response.epoch!.committee!; + + return { + members: committee.members!.members.map((member) => ({ + publicKey: member.publicKey!, + weight: member.weight!, + })), + }; + } + + public async currentEpoch(): Promise { + const response = await this.#client.getServiceInfo({ + readMask: { paths: CURRENT_EPOCH_FIELDS }, + }); + + return response.epoch; + } + + public async epochCloseSummary( + epoch: bigint, + ): Promise { + const response = await this.#client.getEpoch({ + epoch, + readMask: { paths: EPOCH_CLOSE_SUMMARY_FIELDS }, + }); + const checkpoint = response.epoch?.epochCloseProof?.checkpoint; + + if (!checkpoint) { + return undefined; + } + + return { + summaryBcs: checkpoint.summary!.bcs!.data!, + signatureBcs: checkpoint.signature!.bcs!.data!, + }; + } +} + +function statusError(method: string, status: Status): Error { + const details = status.message ? `: ${status.message}` : ""; + + return new Error(`${method} failed with gRPC status ${status.code}${details}`); +} diff --git a/bindings/wasm/poi_wasm/lib/poi-client.ts b/bindings/wasm/poi_wasm/lib/poi-client.ts new file mode 100644 index 00000000..6d122386 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/poi-client.ts @@ -0,0 +1,68 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { Transport } from "@connectrpc/connect"; + +import { + type CommitteeResolution, + CommitteeResolver, + ProofBuilder, +} from "../node/poi_wasm.js"; +import { LedgerSource } from "./ledger-source.js"; + +const MAINNET_ENDPOINT = "https://grpc.mainnet.iota.cafe:443"; +const TESTNET_ENDPOINT = "https://grpc.testnet.iota.cafe:443"; +const DEVNET_ENDPOINT = "https://grpc.devnet.iota.cafe:443"; + +/** + * Options for configuring the ledger connection used by a {@link PoiClient}. + */ +export interface PoiClientOptions { + /** Default timeout applied to ledger requests, in milliseconds. */ + defaultTimeoutMs?: number; + /** Maximum response size accepted from the ledger, in bytes. */ + readMaxBytes?: number; + /** Custom ConnectRPC transport, primarily for advanced use and testing. */ + transport?: Transport; +} + +/** + * Creates Proof of Inclusion builders backed by an IOTA ledger endpoint. + * + * Use one of the named public-network constructors, or construct a client with + * an explicit endpoint for a private node, archive, local network, or + * alternative endpoint. + */ +export class PoiClient { + readonly #source: LedgerSource; + + /** Creates a client connected to an explicit IOTA gRPC endpoint. */ + public constructor(endpoint: string, options: PoiClientOptions = {}) { + this.#source = new LedgerSource(endpoint, options); + } + + /** Creates a client connected to the public IOTA mainnet gRPC endpoint. */ + public static mainnet(options: PoiClientOptions = {}): PoiClient { + return new PoiClient(MAINNET_ENDPOINT, options); + } + + /** Creates a client connected to the public IOTA testnet gRPC endpoint. */ + public static testnet(options: PoiClientOptions = {}): PoiClient { + return new PoiClient(TESTNET_ENDPOINT, options); + } + + /** Creates a client connected to the public IOTA devnet gRPC endpoint. */ + public static devnet(options: PoiClientOptions = {}): PoiClient { + return new PoiClient(DEVNET_ENDPOINT, options); + } + + /** Creates a fresh builder for one Proof of Inclusion. */ + public proof(): ProofBuilder { + return new ProofBuilder(this.#source); + } + + /** Creates a verifier using the selected committee-resolution strategy. */ + public verifier(resolution: CommitteeResolution): CommitteeResolver { + return new CommitteeResolver(this.#source, resolution); + } +} diff --git a/bindings/wasm/poi_wasm/lib/source-types.ts b/bindings/wasm/poi_wasm/lib/source-types.ts new file mode 100644 index 00000000..877a4c28 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/source-types.ts @@ -0,0 +1,55 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/** + * Serialized transaction data needed by `poi-rs` to build a proof. + * + * These values are opaque BCS bytes. JavaScript fetches them, while Rust + * remains responsible for decoding and validating them. + */ +export interface TransactionEvidence { + transactionBcs: Uint8Array; + signaturesBcs: Uint8Array[]; + effectsBcs: Uint8Array; + eventsBcs?: Uint8Array[]; + checkpointSequenceNumber: bigint; +} + +/** Serialized checkpoint data needed by `poi-rs` to authenticate a transaction. */ +export interface CheckpointEvidence { + summaryBcs: Uint8Array; + signatureBcs: Uint8Array; + contentsBcs: Uint8Array; +} + +/** Serialized certified checkpoint summary used to authenticate an epoch handoff. */ +export interface CheckpointSummaryEvidence { + summaryBcs: Uint8Array; + signatureBcs: Uint8Array; +} + +/** A validator entry reported by a trusted IOTA node. */ +export interface CommitteeMember { + publicKey: Uint8Array; + weight: bigint; +} + +/** Committee data reported by a trusted IOTA node. */ +export interface Committee { + members: CommitteeMember[]; +} + +/** Internal JavaScript contract consumed by the WASM proof builder. */ +export interface LedgerSource { + chainIdentifier(): Promise; + transaction( + digest: Uint8Array, + ): Promise; + object(objectId: Uint8Array, version?: bigint): Promise; + checkpoint(sequenceNumber: bigint): Promise; + committee(epoch: bigint): Promise; + currentEpoch(): Promise; + epochCloseSummary( + epoch: bigint, + ): Promise; +} diff --git a/bindings/wasm/poi_wasm/lib/tsconfig.json b/bindings/wasm/poi_wasm/lib/tsconfig.json new file mode 100644 index 00000000..eabf6698 --- /dev/null +++ b/bindings/wasm/poi_wasm/lib/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "declaration": true, + "module": "CommonJS", + "moduleResolution": "Node", + "noEmit": false, + "outDir": "../node", + "rootDir": ".", + "verbatimModuleSyntax": false + }, + "include": ["**/*.ts"] +} diff --git a/bindings/wasm/poi_wasm/package-lock.json b/bindings/wasm/poi_wasm/package-lock.json new file mode 100644 index 00000000..8f43d166 --- /dev/null +++ b/bindings/wasm/poi_wasm/package-lock.json @@ -0,0 +1,1094 @@ +{ + "name": "@iota/poi-wasm", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@iota/poi-wasm", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.13.0", + "@connectrpc/connect": "2.1.2", + "@connectrpc/connect-node": "2.1.2" + }, + "devDependencies": { + "@bufbuild/buf": "1.72.0", + "@bufbuild/protoc-gen-es": "2.13.0", + "@types/node": "22.20.1", + "rimraf": "6.0.1", + "tsx": "4.23.1", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@bufbuild/buf": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.72.0.tgz", + "integrity": "sha512-BwBKTX/WXkhAhqWJGrEKnqU03/4tK1O0OozSlwUMBCOEo8pLL3xu3M24RT3+umExEeM0wjlANO6axqGWMqtt4Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "buf": "bin/buf", + "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", + "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@bufbuild/buf-darwin-arm64": "1.72.0", + "@bufbuild/buf-darwin-x64": "1.72.0", + "@bufbuild/buf-linux-aarch64": "1.72.0", + "@bufbuild/buf-linux-armv7": "1.72.0", + "@bufbuild/buf-linux-x64": "1.72.0", + "@bufbuild/buf-win32-arm64": "1.72.0", + "@bufbuild/buf-win32-x64": "1.72.0" + } + }, + "node_modules/@bufbuild/buf-darwin-arm64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.72.0.tgz", + "integrity": "sha512-rKHRvjwAThapxIoOn92vIoTjYSz5FmRemDRLU4BYT4T6QWMEC13PM3/pPnqVgsNKZ5aW7iYDm9ztnisEqSi5yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-darwin-x64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.72.0.tgz", + "integrity": "sha512-4TQ1AGft8sGspNg9NMsEjsKKis7nGaVV8tZLnNa3cKUBmx22gwOnB6VRhgKWwjf+BDqr85lUEzQ6wHCboNUutg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-aarch64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.72.0.tgz", + "integrity": "sha512-cbIsUcgM5bHhbZWcDaAXqaYOAi8N0c0u+NiDydwVmZ04Et3s1EZ3TDqfQDRzwvoBPDP+lsO6YuTRXX6nI28x4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-armv7": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.72.0.tgz", + "integrity": "sha512-v/bXVsFL8YNm2HgosGb9r3+nAt4jQiUc3r3JipYuiVY3DAJZAjoEvcak6/BkxQMTEQz9Zb8gRRlule9IFkbc5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-x64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.72.0.tgz", + "integrity": "sha512-4xHGXEjqFxo1wX1zMGq4CzhYt5++nrj4C7k30j+YmGtvqCnipfdSe+V6kknBYRfYswVZEUwUbQOh6pnMTcGcrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-win32-arm64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.72.0.tgz", + "integrity": "sha512-WH7ClsoB9A0e/5fFhx0DLqLzillYPRdHBhlwzihgvjGci0bBdyJVHSQGf0B9uspCMU6sn6W/N1S9/2vvQBNMug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-win32-x64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.72.0.tgz", + "integrity": "sha512-X3eWqFzhDmu8CYQZz+Fu7i+PgH+yUl8UwJ5+x+bhZRYAIdcijikthodk60c5u/qq42m1Z2XAnAGyp/mTf7IffA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.13.0.tgz", + "integrity": "sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoc-gen-es": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.13.0.tgz", + "integrity": "sha512-ylI1vrLksdnXrVZRs9xGxmrQxKGhUm6pPszv26kqBvNiO3qPTktk+hgfwbLISBY4M/reShkT2dFLGT9fbydBXg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.13.0", + "@bufbuild/protoplugin": "2.13.0" + }, + "bin": { + "protoc-gen-es": "bin/protoc-gen-es" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "2.13.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + } + } + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.13.0.tgz", + "integrity": "sha512-32eMChKaL/A8Hh5AfMmXSdnuyznN85uoEjoyWiWeRrvtQOtpqX/v1R9PDe0g9vMIgzznK9inMT3CUaal0kjLUQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.13.0", + "@typescript/vfs": "^1.6.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@connectrpc/connect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.2.tgz", + "integrity": "sha512-MXkBijtcX09R10Eb6sFeIetc6w6746eio6xtfuyVOH7oQAacT1X0GzMIQFux6Qy8cq3W/T5qX5Bei8YbFtmRGA==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-2.1.2.tgz", + "integrity": "sha512-+i/aAOpsI8sIx1mbYp6d99zvxaUSF6t/jP9Ux9maAmjsZPgmIQ3JuIeYi0zJIP9zlCnBlJjkpPosshCgdRuThQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0", + "@connectrpc/connect": "2.1.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript/vfs": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz", + "integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/bindings/wasm/poi_wasm/package.json b/bindings/wasm/poi_wasm/package.json new file mode 100644 index 00000000..3d1b366d --- /dev/null +++ b/bindings/wasm/poi_wasm/package.json @@ -0,0 +1,48 @@ +{ + "name": "@iota/poi-wasm", + "version": "0.0.0", + "private": true, + "description": "Node.js WASM bindings for the IOTA Proof of Inclusion Package.", + "license": "Apache-2.0", + "type": "module", + "main": "./node/index.js", + "types": "./node/index.d.ts", + "exports": { + ".": { + "types": "./node/index.d.ts", + "import": "./node/index.js", + "require": "./node/index.js" + } + }, + "files": [ + "node/*" + ], + "scripts": { + "build:src:nodejs": "cargo build --lib --release --target wasm32-unknown-unknown --target-dir ../target", + "prebundle:nodejs": "rimraf node", + "bundle:nodejs": "wasm-bindgen ../target/wasm32-unknown-unknown/release/poi_wasm.wasm --typescript --target nodejs --out-dir node && node ../build/node poi_wasm --skip-fetch-polyfill && tsc --project ./lib/tsconfig.json", + "build:nodejs": "npm run build:src:nodejs && npm run bundle:nodejs && wasm-opt -O node/poi_wasm_bg.wasm -o node/poi_wasm_bg.wasm", + "grpc:schema:update": "node scripts/update-iota-schema.mjs", + "grpc:generate": "node scripts/generate-grpc.mjs", + "typecheck": "tsc --noEmit", + "test": "tsx --test tests/*.test.ts", + "verify": "npm run grpc:generate && npm run build:nodejs && npm run typecheck && npm test", + "example:service-info": "tsx examples/service-info.ts" + }, + "dependencies": { + "@bufbuild/protobuf": "2.13.0", + "@connectrpc/connect": "2.1.2", + "@connectrpc/connect-node": "2.1.2" + }, + "devDependencies": { + "@bufbuild/buf": "1.72.0", + "@bufbuild/protoc-gen-es": "2.13.0", + "@types/node": "22.20.1", + "rimraf": "6.0.1", + "tsx": "4.23.1", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=20" + } +} diff --git a/bindings/wasm/poi_wasm/rust-toolchain.toml b/bindings/wasm/poi_wasm/rust-toolchain.toml new file mode 100644 index 00000000..825d39b5 --- /dev/null +++ b/bindings/wasm/poi_wasm/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "stable" +components = ["rustfmt"] +targets = ["wasm32-unknown-unknown"] +profile = "minimal" diff --git a/bindings/wasm/poi_wasm/scripts/generate-grpc.mjs b/bindings/wasm/poi_wasm/scripts/generate-grpc.mjs new file mode 100644 index 00000000..6e0e01c0 --- /dev/null +++ b/bindings/wasm/poi_wasm/scripts/generate-grpc.mjs @@ -0,0 +1,49 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const lock = JSON.parse( + await readFile(resolve(packageRoot, "grpc/iota-schema.lock.json"), "utf8"), +); +const image = await readFile(resolve(packageRoot, "grpc/iota-ledger.binpb")); +const actualDigest = `sha256:${createHash("sha256").update(image).digest("hex")}`; + +if (lock.imageSha256 !== actualDigest) { + throw new Error( + `IOTA schema image digest mismatch: expected ${lock.imageSha256}, received ${actualDigest}`, + ); +} + +await run("buf", ["generate", "--template", "grpc/buf.gen.yaml"]); + +function run(command, args) { + return new Promise((resolvePromise, reject) => { + const child = spawn(command, args, { + cwd: packageRoot, + env: { + ...process.env, + BUF_CACHE_DIR: resolve(packageRoot, ".cache/buf"), + }, + stdio: "inherit", + shell: false, + }); + + child.once("error", (error) => { + reject(new Error(`failed to start ${command}: ${error.message}`, { cause: error })); + }); + child.once("exit", (code, signal) => { + if (code === 0) { + resolvePromise(); + } else { + reject(new Error(`${command} failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`)); + } + }); + }); +} + diff --git a/bindings/wasm/poi_wasm/scripts/update-iota-schema.mjs b/bindings/wasm/poi_wasm/scripts/update-iota-schema.mjs new file mode 100644 index 00000000..0c0cd50b --- /dev/null +++ b/bindings/wasm/poi_wasm/scripts/update-iota-schema.mjs @@ -0,0 +1,77 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFile, rename, rm, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const lockPath = resolve(packageRoot, "grpc/iota-schema.lock.json"); +const imagePath = resolve(packageRoot, "grpc/iota-ledger.binpb"); +const temporaryImagePath = `${imagePath}.tmp`; +const lock = JSON.parse(await readFile(lockPath, "utf8")); +const revision = process.argv[2] ?? lock.revision; + +if (!/^[0-9a-f]{40}$/.test(revision)) { + throw new Error("IOTA SDK revision must be a complete 40-character lowercase Git commit"); +} + +if (lock.repository !== "https://github.com/iotaledger/iota-rust-sdk") { + throw new Error(`refusing to download schema from unapproved repository: ${lock.repository}`); +} + +const archive = `${lock.repository}/archive/${revision}.tar.gz`; +const input = `${archive}#strip_components=1,subdir=${lock.protoRoot}`; +const args = ["build", input, "--timeout", "60s", "--output", temporaryImagePath]; + +for (const entrypoint of lock.entrypoints) { + args.push("--path", entrypoint); +} + +await rm(temporaryImagePath, { force: true }); + +try { + await run("buf", args); + + const image = await readFile(temporaryImagePath); + const imageSha256 = `sha256:${createHash("sha256").update(image).digest("hex")}`; + + await rename(temporaryImagePath, imagePath); + await writeFile( + lockPath, + `${JSON.stringify({ ...lock, revision, imageSha256 }, null, 2)}\n`, + ); + + console.log(`Updated IOTA gRPC schema to ${revision}`); + console.log(`Buf image: ${imageSha256}`); +} finally { + await rm(temporaryImagePath, { force: true }); +} + +function run(command, args) { + return new Promise((resolvePromise, reject) => { + const child = spawn(command, args, { + cwd: packageRoot, + env: { + ...process.env, + BUF_CACHE_DIR: resolve(packageRoot, ".cache/buf"), + }, + stdio: "inherit", + shell: false, + }); + + child.once("error", (error) => { + reject(new Error(`failed to start ${command}: ${error.message}`, { cause: error })); + }); + child.once("exit", (code, signal) => { + if (code === 0) { + resolvePromise(); + } else { + reject(new Error(`${command} failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`)); + } + }); + }); +} + diff --git a/bindings/wasm/poi_wasm/src/committee.rs b/bindings/wasm/poi_wasm/src/committee.rs new file mode 100644 index 00000000..19da2df0 --- /dev/null +++ b/bindings/wasm/poi_wasm/src/committee.rs @@ -0,0 +1,131 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use iota_types::{ + base_types::AuthorityName, + committee::{Committee, EpochId, StakeUnit, TOTAL_VOTING_POWER}, +}; +use js_sys::Uint8Array; +use poi_rs::{CommitteeResolution, CommitteeResolver}; +use serde::Deserialize; +use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; + +use crate::{ + error::{PoiError, WasmResult}, + proof::WasmProof, + source::LedgerSource, +}; + +#[derive(Deserialize)] +struct CommitteeJson { + epoch: EpochId, + voting_rights: Vec<(AuthorityName, StakeUnit)>, +} + +/// A validator committee used to verify a Proof of Inclusion proof. +#[wasm_bindgen(js_name = Committee)] +pub struct WasmCommittee(Committee); + +impl WasmCommittee { + pub(crate) const fn inner(&self) -> &Committee { + &self.0 + } +} + +#[wasm_bindgen(js_class = Committee)] +impl WasmCommittee { + /// Deserializes and validates a committee from its Rust JSON representation. + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(json: &str) -> Result { + let committee: CommitteeJson = serde_json::from_str(json) + .map_err(|error| PoiError::invalid_input(format!("invalid committee JSON: {error}"))) + .wasm_result()?; + let mut voting_rights = BTreeMap::new(); + let mut total_voting_power = 0_u64; + + for (authority, voting_power) in committee.voting_rights { + if voting_rights.insert(authority, voting_power).is_some() { + return Err(PoiError::invalid_input("committee contains a duplicate authority")).wasm_result(); + } + total_voting_power = total_voting_power + .checked_add(voting_power) + .ok_or_else(|| PoiError::invalid_input("committee voting power exceeds the supported range")) + .wasm_result()?; + } + + if total_voting_power != TOTAL_VOTING_POWER { + return Err(PoiError::invalid_input(format!( + "committee voting power must total {TOTAL_VOTING_POWER}, received {total_voting_power}" + ))) + .wasm_result(); + } + + Ok(WasmCommittee(Committee::new(committee.epoch, voting_rights))) + } + + /// Returns the epoch governed by this committee. + #[wasm_bindgen(getter)] + pub fn epoch(&self) -> u64 { + self.0.epoch() + } +} + +/// Selects how a resolver establishes trust in committee data. +#[wasm_bindgen(js_name = CommitteeResolution)] +pub struct WasmCommitteeResolution(CommitteeResolution); + +#[wasm_bindgen(js_class = CommitteeResolution)] +impl WasmCommitteeResolution { + /// Accepts committee data returned directly by the JavaScript source. + /// + /// This does not authenticate committee lineage. Use it only when the + /// source is inside the caller's trust boundary. + #[wasm_bindgen(js_name = trustedNode)] + pub fn trusted_node() -> Self { + Self(CommitteeResolution::TrustedNode) + } + + /// Authenticates committee lineage from an already trusted committee. + pub fn anchored(committee: &WasmCommittee) -> Self { + Self(CommitteeResolution::anchored(committee.0.clone())) + } + + /// Authenticates committee lineage from the committee in a trusted genesis blob. + #[wasm_bindgen(js_name = fromGenesis)] + pub fn from_genesis(genesis_blob: Uint8Array) -> Result { + let bytes = genesis_blob.to_vec(); + CommitteeResolution::from_genesis(bytes.as_slice()) + .map(Self) + .map_err(|error| PoiError::invalid_input(format!("failed to load trusted genesis blob: {error}"))) + .wasm_result() + } +} + +/// Resolves the committee required to verify a Proof of Inclusion proof. +/// +/// Node mode trusts the JavaScript source for committee data. Anchored mode +/// authenticates committee lineage from a trusted committee and caches verified +/// committees in memory. +#[wasm_bindgen(js_name = CommitteeResolver)] +pub struct WasmCommitteeResolver(CommitteeResolver); + +#[wasm_bindgen(js_class = CommitteeResolver)] +impl WasmCommitteeResolver { + /// Creates a resolver backed by a JavaScript ledger source. + #[wasm_bindgen(constructor)] + pub fn new(source: LedgerSource, resolution: &WasmCommitteeResolution) -> Self { + Self(CommitteeResolver::new(source, resolution.0.clone())) + } + + /// Resolves the committee governing `epoch`. + pub async fn resolve(&self, epoch: u64) -> Result { + self.0.resolve(epoch).await.map(WasmCommittee).wasm_result() + } + + /// Resolves the committee required by `proof` and verifies the proof with it. + pub async fn verify(&self, proof: &WasmProof) -> Result<(), JsValue> { + self.0.verify(&proof.0).await.wasm_result() + } +} diff --git a/bindings/wasm/poi_wasm/src/error.rs b/bindings/wasm/poi_wasm/src/error.rs new file mode 100644 index 00000000..96ac80e6 --- /dev/null +++ b/bindings/wasm/poi_wasm/src/error.rs @@ -0,0 +1,61 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::error::Error; + +use wasm_bindgen::{JsCast, JsValue}; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum PoiError { + #[error("{0}")] + JavaScript(String), + #[error("{0}")] + InvalidInput(String), + #[error("{0}")] + InvalidResponse(String), +} + +impl PoiError { + pub(crate) fn from_js(value: JsValue) -> Self { + let message = value + .dyn_ref::() + .map(js_sys::Error::message) + .and_then(|message| message.as_string()) + .or_else(|| value.as_string()) + .unwrap_or_else(|| format!("{value:?}")); + + Self::JavaScript(message) + } + + pub(crate) fn invalid_response(message: impl Into) -> Self { + Self::InvalidResponse(message.into()) + } + + pub(crate) fn invalid_input(message: impl Into) -> Self { + Self::InvalidInput(message.into()) + } +} + +pub(crate) trait WasmResult { + fn wasm_result(self) -> Result; +} + +impl WasmResult for Result +where + E: Error, +{ + fn wasm_result(self) -> Result { + self.map_err(|error| { + let mut message = error.to_string(); + let mut source = error.source(); + + while let Some(cause) = source { + message.push_str(": "); + message.push_str(&cause.to_string()); + source = cause.source(); + } + + js_sys::Error::new(&message).into() + }) + } +} diff --git a/bindings/wasm/poi_wasm/src/lib.rs b/bindings/wasm/poi_wasm/src/lib.rs new file mode 100644 index 00000000..813291c6 --- /dev/null +++ b/bindings/wasm/poi_wasm/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod committee; +mod error; +mod proof; +mod source; +mod versioned; + +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const LEDGER_SOURCE_IMPORT: &str = r#" +import type { LedgerSource } from "./source-types.js"; +"#; diff --git a/bindings/wasm/poi_wasm/src/proof.rs b/bindings/wasm/poi_wasm/src/proof.rs new file mode 100644 index 00000000..7a1bc632 --- /dev/null +++ b/bindings/wasm/poi_wasm/src/proof.rs @@ -0,0 +1,95 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk_types::{ObjectId, TransactionDigest}; +use iota_types::event::EventID; +use js_sys::Uint8Array; +use poi_rs::{Proof, ProofBuilder}; +use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; + +use crate::committee::WasmCommittee; +use crate::error::WasmResult; +use crate::source::LedgerSource; + +/// Proof of Inclusion evidence constructed by `poi-rs`. +#[wasm_bindgen(js_name = Proof)] +pub struct WasmProof(pub(crate) Proof); + +#[wasm_bindgen(js_class = Proof)] +impl WasmProof { + /// Deserializes a proof from JSON. + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(json: &str) -> Result { + Proof::from_json_slice(json.as_bytes()).map(WasmProof).wasm_result() + } + + /// Returns the proof format version. + #[wasm_bindgen(getter)] + pub fn version(&self) -> u16 { + self.0.version().value() + } + + /// Returns the epoch of the committee that certified this proof. + #[wasm_bindgen(getter, js_name = checkpointEpoch)] + pub fn checkpoint_epoch(&self) -> u64 { + self.0.checkpoint_summary.epoch() + } + + /// Verifies this proof locally with the supplied committee. + pub fn verify(&self, committee: &WasmCommittee) -> Result<(), JsValue> { + poi_rs::ProofVerifier::new(committee.inner()) + .verify(&self.0) + .wasm_result() + } + + /// Validates the proof format version. + pub fn validate(&self) -> Result<(), JsValue> { + self.0.validate().wasm_result() + } + + /// Serializes this proof as JSON. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + let bytes = self.0.to_json_vec().wasm_result()?; + String::from_utf8(bytes).wasm_result() + } +} + +/// Builds Proof of Inclusion evidence with an internal JavaScript ledger source. +#[wasm_bindgen(js_name = ProofBuilder)] +pub struct WasmProofBuilder(ProofBuilder); + +#[wasm_bindgen(js_class = ProofBuilder)] +impl WasmProofBuilder { + /// Creates a builder backed by the provided JavaScript ledger source. + #[wasm_bindgen(constructor)] + pub fn new(source: LedgerSource) -> Self { + Self(ProofBuilder::new(source)) + } + + /// Adds a transaction proof request. + pub fn transaction(self, transaction_digest: Uint8Array) -> Result { + let digest = TransactionDigest::from_bytes(transaction_digest.to_vec()).wasm_result()?; + Ok(Self(self.0.transaction(digest))) + } + + /// Adds an object proof request. + pub fn object(self, object_id: Uint8Array) -> Result { + let object_id = ObjectId::from_bytes(object_id.to_vec()).wasm_result()?; + Ok(Self(self.0.object(object_id))) + } + + /// Adds an event proof request. + pub fn event(self, transaction_digest: Uint8Array, event_sequence: u64) -> Result { + let tx_digest = TransactionDigest::from_bytes(transaction_digest.to_vec()).wasm_result()?; + Ok(Self(self.0.event(EventID { + tx_digest, + event_seq: event_sequence, + }))) + } + + /// Fetches the requested evidence and constructs the proof. + pub async fn build(self) -> Result { + self.0.build().await.map(WasmProof).wasm_result() + } +} diff --git a/bindings/wasm/poi_wasm/src/source.rs b/bindings/wasm/poi_wasm/src/source.rs new file mode 100644 index 00000000..e0b1a724 --- /dev/null +++ b/bindings/wasm/poi_wasm/src/source.rs @@ -0,0 +1,365 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use async_trait::async_trait; +use fastcrypto::traits::ToFromBytes; +use iota_sdk_types::{ + CheckpointContents, CheckpointDigest, ObjectId, SignedCheckpointSummary, SignedTransaction, Transaction, + TransactionDigest, UserSignature, Version, +}; +use iota_types::{ + base_types::AuthorityName, + committee::{Committee, EpochId}, + digests::ChainIdentifier, + effects::{TransactionEffects, TransactionEffectsAPI, TransactionEvents}, + messages_checkpoint::CertifiedCheckpointSummary, + object::Object, +}; +use js_sys::Uint8Array; +use poi_rs::{Source, SourceCheckpoint, SourceError, SourceTransaction}; +use serde::Deserialize; +use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; + +use crate::error::PoiError; +use crate::versioned::VersionedObject; +use crate::versioned::{VersionedCheckpointSummary, VersionedEvent, VersionedValidatorAggregatedSignature}; + +#[wasm_bindgen] +extern "C" { + /// JavaScript source that owns the generated ledger client. + #[derive(Clone)] + #[wasm_bindgen(typescript_type = "LedgerSource")] + pub type LedgerSource; + + #[wasm_bindgen(method, catch, structural, js_name = chainIdentifier)] + async fn chain_identifier(this: &LedgerSource) -> Result; + + #[wasm_bindgen(method, catch, structural)] + async fn transaction(this: &LedgerSource, digest: Uint8Array) -> Result; + + #[wasm_bindgen(method, catch, structural)] + async fn object(this: &LedgerSource, object_id: Uint8Array, version: Option) -> Result; + + #[wasm_bindgen(method, catch, structural)] + async fn checkpoint(this: &LedgerSource, sequence_number: u64) -> Result; + + #[wasm_bindgen(method, catch, structural)] + async fn committee(this: &LedgerSource, epoch: u64) -> Result; + + #[wasm_bindgen(method, catch, structural, js_name = currentEpoch)] + async fn current_epoch(this: &LedgerSource) -> Result; + + #[wasm_bindgen(method, catch, structural, js_name = epochCloseSummary)] + async fn epoch_close_summary(this: &LedgerSource, epoch: u64) -> Result; +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsTransactionEvidence { + transaction_bcs: Vec, + signatures_bcs: Vec>, + effects_bcs: Vec, + events_bcs: Option>>, + checkpoint_sequence_number: u64, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsCheckpointEvidence { + summary_bcs: Vec, + signature_bcs: Vec, + contents_bcs: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsCheckpointSummaryEvidence { + summary_bcs: Vec, + signature_bcs: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsCommittee { + members: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct JsCommitteeMember { + public_key: Vec, + weight: u64, +} + +#[async_trait(?Send)] +impl Source for LedgerSource { + async fn chain_identifier(&self) -> Result { + let bytes = self + .chain_identifier() + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))? + .to_vec(); + let digest = bytes.try_into().map_err(|bytes: Vec| { + SourceError::invalid_response(PoiError::invalid_response(format!( + "chain identifier must contain 32 bytes, received {}", + bytes.len() + ))) + })?; + + Ok(ChainIdentifier::from(CheckpointDigest::new(digest))) + } + + async fn transaction( + &self, + transaction_digest: TransactionDigest, + ) -> Result, SourceError> { + let digest = Uint8Array::from(transaction_digest.as_ref()); + let value = self + .transaction(digest) + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))?; + + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let evidence: JsTransactionEvidence = serde_wasm_bindgen::from_value(value) + .map_err(|source| SourceError::invalid_response(PoiError::invalid_response(source.to_string())))?; + + decode_transaction(evidence).map(Some) + } + + async fn object(&self, object_id: ObjectId, version: Option) -> Result, SourceError> { + let object_id_bytes = Uint8Array::from(object_id.as_ref()); + let value = self + .object(object_id_bytes, version.map(|version| version.as_u64())) + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))?; + + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let bytes = Uint8Array::new(&value).to_vec(); + let versioned: VersionedObject = decode_bcs(&bytes).map_err(SourceError::invalid_response)?; + let VersionedObject::V1(object) = versioned; + + Ok(Some(object.into())) + } + + async fn checkpoint(&self, sequence_number: u64) -> Result { + let value = self + .checkpoint(sequence_number) + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))?; + let evidence: JsCheckpointEvidence = serde_wasm_bindgen::from_value(value) + .map_err(|source| SourceError::invalid_response(PoiError::invalid_response(source.to_string())))?; + + decode_checkpoint(evidence) + } + + async fn committee(&self, epoch: EpochId) -> Result { + let value = self + .committee(epoch) + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))?; + let evidence: JsCommittee = serde_wasm_bindgen::from_value(value) + .map_err(|source| SourceError::invalid_response(PoiError::invalid_response(source.to_string())))?; + + decode_committee(epoch, evidence).map_err(SourceError::invalid_response) + } + + async fn current_epoch(&self) -> Result, SourceError> { + let value = self + .current_epoch() + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))?; + + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let epoch = serde_wasm_bindgen::from_value(value) + .map_err(|source| SourceError::invalid_response(PoiError::invalid_response(source.to_string())))?; + + Ok(Some(epoch)) + } + + async fn epoch_close_summary(&self, epoch: EpochId) -> Result, SourceError> { + let value = self + .epoch_close_summary(epoch) + .await + .map_err(|source| SourceError::request(PoiError::from_js(source)))?; + + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let evidence: JsCheckpointSummaryEvidence = serde_wasm_bindgen::from_value(value) + .map_err(|source| SourceError::invalid_response(PoiError::invalid_response(source.to_string())))?; + + decode_certified_summary(&evidence.summary_bcs, &evidence.signature_bcs) + .map(Some) + .map_err(SourceError::invalid_response) + } +} + +fn decode_transaction(evidence: JsTransactionEvidence) -> Result { + let transaction: Transaction = decode_bcs(&evidence.transaction_bcs).map_err(SourceError::invalid_response)?; + let signatures = evidence + .signatures_bcs + .iter() + .map(|bytes| decode_bcs::(bytes)) + .collect::, _>>() + .map_err(SourceError::invalid_response)?; + let transaction: iota_types::transaction::Transaction = SignedTransaction { + transaction, + signatures, + } + .into(); + let effects: TransactionEffects = decode_bcs(&evidence.effects_bcs).map_err(SourceError::invalid_response)?; + let events = if effects.events_digest().is_some() { + let events_bcs = evidence.events_bcs.ok_or_else(|| { + SourceError::missing_data(PoiError::invalid_response( + "transaction effects commit to events but eventsBcs is missing", + )) + })?; + let events = events_bcs + .iter() + .map(|bytes| { + let VersionedEvent::V1(event) = decode_bcs::(bytes)?; + Ok(event) + }) + .collect::, bcs::Error>>() + .map_err(SourceError::invalid_response)?; + + Some(TransactionEvents(events)) + } else { + None + }; + + Ok(SourceTransaction { + transaction, + effects, + events, + checkpoint_sequence_number: evidence.checkpoint_sequence_number, + }) +} + +fn decode_checkpoint(evidence: JsCheckpointEvidence) -> Result { + let summary = decode_certified_summary(&evidence.summary_bcs, &evidence.signature_bcs) + .map_err(SourceError::invalid_response)?; + let contents: CheckpointContents = decode_bcs(&evidence.contents_bcs).map_err(SourceError::invalid_response)?; + + Ok(SourceCheckpoint { summary, contents }) +} + +fn decode_certified_summary(summary_bcs: &[u8], signature_bcs: &[u8]) -> Result { + let VersionedCheckpointSummary::V1(summary) = + decode_bcs(summary_bcs).map_err(|source| PoiError::invalid_response(source.to_string()))?; + let VersionedValidatorAggregatedSignature::V1(signature) = + decode_bcs(signature_bcs).map_err(|source| PoiError::invalid_response(source.to_string()))?; + + SignedCheckpointSummary { + checkpoint: summary, + signature, + } + .try_into() + .map_err( + |source: iota_types::iota_sdk_types_conversions::SdkTypeConversionError| { + PoiError::invalid_response(source.to_string()) + }, + ) +} + +fn decode_committee(epoch: EpochId, evidence: JsCommittee) -> Result { + let voting_rights = evidence + .members + .into_iter() + .map(|member| { + AuthorityName::from_bytes(&member.public_key) + .map(|authority| (authority, member.weight)) + .map_err(|source| PoiError::invalid_response(format!("invalid committee public key: {source}"))) + }) + .collect::, _>>()?; + + Ok(Committee::new(epoch, voting_rights)) +} + +fn decode_bcs(bytes: &[u8]) -> Result +where + T: for<'de> Deserialize<'de>, +{ + bcs::from_bytes(bytes) +} + +#[cfg(test)] +mod tests { + use iota_sdk_types::{ + CheckpointContents as SdkCheckpointContents, SignedCheckpointSummary as SdkSignedCheckpointSummary, + SignedTransaction as SdkSignedTransaction, + }; + use poi_rs::Proof; + + use super::*; + + #[test] + fn decodes_the_grpc_bcs_evidence_into_existing_iota_types() { + let proof = Proof::from_json_slice(include_bytes!("../../../../poi-rs/tests/fixtures/current/event.json")) + .expect("fixture must deserialize"); + let signed_transaction: SdkSignedTransaction = proof + .transaction_proof + .transaction + .clone() + .try_into() + .expect("transaction must convert to SDK types"); + let events_bcs = proof.transaction_proof.events.as_ref().map(|events| { + events + .0 + .iter() + .cloned() + .map(|event| bcs::to_bytes(&VersionedEvent::V1(event)).expect("event must serialize")) + .collect() + }); + let transaction = decode_transaction(JsTransactionEvidence { + transaction_bcs: bcs::to_bytes(&signed_transaction.transaction).expect("transaction must serialize"), + signatures_bcs: signed_transaction + .signatures + .iter() + .map(|signature| bcs::to_bytes(signature).expect("signature must serialize")) + .collect(), + effects_bcs: bcs::to_bytes(&proof.transaction_proof.effects).expect("effects must serialize"), + events_bcs, + checkpoint_sequence_number: proof.checkpoint_summary.sequence_number, + }) + .expect("transaction evidence must decode"); + + assert_eq!(transaction.transaction, proof.transaction_proof.transaction); + assert_eq!(transaction.effects, proof.transaction_proof.effects); + assert_eq!(transaction.events, proof.transaction_proof.events); + + let signed_summary: SdkSignedCheckpointSummary = proof + .checkpoint_summary + .clone() + .try_into() + .expect("checkpoint summary must convert to SDK types"); + let contents = SdkCheckpointContents::try_from(proof.checkpoint_contents.clone()) + .expect("checkpoint contents must convert to SDK types"); + let checkpoint = decode_checkpoint(JsCheckpointEvidence { + summary_bcs: bcs::to_bytes(&VersionedCheckpointSummary::V1(signed_summary.checkpoint)) + .expect("checkpoint summary must serialize"), + signature_bcs: bcs::to_bytes(&VersionedValidatorAggregatedSignature::V1(signed_summary.signature)) + .expect("checkpoint signature must serialize"), + contents_bcs: bcs::to_bytes(&contents).expect("checkpoint contents must serialize"), + }) + .expect("checkpoint evidence must decode"); + + assert_eq!( + bcs::to_bytes(&checkpoint.summary).expect("decoded checkpoint summary must serialize"), + bcs::to_bytes(&proof.checkpoint_summary).expect("fixture checkpoint summary must serialize") + ); + assert_eq!(checkpoint.contents, proof.checkpoint_contents); + } +} diff --git a/bindings/wasm/poi_wasm/src/versioned.rs b/bindings/wasm/poi_wasm/src/versioned.rs new file mode 100644 index 00000000..9751eb7f --- /dev/null +++ b/bindings/wasm/poi_wasm/src/versioned.rs @@ -0,0 +1,28 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk_types::Object; +use iota_sdk_types::{CheckpointSummary, Event, ValidatorAggregatedSignature}; +use serde::{Deserialize, Serialize}; + +// These one-variant envelopes match the BCS version discriminants used by the +// IOTA gRPC API. The inner values remain the canonical iota-sdk-types values. +#[derive(Deserialize, Serialize)] +pub(crate) enum VersionedObject { + V1(Object), +} + +#[derive(Deserialize, Serialize)] +pub(crate) enum VersionedEvent { + V1(Event), +} + +#[derive(Deserialize, Serialize)] +pub(crate) enum VersionedCheckpointSummary { + V1(CheckpointSummary), +} + +#[derive(Deserialize, Serialize)] +pub(crate) enum VersionedValidatorAggregatedSignature { + V1(ValidatorAggregatedSignature), +} diff --git a/bindings/wasm/poi_wasm/tests/client.test.ts b/bindings/wasm/poi_wasm/tests/client.test.ts new file mode 100644 index 00000000..e05f4b9e --- /dev/null +++ b/bindings/wasm/poi_wasm/tests/client.test.ts @@ -0,0 +1,118 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { create } from "@bufbuild/protobuf"; +import { createRouterTransport } from "@connectrpc/connect"; + +import { createIotaGrpcClient } from "../lib/client.js"; +import { + CheckpointDataSchema, + GetObjectsResponseSchema, + GetServiceInfoResponseSchema, + GetTransactionsResponseSchema, + LedgerService, +} from "../lib/grpc/generated/iota/grpc/v1/ledger_service_pb.js"; + +test("creates the generated LedgerService client", async () => { + const requests = { + serviceInfo: false, + objects: false, + transactions: false, + checkpoint: false, + }; + const transport = createRouterTransport((router) => { + router.service(LedgerService, { + getServiceInfo(request) { + requests.serviceInfo = true; + assert.deepEqual(request.readMask?.paths, ["chain_id"]); + + return create(GetServiceInfoResponseSchema, { + chainId: { digest: new Uint8Array(32).fill(0xab) }, + }); + }, + async *getObjects(request) { + requests.objects = true; + assert.deepEqual(request.readMask?.paths, ["bcs"]); + + yield create(GetObjectsResponseSchema, { + objects: [], + hasNext: false, + }); + }, + async *getTransactions(request) { + requests.transactions = true; + assert.deepEqual(request.readMask?.paths, ["transaction.bcs"]); + + yield create(GetTransactionsResponseSchema, { + transactionResults: [], + hasNext: false, + }); + }, + async *getCheckpoint(request) { + requests.checkpoint = true; + assert.deepEqual(request.checkpointId, { + case: "sequenceNumber", + value: 42n, + }); + + yield create(CheckpointDataSchema, { + payload: { + case: "endMarker", + value: { sequenceNumber: 42n }, + }, + }); + }, + }); + }); + const client = createIotaGrpcClient("http://unused.test/", { transport }); + + const serviceInfo = await client.getServiceInfo({ + readMask: { paths: ["chain_id"] }, + }); + const objects = await collect( + client.getObjects({ + readMask: { paths: ["bcs"] }, + }), + ); + const transactions = await collect( + client.getTransactions({ + readMask: { paths: ["transaction.bcs"] }, + }), + ); + const checkpoint = await collect( + client.getCheckpoint({ + checkpointId: { + case: "sequenceNumber", + value: 42n, + }, + }), + ); + + assert.deepEqual(serviceInfo.chainId?.digest, new Uint8Array(32).fill(0xab)); + assert.equal(objects.length, 1); + assert.equal(transactions.length, 1); + assert.equal(checkpoint[0]?.payload.case, "endMarker"); + assert.deepEqual(requests, { + serviceInfo: true, + objects: true, + transactions: true, + checkpoint: true, + }); +}); + +test("rejects an empty endpoint", () => { + assert.throws(() => createIotaGrpcClient(" "), /endpoint must not be empty/); +}); + +async function collect(stream: AsyncIterable): Promise { + const values = []; + + for await (const value of stream) { + values.push(value); + } + + return values; +} diff --git a/bindings/wasm/poi_wasm/tests/committee-bindings.test.ts b/bindings/wasm/poi_wasm/tests/committee-bindings.test.ts new file mode 100644 index 00000000..e239aa43 --- /dev/null +++ b/bindings/wasm/poi_wasm/tests/committee-bindings.test.ts @@ -0,0 +1,43 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +import { Committee } from "../node/poi_wasm.js"; + +test("the WASM committee can be deserialized from Rust JSON", async () => { + const json = await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ); + + const committee = Committee.fromJSON(json); + + assert.equal(committee.epoch, 0n); +}); + +test("the WASM committee rejects invalid total voting power", async () => { + const fixture = JSON.parse( + await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ), + ) as { + epoch: number; + voting_rights: [string, number][]; + }; + fixture.voting_rights[0]![1] = 9_999; + + assert.throws( + () => Committee.fromJSON(JSON.stringify(fixture)), + /committee voting power must total 10000, received 9999/, + ); +}); diff --git a/bindings/wasm/poi_wasm/tests/committee-resolution.test.ts b/bindings/wasm/poi_wasm/tests/committee-resolution.test.ts new file mode 100644 index 00000000..dbc77ace --- /dev/null +++ b/bindings/wasm/poi_wasm/tests/committee-resolution.test.ts @@ -0,0 +1,199 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +import { + Committee, + CommitteeResolution, + CommitteeResolver, + Proof, +} from "../node/poi_wasm.js"; +import type { LedgerSource } from "../lib/source-types.js"; + +test("the WASM resolver constructs a committee reported by a trusted node", async () => { + const fixture = JSON.parse( + await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ), + ) as { + epoch: number; + voting_rights: [string, number][]; + }; + const source = { + async committee() { + return { + members: fixture.voting_rights.map(([publicKey, weight]) => ({ + publicKey: Buffer.from(publicKey, "base64"), + weight: BigInt(weight), + })), + }; + }, + } as unknown as LedgerSource; + + const committee = await new CommitteeResolver( + source, + CommitteeResolution.trustedNode(), + ).resolve(0n); + + assert.equal(committee.epoch, 0n); +}); + +test("the anchored verifier resolves the committee and verifies the proof", async () => { + const [committeeJson, proofJson] = await Promise.all([ + readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ), + readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/transaction.json", + import.meta.url, + ), + "utf8", + ), + ]); + const committee = Committee.fromJSON(committeeJson); + const proof = Proof.fromJSON(proofJson); + const source = {} as LedgerSource; + + await assert.doesNotReject( + new CommitteeResolver( + source, + CommitteeResolution.anchored(committee), + ).verify(proof), + ); +}); + +test("the anchored resolver returns its trusted committee without fetching it again", async () => { + const fixture = JSON.parse( + await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ), + ) as { + voting_rights: [string, number][]; + }; + const source = { + async committee() { + return { + members: fixture.voting_rights.map(([publicKey, weight]) => ({ + publicKey: Buffer.from(publicKey, "base64"), + weight: BigInt(weight), + })), + }; + }, + } as unknown as LedgerSource; + const committee = await new CommitteeResolver( + source, + CommitteeResolution.trustedNode(), + ).resolve(0n); + const anchored = await new CommitteeResolver( + source, + CommitteeResolution.anchored(committee), + ).resolve(0n); + + assert.equal(anchored.epoch, 0n); +}); + +test("the anchored resolver reports a missing current epoch", async () => { + const fixture = JSON.parse( + await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ), + ) as { + voting_rights: [string, number][]; + }; + const source = { + async committee() { + return { + members: fixture.voting_rights.map(([publicKey, weight]) => ({ + publicKey: Buffer.from(publicKey, "base64"), + weight: BigInt(weight), + })), + }; + }, + async currentEpoch() { + return undefined; + }, + } as unknown as LedgerSource; + const committee = await new CommitteeResolver( + source, + CommitteeResolution.trustedNode(), + ).resolve(0n); + + await assert.rejects( + new CommitteeResolver( + source, + CommitteeResolution.anchored(committee), + ).resolve(1n), + /service information is missing the current epoch/, + ); +}); + +test("the anchored resolver requests epoch-close evidence through the JavaScript source", async () => { + const fixture = JSON.parse( + await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/committee.json", + import.meta.url, + ), + "utf8", + ), + ) as { + voting_rights: [string, number][]; + }; + let requestedEpoch: bigint | undefined; + const source = { + async committee() { + return { + members: fixture.voting_rights.map(([publicKey, weight]) => ({ + publicKey: Buffer.from(publicKey, "base64"), + weight: BigInt(weight), + })), + }; + }, + async currentEpoch() { + return 1n; + }, + async epochCloseSummary(epoch: bigint) { + requestedEpoch = epoch; + + return { + // Deliberately invalid BCS: the Rust adapter must receive and decode + // the epoch-close evidence before committee authentication begins. + summaryBcs: new Uint8Array([0xff]), + signatureBcs: new Uint8Array([0xff]), + }; + }, + } as unknown as LedgerSource; + const committee = await new CommitteeResolver( + source, + CommitteeResolution.trustedNode(), + ).resolve(0n); + + await assert.rejects( + new CommitteeResolver( + source, + CommitteeResolution.anchored(committee), + ).resolve(1n), + /failed to fetch end-of-epoch checkpoint information for epoch 0/, + ); + assert.equal(requestedEpoch, 0n); +}); diff --git a/bindings/wasm/poi_wasm/tests/ledger-source.test.ts b/bindings/wasm/poi_wasm/tests/ledger-source.test.ts new file mode 100644 index 00000000..9f7b58ef --- /dev/null +++ b/bindings/wasm/poi_wasm/tests/ledger-source.test.ts @@ -0,0 +1,245 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { create } from "@bufbuild/protobuf"; +import { createRouterTransport } from "@connectrpc/connect"; + +import { + CheckpointDataSchema, + GetEpochResponseSchema, + GetObjectsResponseSchema, + GetServiceInfoResponseSchema, + GetTransactionsResponseSchema, + LedgerService, +} from "../lib/grpc/generated/iota/grpc/v1/ledger_service_pb.js"; +import { LedgerSource } from "../lib/ledger-source.js"; + +test("returns the BCS evidence needed by poi-rs", async () => { + const chainId = bytes(0x01); + const transactionDigest = bytes(0x02); + const transactionBcs = bytes(0x03); + const signatureBcs = bytes(0x04); + const effectsBcs = bytes(0x05); + const eventBcs = bytes(0x06); + const objectId = bytes(0x07); + const objectBcs = bytes(0x08); + const summaryBcs = bytes(0x09); + const checkpointSignatureBcs = bytes(0x0a); + const contentsBcs = bytes(0x0b); + const committeePublicKey = new Uint8Array(96).fill(0x0c); + const epochCloseSummaryBcs = bytes(0x0d); + const epochCloseSignatureBcs = bytes(0x0e); + let serviceInfoRequest = 0; + let epochRequest = 0; + + const transport = createRouterTransport((router) => { + router.service(LedgerService, { + getServiceInfo(request) { + serviceInfoRequest += 1; + + if (serviceInfoRequest === 1) { + assert.deepEqual(request.readMask?.paths, ["chain_id"]); + + return create(GetServiceInfoResponseSchema, { + chainId: { digest: chainId }, + }); + } + + assert.deepEqual(request.readMask?.paths, ["epoch"]); + + return create(GetServiceInfoResponseSchema, { epoch: 9n }); + }, + async *getTransactions(request) { + assert.deepEqual(request.readMask?.paths, [ + "transaction.bcs", + "signatures", + "effects.bcs", + "events.digest", + "events.events.bcs", + "checkpoint", + ]); + assert.deepEqual( + request.requests?.requests[0]?.digest?.digest, + transactionDigest, + ); + + yield create(GetTransactionsResponseSchema, { + transactionResults: [ + { + result: { + case: "executedTransaction", + value: { + transaction: { bcs: { data: transactionBcs } }, + signatures: { + signatures: [{ bcs: { data: signatureBcs } }], + }, + effects: { bcs: { data: effectsBcs } }, + events: { + events: { + events: [{ bcs: { data: eventBcs } }], + }, + }, + checkpoint: 42n, + }, + }, + }, + ], + }); + }, + async *getObjects(request) { + assert.deepEqual(request.readMask?.paths, ["bcs"]); + assert.deepEqual( + request.requests?.requests[0]?.objectRef?.objectId?.objectId, + objectId, + ); + assert.equal( + request.requests?.requests[0]?.objectRef?.version, + 7n, + ); + + yield create(GetObjectsResponseSchema, { + objects: [ + { + result: { + case: "object", + value: { bcs: { data: objectBcs } }, + }, + }, + ], + }); + }, + async *getCheckpoint(request) { + assert.deepEqual(request.checkpointId, { + case: "sequenceNumber", + value: 42n, + }); + assert.deepEqual(request.readMask?.paths, [ + "checkpoint.summary.bcs", + "checkpoint.signature", + "checkpoint.contents.bcs", + ]); + + yield create(CheckpointDataSchema, { + payload: { + case: "checkpoint", + value: { + sequenceNumber: 42n, + summary: { bcs: { data: summaryBcs } }, + signature: { bcs: { data: checkpointSignatureBcs } }, + contents: { bcs: { data: contentsBcs } }, + }, + }, + }); + yield create(CheckpointDataSchema, { + payload: { + case: "endMarker", + value: { sequenceNumber: 42n }, + }, + }); + }, + getEpoch(request) { + epochRequest += 1; + assert.equal(request.epoch, 7n); + + if (epochRequest === 1) { + assert.deepEqual(request.readMask?.paths, ["committee"]); + + return create(GetEpochResponseSchema, { + epoch: { + committee: { + epoch: 7n, + members: { + members: [{ publicKey: committeePublicKey, weight: 10_000n }], + }, + }, + }, + }); + } + + assert.deepEqual(request.readMask?.paths, [ + "epoch_close_proof.checkpoint", + ]); + + return create(GetEpochResponseSchema, { + epoch: { + epochCloseProof: { + checkpoint: { + summary: { bcs: { data: epochCloseSummaryBcs } }, + signature: { bcs: { data: epochCloseSignatureBcs } }, + }, + }, + }, + }); + }, + }); + }); + const source = new LedgerSource("http://unused.test", { transport }); + + assert.deepEqual(await source.chainIdentifier(), chainId); + assert.deepEqual(await source.transaction(transactionDigest), { + transactionBcs, + signaturesBcs: [signatureBcs], + effectsBcs, + eventsBcs: [eventBcs], + checkpointSequenceNumber: 42n, + }); + assert.deepEqual(await source.object(objectId, 7n), objectBcs); + assert.deepEqual(await source.checkpoint(42n), { + summaryBcs, + signatureBcs: checkpointSignatureBcs, + contentsBcs, + }); + assert.deepEqual(await source.committee(7n), { + members: [{ publicKey: committeePublicKey, weight: 10_000n }], + }); + assert.equal(await source.currentEpoch(), 9n); + assert.deepEqual(await source.epochCloseSummary(7n), { + summaryBcs: epochCloseSummaryBcs, + signatureBcs: epochCloseSignatureBcs, + }); +}); + +test("returns undefined when a transaction or object is not returned", async () => { + const transport = createRouterTransport((router) => { + router.service(LedgerService, { + async *getTransactions() { + yield create(GetTransactionsResponseSchema); + }, + async *getObjects() { + yield create(GetObjectsResponseSchema); + }, + }); + }); + const source = new LedgerSource("http://unused.test", { transport }); + + assert.equal(await source.transaction(bytes(0x01)), undefined); + assert.equal(await source.object(bytes(0x02)), undefined); +}); + +test("rejects incomplete checkpoint evidence", async () => { + const transport = createRouterTransport((router) => { + router.service(LedgerService, { + async *getCheckpoint() { + yield create(CheckpointDataSchema, { + payload: { + case: "endMarker", + value: { sequenceNumber: 42n }, + }, + }); + }, + }); + }); + const source = new LedgerSource("http://unused.test", { transport }); + + await assert.rejects( + source.checkpoint(42n), + /returned no checkpoint for sequence number 42/, + ); +}); + +function bytes(value: number): Uint8Array { + return new Uint8Array(32).fill(value); +} diff --git a/bindings/wasm/poi_wasm/tests/proof-bindings.test.ts b/bindings/wasm/poi_wasm/tests/proof-bindings.test.ts new file mode 100644 index 00000000..c54fe18c --- /dev/null +++ b/bindings/wasm/poi_wasm/tests/proof-bindings.test.ts @@ -0,0 +1,128 @@ +// Copyright 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +import { Proof, ProofBuilder } from "../node/poi_wasm.js"; +import type { LedgerSource } from "../lib/source-types.js"; + +test("the WASM builder reads transaction evidence from the ledger source", async () => { + const transactionDigest = new Uint8Array(32).fill(0x2a); + let requestedDigest: Uint8Array | undefined; + const source = { + async transaction(digest: Uint8Array) { + requestedDigest = digest; + + return { + // Deliberately invalid BCS: the test is proving that the WASM adapter + // reached this source and attempted Rust-side decoding. + transactionBcs: new Uint8Array([0xff]), + signaturesBcs: [], + effectsBcs: new Uint8Array([0xff]), + checkpointSequenceNumber: 7n, + }; + }, + } as unknown as LedgerSource; + + await assert.rejects( + new ProofBuilder(source).transaction(transactionDigest).build(), + /source failed while reading proof evidence: source returned an invalid response/, + ); + assert.deepEqual(requestedDigest, transactionDigest); +}); + +test("the WASM builder validates digest lengths before fetching", () => { + const source = {} as LedgerSource; + + assert.throws( + () => new ProofBuilder(source).transaction(new Uint8Array(31)), + /invalid digest byte length: expected 32, got 31/, + ); +}); + +test("the WASM proof can be deserialized for verification", async () => { + const json = await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/transaction.json", + import.meta.url, + ), + "utf8", + ); + + const proof = Proof.fromJSON(json); + const serialized = JSON.parse(proof.toJSON()) as { + targets: { + transaction: string | null; + objects: unknown[]; + events: unknown[]; + }; + checkpoint_contents: unknown; + transaction_proof: Record; + }; + + assert.equal(proof.version, 1); + assert.equal(proof.checkpointEpoch, 0n); + assert.doesNotThrow(() => proof.validate()); + assert.equal(typeof serialized.targets.transaction, "string"); + assert.deepEqual(serialized.targets.objects, []); + assert.deepEqual(serialized.targets.events, []); + assert.ok(serialized.checkpoint_contents); + assert.deepEqual(Object.keys(serialized.transaction_proof), [ + "transaction", + "effects", + "events", + ]); +}); + +test("the WASM proof keeps selected events separate from event evidence", async () => { + const json = await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/event.json", + import.meta.url, + ), + "utf8", + ); + + const proof = Proof.fromJSON(json); + const serialized = JSON.parse(proof.toJSON()) as { + targets: { + transaction: string | null; + objects: unknown[]; + events: Array<{ txDigest: string; eventSeq: string }>; + }; + transaction_proof: { events: unknown[] | null }; + }; + + assert.equal(serialized.targets.transaction, null); + assert.deepEqual(serialized.targets.objects, []); + assert.equal(serialized.targets.events.length, 1); + assert.equal(serialized.targets.events[0]?.eventSeq, "0"); + assert.equal(serialized.transaction_proof.events?.length, 1); +}); + +test("the WASM proof stores selected objects only in its targets", async () => { + const json = await readFile( + new URL( + "../../../../poi-rs/tests/fixtures/current/object.json", + import.meta.url, + ), + "utf8", + ); + + const proof = Proof.fromJSON(json); + const serialized = JSON.parse(proof.toJSON()) as { + targets: { + transaction: string | null; + objects: unknown[]; + events: unknown[]; + }; + transaction_proof: { events: unknown[] | null }; + }; + + assert.equal(serialized.targets.transaction, null); + assert.equal(serialized.targets.objects.length, 1); + assert.deepEqual(serialized.targets.events, []); + assert.equal(serialized.transaction_proof.events, null); +}); diff --git a/bindings/wasm/poi_wasm/tsconfig.json b/bindings/wasm/poi_wasm/tsconfig.json new file mode 100644 index 00000000..9d6f496f --- /dev/null +++ b/bindings/wasm/poi_wasm/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["lib/**/*.ts", "tests/**/*.ts", "examples/**/*.ts"] +} diff --git a/poi-rs/Cargo.toml b/poi-rs/Cargo.toml new file mode 100644 index 00000000..c912725e --- /dev/null +++ b/poi-rs/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "poi-rs" +version = "0.1.0-alpha" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "proof", "inclusion", "notarization"] +license.workspace = true +repository.workspace = true +rust-version.workspace = true +description = "Proof of Inclusion support for the IOTA Notarization Toolkit." + +[features] +default = ["native-grpc"] +native-grpc = ["dep:iota-grpc-client", "dep:iota-grpc-types"] +cli = [ + "native-grpc", + "dep:anyhow", + "dep:clap", + "dep:iota-config", + "dep:reqwest", + "serde_json/std", + "tokio/macros", + "tokio/process", + "tokio/rt", +] + +[dependencies] +anyhow = { workspace = true, optional = true } +async-trait.workspace = true +bcs.workspace = true +clap = { workspace = true, optional = true } +iota-grpc-client = { workspace = true, optional = true } +iota-grpc-types = { workspace = true, optional = true } +iota-config = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436", optional = true } +iota-sdk-types.workspace = true +iota-types.workspace = true +reqwest = { workspace = true, optional = true } +serde.workspace = true +serde_json = { workspace = true, features = ["alloc"] } +thiserror.workspace = true +tokio = { version = "1.52.2", default-features = false, features = ["sync"] } + +[dev-dependencies] +iota-config = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436" } +test-cluster = { git = "https://github.com/iotaledger/iota.git", rev = "420df58ea50ce916a927ebba2dcaee192832e436", package = "test-cluster" } +tokio = { version = "1.52.2", default-features = false, features = ["macros", "process", "rt", "sync"] } + +[[bin]] +name = "poi" +path = "src/bin/poi.rs" +required-features = ["cli"] diff --git a/poi-rs/README.md b/poi-rs/README.md new file mode 100644 index 00000000..cd8c472a --- /dev/null +++ b/poi-rs/README.md @@ -0,0 +1,130 @@ +# IOTA Proof of Inclusion Rust Package + +The Proof of Inclusion Rust package provides proof data types and offline verification for inclusion claims in the IOTA +Notarization Toolkit. + +Use Proof of Inclusion when a verifier needs cryptographic evidence that a transaction, event, or object state is tied to +a certified IOTA checkpoint. `ProofBuilder` fetches the proof material, while `ProofVerifier` verifies that material +locally without trusting the source that supplied it. + +## Proof Construction + +`PoiClient` provides explicit constructors for the public IOTA networks. The client does not select a default network, so +the calling application always chooses where it fetches proof material. + +```rust,no_run +use iota_sdk_types::TransactionDigest; +use poi_rs::PoiClient; + +# async fn example() -> Result<(), Box> { +let transaction_digest: TransactionDigest = todo!(); +let client = PoiClient::mainnet()?; +let proof = client + .proof() + .transaction(transaction_digest) + .build() + .await?; +# Ok(()) +# } +``` + +Use `PoiClient::testnet()` or `PoiClient::devnet()` for the other public networks. Applications can pass a custom `Source` +to `PoiClient::new(source)` when they use a private node, archive, fixture, or local test cluster. `ProofBuilder` remains +available directly for lower-level use. + +A builder can stack multiple object and event targets by calling `object()` and `event()` repeatedly or by using the +`objects()` and `events()` batch methods. Every target must belong to the same transaction. The builder ignores exact +duplicates and reuses one transaction and one checkpoint for the complete target set. + +`Source` is the transport boundary: it fetches decoded transaction, object, checkpoint, chain, and committee evidence. +`ProofBuilder` owns target resolution, consistency checks, and proof construction, while `CommitteeResolver` owns +committee authentication and caching, so custom sources do not reimplement either workflow. + +Network selection configures only the proof source. It does not make the returned proof trusted or select an authoritative +committee for verification. + +The default `native-grpc` feature implements `Source` directly for the SDK `GrpcClient` and provides the public-network +constructors. WASM packages can disable default features and supply a JavaScript-backed `Source` without compiling native +gRPC. + +## Proof Model + +A `Proof` contains three layers of evidence: + +- `ProofTargets` recording the transaction, objects, and events explicitly selected by the caller. +- A `CertifiedCheckpointSummary` and its `CheckpointContents` linking the transaction to a committee-certified + checkpoint. +- A required `TransactionProof` containing the transaction, its effects, and event data when event targets are present. + +Object targets contain their exact object values; verification derives each object reference and finds it in the +transaction effects. Event targets contain `EventID` values, while the transaction proof carries the complete event list +needed to verify the effects' event digest. A transaction target is present only when the caller explicitly requested the +transaction itself, although transaction evidence supports every proof. + +## Verification + +For the common source-backed workflow, create a verifier from the same `PoiClient`. The verifier resolves the committee +required by the proof and then performs offline proof verification: + +```rust,no_run +use std::fs::File; + +use poi_rs::{CommitteeResolution, PoiClient, Proof}; + +# async fn example(proof: &Proof) -> Result<(), Box> { +let client = PoiClient::testnet()?; +let resolution = CommitteeResolution::from_genesis(File::open("genesis.blob")?)?; +let verifier = client.verifier(resolution); + +verifier.verify(proof).await?; +# Ok(()) +# } +``` + +`CommitteeResolution::TrustedNode` is available when the connected node is explicitly inside the caller's trust +boundary. `CommitteeResolution::from_genesis()` loads an anchor committee from a trusted BCS-encoded genesis blob, +while `CommitteeResolution::anchored()` accepts an already extracted trusted committee. Use +`CommitteeResolution::anchored_with_cache()` or `CommitteeResolution::from_genesis_with_cache()` to supply a cache +that contains committees authenticated for the same network. +Retain the verifier when checking multiple proofs so its authenticated committee cache is reused. + +`ProofVerifier` remains the offline verification entry point for callers that already possess the authoritative +committee. It verifies only the proof material passed by the caller. + +Verification checks: + +- the proof format version is supported +- the checkpoint summary is certified by the supplied committee +- the checkpoint contents match the certified checkpoint summary +- the transaction digest matches the transaction effects +- the transaction effects are included in the checkpoint contents +- an explicitly requested transaction matches the packaged transaction +- requested object targets derive references present in the transaction effects +- event data, when required, matches the event digest recorded in the effects +- requested event targets belong to the transaction and select events in the authenticated event list + +## Trust Boundaries + +`ProofVerifier` is intentionally offline. It does not make RPC calls and does not decide which committee is authoritative. +`CommitteeResolver::verify()` composes committee resolution with offline verification for source-backed workflows. +`CommitteeResolver::resolve()` remains available when callers need the authenticated committee itself. + +The verifier treats all proof payloads as untrusted until verification succeeds. After verification succeeds, callers can +trust the authenticated target claims relative to the supplied committee. + +## Main Types + +- `Proof`: Versioned Proof of Inclusion envelope. +- `ProofVersion`: Proof format version used for compatibility checks. +- `TransactionProof`: Transaction, effects, and optional event evidence used to prove inclusion. +- `ProofTargets`: Transaction, object, and event claims explicitly selected by the caller. +- `PoiClient`: Source-backed entry point for proof construction and committee-aware verification. +- `CommitteeResolution`: Trusted-node or anchored committee-resolution configuration, including the committee cache. +- `ProofBuilder`: Network-aware or custom-source proof construction. +- `Source`: Ledger-read boundary for gRPC nodes, JavaScript clients, archives, fixtures, and other evidence sources. +- `SourceTransaction` and `SourceCheckpoint`: Transport-independent decoded evidence returned by a `Source`. +- `CommitteeResolver`: Committee resolution and source-backed proof verification configured by `CommitteeResolution`. +- `ProofVerifier`: Offline verifier for `Proof` values. +- `SourceError`: Transport and response failures from a ledger source. +- `ProofBuilderError`, `CommitteeResolutionError`, `ProofVerificationError`, `VerifyError`, `SerializationError`, and + `VersionError`: Operation-specific errors. diff --git a/poi-rs/src/bin/poi.rs b/poi-rs/src/bin/poi.rs new file mode 100644 index 00000000..7329953d --- /dev/null +++ b/poi-rs/src/bin/poi.rs @@ -0,0 +1,424 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#![forbid(unsafe_code)] + +use std::{ + fs, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result, bail}; +use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum}; +use iota_config::{IOTA_GENESIS_FILENAME, iota_config_dir}; +use iota_grpc_client::Client as GrpcClient; +use iota_sdk_types::{ObjectId, TransactionDigest}; +use iota_types::event::EventID; +use poi_rs::{CommitteeResolution, PoiClient, Proof}; + +const GENESIS_CACHE_DIR: &str = "poi"; +const MAINNET_GENESIS_URL: &str = "https://dbfiles.mainnet.iota.cafe/genesis.blob"; +const TESTNET_GENESIS_URL: &str = "https://dbfiles.testnet.iota.cafe/genesis.blob"; +const DEVNET_GENESIS_URL: &str = "https://dbfiles.devnet.iota.cafe/genesis.blob"; +const CREATE_EXAMPLES: &str = r#"Examples: + poi create --network mainnet --transaction TRANSACTION_DIGEST + poi create --network testnet --object OBJECT_ID --output proof.json + poi create --grpc-url http://localhost:9000 --event TRANSACTION_DIGEST:EVENT_SEQUENCE + +The selected endpoint supplies untrusted proof material; it does not establish verification trust."#; +const VERIFY_EXAMPLES: &str = r#"Examples: + poi verify --network mainnet proof.json + poi verify --network testnet --genesis trusted-genesis.blob proof.json + poi verify --grpc-url http://localhost:9000 --genesis genesis.blob - + +Known networks download and cache their genesis blob automatically. An explicit --genesis path overrides the managed blob. +The genesis blob is the trust anchor. The selected endpoint only supplies committee-walking data."#; + +#[derive(Debug, Parser)] +#[command( + name = "poi", + version, + about = "Create and verify IOTA Proof of Inclusion proofs", + long_about = "Create portable IOTA Proof of Inclusion proofs and verify them against committee history authenticated from a trusted genesis blob.", + arg_required_else_help = true, + propagate_version = true +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Create a proof from an IOTA gRPC source. + Create(CreateArgs), + /// Verify a proof using genesis-anchored committee history. + Verify(VerifyArgs), +} + +impl Command { + async fn execute(self) -> Result<()> { + match self { + Self::Create(args) => args.execute().await, + Self::Verify(args) => args.execute().await, + } + } +} + +#[derive(Debug, Args)] +#[command( + long_about = "Create a Proof of Inclusion for one transaction and any requested object or event targets that belong to it.", + after_help = CREATE_EXAMPLES, + group( + ArgGroup::new("target") + .required(true) + .multiple(true) + .args(["transaction", "object", "event"]) + ) +)] +struct CreateArgs { + #[command(flatten)] + endpoint: EndpointArgs, + /// Transaction digest to prove. + #[arg(long, value_name = "DIGEST", value_parser = parse_transaction_digest, group = "target")] + transaction: Option, + /// Object ID to prove. The source resolves its latest version unless a transaction or event scopes the proof. May be repeated. + #[arg(long, value_name = "OBJECT_ID", value_parser = parse_object_id, group = "target")] + object: Vec, + /// Event identifier formatted as TRANSACTION_DIGEST:EVENT_SEQUENCE. May be repeated. + #[arg(long, value_name = "EVENT_ID", value_parser = parse_event_id, group = "target")] + event: Vec, + /// Output file. Write JSON to stdout when omitted or set to '-'. + #[arg(short, long, value_name = "PATH")] + output: Option, +} + +impl CreateArgs { + async fn execute(self) -> Result<()> { + let Self { + endpoint, + transaction, + object, + event, + output, + } = self; + let client = PoiClient::from_grpc_client(endpoint.client()?); + let mut builder = client.proof(); + + if let Some(transaction) = transaction { + builder = builder.transaction(transaction); + } + let proof = builder + .objects(object) + .events(event) + .build() + .await + .context("failed to create proof")?; + + match output.as_deref() { + Some(path) if path != Path::new("-") => { + let file = fs::File::create(path) + .with_context(|| format!("failed to create proof file '{}'", path.display()))?; + serde_json::to_writer_pretty(file, &proof) + .with_context(|| format!("failed to write proof JSON to '{}'", path.display())) + } + _ => serde_json::to_writer_pretty(io::stdout().lock(), &proof) + .context("failed to write proof JSON to stdout"), + } + } +} + +#[derive(Debug, Args)] +#[command( + long_about = "Verify a Proof of Inclusion locally after authenticating the checkpoint committee from a trusted genesis blob.", + after_help = VERIFY_EXAMPLES +)] +struct VerifyArgs { + #[command(flatten)] + endpoint: EndpointArgs, + /// Proof JSON file, or '-' to read from stdin. + #[arg(value_name = "PROOF")] + proof: PathBuf, + /// Trusted genesis blob. Required with --grpc-url; overrides the managed network blob. + #[arg(long, value_name = "PATH", required_unless_present = "network")] + genesis: Option, +} + +impl VerifyArgs { + async fn execute(self) -> Result<()> { + let proof: Proof = if self.proof == Path::new("-") { + serde_json::from_reader(io::stdin().lock()).context("failed to read proof JSON from stdin")? + } else { + let file = fs::File::open(&self.proof) + .with_context(|| format!("failed to open proof file '{}'", self.proof.display()))?; + serde_json::from_reader(file) + .with_context(|| format!("failed to read proof JSON from '{}'", self.proof.display()))? + }; + proof.validate().context("proof format is not supported")?; + + let genesis = match self.genesis.as_deref() { + Some(path) => { + fs::File::open(path).with_context(|| format!("failed to open genesis blob '{}'", path.display()))? + } + None => { + load_genesis( + self.endpoint + .network + .context("a known network or explicit genesis blob is required for verification")?, + ) + .await? + } + }; + let resolution = CommitteeResolution::from_genesis(genesis) + .map_err(|error| anyhow::anyhow!("failed to load trusted genesis blob: {error}"))?; + PoiClient::from_grpc_client(self.endpoint.client()?) + .verifier(resolution) + .verify(&proof) + .await + .context("proof verification failed")?; + writeln!(io::stdout().lock(), "valid").context("failed to write verification result to stdout") + } +} + +#[derive(Debug, Args)] +#[command(group( + ArgGroup::new("endpoint") + .required(true) + .multiple(false) + .args(["network", "grpc_url"]) +))] +struct EndpointArgs { + /// Public IOTA network whose default gRPC endpoint should be used. + #[arg(long, value_enum)] + network: Option, + /// Custom IOTA gRPC endpoint. + #[arg(long, value_name = "URL")] + grpc_url: Option, +} + +impl EndpointArgs { + fn client(&self) -> Result { + if let Some(network) = self.network { + return network.client(); + } + if let Some(url) = self.grpc_url.as_deref() { + return GrpcClient::new(url).with_context(|| format!("failed to configure gRPC endpoint '{url}'")); + } + + bail!("an IOTA network or gRPC URL is required") + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum Network { + Mainnet, + Testnet, + Devnet, +} + +impl Network { + const fn name(self) -> &'static str { + match self { + Self::Mainnet => "mainnet", + Self::Testnet => "testnet", + Self::Devnet => "devnet", + } + } + + const fn genesis_url(self) -> &'static str { + match self { + Self::Mainnet => MAINNET_GENESIS_URL, + Self::Testnet => TESTNET_GENESIS_URL, + Self::Devnet => DEVNET_GENESIS_URL, + } + } + + fn client(self) -> Result { + match self { + Self::Mainnet => GrpcClient::new_mainnet().context("failed to configure mainnet gRPC endpoint"), + Self::Testnet => GrpcClient::new_testnet().context("failed to configure testnet gRPC endpoint"), + Self::Devnet => GrpcClient::new_devnet().context("failed to configure devnet gRPC endpoint"), + } + } +} + +async fn load_genesis(network: Network) -> Result { + let path = iota_config_dir() + .context("failed to locate the IOTA configuration directory")? + .join(GENESIS_CACHE_DIR) + .join(network.name()) + .join(IOTA_GENESIS_FILENAME); + + if !path.is_file() { + let parent = path + .parent() + .context("managed genesis path does not have a parent directory")?; + fs::create_dir_all(parent) + .with_context(|| format!("failed to create genesis cache directory '{}'", parent.display()))?; + + let url = network.genesis_url(); + let bytes = reqwest::get(url) + .await + .with_context(|| format!("failed to download {} genesis blob from '{url}'", network.name()))? + .bytes() + .await + .with_context(|| format!("failed to read genesis blob from '{url}'"))?; + fs::write(&path, bytes).with_context(|| format!("failed to cache genesis blob at '{}'", path.display()))?; + } + + fs::File::open(&path).with_context(|| format!("failed to open genesis blob '{}'", path.display())) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + Cli::parse().command.execute().await +} + +fn parse_transaction_digest(value: &str) -> Result { + value + .parse() + .map_err(|error| format!("invalid transaction digest '{value}': {error}")) +} + +fn parse_object_id(value: &str) -> Result { + value + .parse::() + .map_err(|error| format!("invalid object ID '{value}': {error}")) +} + +fn parse_event_id(value: &str) -> Result { + let mut parts = value.split(':'); + let (Some(transaction), Some(sequence), None) = (parts.next(), parts.next(), parts.next()) else { + return Err(format!( + "invalid event ID '{value}'; expected TRANSACTION_DIGEST:EVENT_SEQUENCE" + )); + }; + let tx_digest = transaction + .parse::() + .map_err(|error| format!("invalid transaction digest in event ID '{value}': {error}"))?; + let event_seq = sequence + .parse::() + .map_err(|error| format!("invalid event sequence in '{value}': {error}"))?; + + Ok(EventID { tx_digest, event_seq }) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + const DIGEST: &str = "11111111111111111111111111111111"; + const OBJECT_ID: &str = "0x0000000000000000000000000000000000000000000000000000000000000000"; + + #[test] + fn create_requires_a_target() { + let error = Cli::try_parse_from(["poi", "create", "--network", "mainnet"]) + .expect_err("create without a target must fail"); + + assert!(error.to_string().contains("--transaction ")); + } + + #[test] + fn create_accepts_mixed_targets() { + let event = format!("{DIGEST}:0"); + let cli = Cli::try_parse_from([ + "poi", + "create", + "--network", + "testnet", + "--transaction", + DIGEST, + "--object", + OBJECT_ID, + "--event", + &event, + ]) + .expect("mixed targets must parse"); + + let Command::Create(args) = cli.command else { + panic!("create command must parse"); + }; + assert!(args.transaction.is_some()); + assert_eq!(args.object.len(), 1); + assert_eq!(args.event.len(), 1); + } + + #[test] + fn endpoint_selection_is_exclusive() { + let error = Cli::try_parse_from([ + "poi", + "create", + "--network", + "mainnet", + "--grpc-url", + "http://localhost:9000", + "--transaction", + DIGEST, + ]) + .expect_err("multiple endpoints must fail"); + + assert!(error.to_string().contains("cannot be used with")); + } + + #[test] + fn known_network_verification_manages_genesis_automatically() { + let cli = Cli::try_parse_from(["poi", "verify", "--network", "mainnet", "proof.json"]) + .expect("known network must not require an explicit genesis blob"); + + let Command::Verify(args) = cli.command else { + panic!("verify command must parse"); + }; + assert!(args.genesis.is_none()); + } + + #[test] + fn custom_endpoint_verification_requires_genesis() { + let error = Cli::try_parse_from(["poi", "verify", "--grpc-url", "http://localhost:9000", "proof.json"]) + .expect_err("custom endpoint must require an explicit genesis blob"); + + assert!(error.to_string().contains("--genesis ")); + } + + #[test] + fn known_network_genesis_urls_match_the_iota_light_client() { + assert_eq!(Network::Mainnet.genesis_url(), MAINNET_GENESIS_URL); + assert_eq!(Network::Testnet.genesis_url(), TESTNET_GENESIS_URL); + assert_eq!(Network::Devnet.genesis_url(), DEVNET_GENESIS_URL); + } + + #[test] + fn invalid_event_id_reports_the_required_format() { + let error = Cli::try_parse_from(["poi", "create", "--network", "mainnet", "--event", "not-an-event"]) + .expect_err("invalid event ID must fail"); + + assert!(error.to_string().contains("TRANSACTION_DIGEST:EVENT_SEQUENCE")); + } + + #[test] + fn invalid_object_id_reports_the_invalid_value() { + let error = Cli::try_parse_from(["poi", "create", "--network", "mainnet", "--object", "not-an-object"]) + .expect_err("invalid object ID must fail"); + + assert!(error.to_string().contains("invalid object ID 'not-an-object'")); + } + + #[test] + fn command_help_explains_the_trust_boundary() { + let mut command = Cli::command(); + let create = command + .find_subcommand_mut("create") + .expect("create subcommand must exist") + .render_long_help() + .to_string(); + let verify = command + .find_subcommand_mut("verify") + .expect("verify subcommand must exist") + .render_long_help() + .to_string(); + + assert!(create.contains("does not establish verification trust")); + assert!(verify.contains("genesis blob is the trust anchor")); + } +} diff --git a/poi-rs/src/builder.rs b/poi-rs/src/builder.rs new file mode 100644 index 00000000..801134b5 --- /dev/null +++ b/poi-rs/src/builder.rs @@ -0,0 +1,343 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "native-grpc")] +use iota_grpc_client::Client as GrpcClient; +use iota_sdk_types::{ObjectId, ObjectReference, TransactionDigest}; +use iota_types::{effects::TransactionEffectsExt, event::EventID, object::Object}; + +use crate::{Proof, ProofTargets, Source, SourceError, TransactionProof}; + +/// Error returned when a proof cannot be constructed by [`ProofBuilder`]. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum ProofBuilderError { + /// No proof request was selected before building. + #[error("proof builder requires a request")] + MissingRequest, + /// The configured source failed while reading proof evidence. + #[error("source failed while reading proof evidence")] + Source { + /// Underlying source failure. + #[source] + source: SourceError, + }, + /// The source did not return a requested transaction. + #[error("transaction {transaction_digest} was not found")] + TransactionNotFound { + /// Transaction digest that was not returned. + transaction_digest: TransactionDigest, + }, + /// The source did not return a requested object. + #[error("object {object_id} was not found")] + ObjectNotFound { + /// Object ID that was not returned. + object_id: ObjectId, + }, + /// The requested event was not present in its transaction. + #[error("event {event_id:?} was not found")] + EventNotFound { + /// Event ID that was not present. + event_id: EventID, + }, + /// The returned object does not match the requested ID or transaction effects. + #[error("object {object_id} reference does not match the requested object")] + ObjectReferenceMismatch { + /// Requested object ID. + object_id: ObjectId, + }, + /// The requested object was not changed by the selected transaction. + #[error("object {object_id} was not changed by transaction {transaction_digest}")] + ObjectNotChangedByTransaction { + /// Requested object ID. + object_id: ObjectId, + /// Transaction selected by the other proof requests. + transaction_digest: TransactionDigest, + }, + /// The requests belong to different transactions. + #[error("proof requests belong to different transactions: {actual}, expected {expected}")] + TransactionMismatch { + /// Transaction selected by the first request. + expected: TransactionDigest, + /// Transaction selected by a conflicting request. + actual: TransactionDigest, + }, +} + +/// Constructs Proof of Inclusion evidence from a caller-provided [`Source`]. +/// +/// The builder keeps proof construction independent of a specific transport. +/// With the `native-grpc` feature enabled, SDK gRPC clients can be adapted +/// through `ProofBuilder::from_grpc_client`. +pub struct ProofBuilder { + source: S, + transaction_digests: Vec, + object_ids: Vec, + event_ids: Vec, +} + +#[cfg(feature = "native-grpc")] +impl ProofBuilder { + /// Creates a proof builder connected to the public IOTA mainnet gRPC endpoint. + /// + /// Selecting an endpoint does not establish verification trust. Verify the + /// constructed proof with a committee trusted for mainnet. + pub fn mainnet() -> iota_grpc_client::Result { + GrpcClient::new_mainnet().map(Self::from_grpc_client) + } + + /// Creates a proof builder connected to the public IOTA testnet gRPC endpoint. + /// + /// Selecting an endpoint does not establish verification trust. Verify the + /// constructed proof with a committee trusted for testnet. + pub fn testnet() -> iota_grpc_client::Result { + GrpcClient::new_testnet().map(Self::from_grpc_client) + } + + /// Creates a proof builder connected to the public IOTA devnet gRPC endpoint. + /// + /// Selecting an endpoint does not establish verification trust. Verify the + /// constructed proof with a committee trusted for devnet. + pub fn devnet() -> iota_grpc_client::Result { + GrpcClient::new_devnet().map(Self::from_grpc_client) + } + + /// Creates a proof builder backed by an existing SDK gRPC client. + pub fn from_grpc_client(client: GrpcClient) -> Self { + Self::new(client) + } +} + +impl ProofBuilder { + /// Creates a proof builder backed by `source`. + pub fn new(source: S) -> Self { + Self { + source, + transaction_digests: Vec::new(), + object_ids: Vec::new(), + event_ids: Vec::new(), + } + } + + /// Adds a transaction proof request. + pub fn transaction(mut self, transaction_digest: TransactionDigest) -> Self { + Self::push_unique(&mut self.transaction_digests, transaction_digest); + self + } + + /// Adds an object proof request by object ID. + /// + /// The source resolves the ID to the exact object reference packaged in the proof. + pub fn object(mut self, object_id: ObjectId) -> Self { + Self::push_unique(&mut self.object_ids, object_id); + self + } + + /// Adds multiple object proof requests by object ID. + pub fn objects(mut self, object_ids: impl IntoIterator) -> Self { + for object_id in object_ids { + Self::push_unique(&mut self.object_ids, object_id); + } + self + } + + /// Adds an event proof request. + pub fn event(mut self, event_id: EventID) -> Self { + Self::push_unique(&mut self.event_ids, event_id); + self + } + + /// Adds multiple event proof requests. + pub fn events(mut self, event_ids: impl IntoIterator) -> Self { + for event_id in event_ids { + Self::push_unique(&mut self.event_ids, event_id); + } + self + } + + /// Builds the requested proof from the configured source. + pub async fn build(self) -> Result { + if self.transaction_digests.is_empty() && self.object_ids.is_empty() && self.event_ids.is_empty() { + return Err(ProofBuilderError::MissingRequest); + } + + self.build_proof().await + } + + async fn build_proof(&self) -> Result { + let mut selected_transaction = None; + + for transaction_digest in self.transaction_digests.iter().copied() { + Self::ensure_same_transaction(&mut selected_transaction, transaction_digest)?; + } + for event_id in &self.event_ids { + Self::ensure_same_transaction(&mut selected_transaction, event_id.tx_digest)?; + } + + let (transaction, objects) = if let Some(transaction_digest) = selected_transaction { + let transaction = self.fetch_transaction(transaction_digest).await?; + let changed_objects = transaction.effects.all_changed_objects(); + let mut objects = Vec::with_capacity(self.object_ids.len()); + + for object_id in self.object_ids.iter().copied() { + let object_ref = changed_objects + .iter() + .find_map(|(object_ref, _, _)| (object_ref.object_id == object_id).then_some(*object_ref)) + .ok_or(ProofBuilderError::ObjectNotChangedByTransaction { + object_id, + transaction_digest, + })?; + objects.push(self.fetch_object(object_id, Some(object_ref)).await?); + } + + (transaction, objects) + } else { + let mut objects = Vec::with_capacity(self.object_ids.len()); + + for object_id in self.object_ids.iter().copied() { + let object = self.fetch_object(object_id, None).await?; + Self::ensure_same_transaction(&mut selected_transaction, object.previous_transaction)?; + objects.push(object); + } + + let transaction_digest = + selected_transaction.expect("ProofBuilder only builds a proof for non-empty requests"); + let transaction = self.fetch_transaction(transaction_digest).await?; + + (transaction, objects) + }; + + let chain_identifier = self + .source + .chain_identifier() + .await + .map_err(|source| ProofBuilderError::Source { source })?; + let checkpoint = self + .source + .checkpoint(transaction.checkpoint_sequence_number) + .await + .map_err(|source| ProofBuilderError::Source { source })?; + let transaction_events = if self.event_ids.is_empty() { + None + } else { + let events = transaction.events.ok_or_else(|| ProofBuilderError::EventNotFound { + event_id: self.event_ids[0], + })?; + + for event_id in &self.event_ids { + let event_exists = usize::try_from(event_id.event_seq) + .ok() + .is_some_and(|index| events.get(index).is_some()); + if !event_exists { + return Err(ProofBuilderError::EventNotFound { event_id: *event_id }); + } + } + + Some(events) + }; + let transaction_proof = TransactionProof::new(transaction.transaction, transaction.effects, transaction_events); + let mut targets = ProofTargets::new(); + if let Some(transaction_digest) = self.transaction_digests.first().copied() { + targets = targets.set_transaction(transaction_digest); + } + for object in objects { + targets = targets.add_object(object); + } + for event_id in self.event_ids.iter().copied() { + targets = targets.add_event(event_id); + } + + Ok(Proof::new( + chain_identifier, + targets, + checkpoint.summary, + checkpoint.contents, + transaction_proof, + )) + } + + async fn fetch_transaction( + &self, + transaction_digest: TransactionDigest, + ) -> Result { + self.source + .transaction(transaction_digest) + .await + .map_err(|source| ProofBuilderError::Source { source })? + .ok_or(ProofBuilderError::TransactionNotFound { transaction_digest }) + } + + async fn fetch_object( + &self, + object_id: ObjectId, + expected_ref: Option, + ) -> Result { + let object = self + .source + .object(object_id, expected_ref.map(|object_ref| object_ref.version)) + .await + .map_err(|source| ProofBuilderError::Source { source })? + .ok_or(ProofBuilderError::ObjectNotFound { object_id })?; + let object_ref = object.as_inner().object_ref(); + + if object_ref.object_id != object_id || expected_ref.is_some_and(|expected| expected != object_ref) { + return Err(ProofBuilderError::ObjectReferenceMismatch { object_id }); + } + + Ok(object) + } + + fn ensure_same_transaction( + selected: &mut Option, + transaction_digest: TransactionDigest, + ) -> Result<(), ProofBuilderError> { + if let Some(expected) = selected { + if *expected != transaction_digest { + return Err(ProofBuilderError::TransactionMismatch { + expected: *expected, + actual: transaction_digest, + }); + } + } else { + *selected = Some(transaction_digest); + } + + Ok(()) + } + + fn push_unique(values: &mut Vec, value: T) { + if !values.contains(&value) { + values.push(value); + } + } +} + +#[cfg(test)] +#[cfg(feature = "native-grpc")] +mod tests { + use super::*; + + #[tokio::test] + async fn mainnet_uses_the_sdk_mainnet_endpoint() { + let builder = ProofBuilder::mainnet().expect("mainnet builder must be configured"); + let expected = GrpcClient::new_mainnet().expect("SDK mainnet client must be configured"); + + assert_eq!(builder.source.uri(), expected.uri()); + } + + #[tokio::test] + async fn testnet_uses_the_sdk_testnet_endpoint() { + let builder = ProofBuilder::testnet().expect("testnet builder must be configured"); + let expected = GrpcClient::new_testnet().expect("SDK testnet client must be configured"); + + assert_eq!(builder.source.uri(), expected.uri()); + } + + #[tokio::test] + async fn devnet_uses_the_sdk_devnet_endpoint() { + let builder = ProofBuilder::devnet().expect("devnet builder must be configured"); + let expected = GrpcClient::new_devnet().expect("SDK devnet client must be configured"); + + assert_eq!(builder.source.uri(), expected.uri()); + } +} diff --git a/poi-rs/src/cache.rs b/poi-rs/src/cache.rs new file mode 100644 index 00000000..61629e2c --- /dev/null +++ b/poi-rs/src/cache.rs @@ -0,0 +1,45 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_types::committee::{Committee, EpochId}; + +use crate::BoxError; + +mod in_memory; + +pub use in_memory::MemoryCommitteeCache; + +/// Error returned by a committee cache. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum CommitteeCacheError { + /// A cached committee conflicts with authenticated committee data. + #[error("cached committee conflicts at epoch {epoch}")] + Conflict { + /// Epoch whose cached material conflicts. + epoch: EpochId, + }, + /// A cache backend failed to read or write committee material. + #[error("committee cache backend failed at epoch {epoch}")] + Backend { + /// Epoch being accessed when the backend failed. + epoch: EpochId, + /// Underlying backend error. + #[source] + source: BoxError, + }, +} + +/// Stores authenticated committees for anchored resolution. +/// +/// A cache is part of the caller's trust boundary. Implementations must return +/// only committees previously authenticated for the same network and must +/// preserve their integrity after storage. +#[async_trait::async_trait] +pub trait CommitteeCache: Send + Sync { + /// Returns the authenticated committee for `epoch`, when available. + async fn committee(&self, epoch: EpochId) -> Result, CommitteeCacheError>; + + /// Stores a committee after the resolver has authenticated it. + async fn store(&self, committee: &Committee) -> Result<(), CommitteeCacheError>; +} diff --git a/poi-rs/src/cache/in_memory.rs b/poi-rs/src/cache/in_memory.rs new file mode 100644 index 00000000..b580a06c --- /dev/null +++ b/poi-rs/src/cache/in_memory.rs @@ -0,0 +1,122 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::BTreeMap, sync::Arc}; + +use iota_types::committee::{Committee, EpochId}; +use tokio::sync::RwLock; + +use super::{CommitteeCache, CommitteeCacheError}; + +/// In-memory committee cache for library usage and tests. +#[derive(Clone, Debug, Default)] +pub struct MemoryCommitteeCache { + committees: Arc>>, +} + +impl MemoryCommitteeCache { + /// Creates an empty in-memory committee cache. + pub fn new() -> Self { + Self::default() + } + + /// Returns the number of cached committees. + pub async fn len(&self) -> usize { + self.committees.read().await.len() + } + + /// Returns whether the cache contains no committees. + pub async fn is_empty(&self) -> bool { + self.committees.read().await.is_empty() + } +} + +#[async_trait::async_trait] +impl CommitteeCache for MemoryCommitteeCache { + async fn committee(&self, epoch: EpochId) -> Result, CommitteeCacheError> { + Ok(self.committees.read().await.get(&epoch).cloned()) + } + + async fn store(&self, committee: &Committee) -> Result<(), CommitteeCacheError> { + let epoch = committee.epoch; + let mut committees = self.committees.write().await; + + if committees.get(&epoch).is_some_and(|cached| cached != committee) { + return Err(CommitteeCacheError::Conflict { epoch }); + } + + committees.entry(epoch).or_insert_with(|| committee.clone()); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn committee_at(epoch: EpochId) -> Committee { + let (committee, _) = Committee::new_simple_test_committee(); + + Committee::new(epoch, committee.voting_rights.iter().cloned().collect()) + } + + #[tokio::test] + async fn new_cache_is_empty() { + let cache = MemoryCommitteeCache::new(); + + assert!(cache.is_empty().await); + assert_eq!(cache.len().await, 0); + assert!(cache.committee(7).await.unwrap().is_none()); + } + + #[tokio::test] + async fn store_makes_a_committee_available_by_epoch() { + let cache = MemoryCommitteeCache::new(); + let committee = committee_at(7); + + cache.store(&committee).await.unwrap(); + + assert_eq!(cache.committee(7).await.unwrap(), Some(committee)); + assert_eq!(cache.len().await, 1); + assert!(!cache.is_empty().await); + } + + #[tokio::test] + async fn storing_the_same_committee_is_idempotent() { + let cache = MemoryCommitteeCache::new(); + let committee = committee_at(7); + + cache.store(&committee).await.unwrap(); + cache.store(&committee).await.unwrap(); + + assert_eq!(cache.committee(7).await.unwrap(), Some(committee)); + assert_eq!(cache.len().await, 1); + } + + #[tokio::test] + async fn conflicting_committee_is_rejected_without_replacing_the_original() { + let cache = MemoryCommitteeCache::new(); + let original = committee_at(7); + let (conflicting, _) = Committee::new_simple_test_committee_of_size(5); + let conflicting = Committee::new(7, conflicting.voting_rights.iter().cloned().collect()); + cache.store(&original).await.unwrap(); + + let error = cache.store(&conflicting).await.unwrap_err(); + + assert!(matches!(error, CommitteeCacheError::Conflict { epoch: 7 })); + assert_eq!(cache.committee(7).await.unwrap(), Some(original)); + assert_eq!(cache.len().await, 1); + } + + #[tokio::test] + async fn clones_share_cached_committees() { + let cache = MemoryCommitteeCache::new(); + let clone = cache.clone(); + let committee = committee_at(7); + + cache.store(&committee).await.unwrap(); + + assert_eq!(clone.committee(7).await.unwrap(), Some(committee)); + } +} diff --git a/poi-rs/src/client.rs b/poi-rs/src/client.rs new file mode 100644 index 00000000..358588b8 --- /dev/null +++ b/poi-rs/src/client.rs @@ -0,0 +1,66 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "native-grpc")] +use iota_grpc_client::Client as GrpcClient; + +use crate::{CommitteeResolution, CommitteeResolver, ProofBuilder, Source}; + +/// Convenient entry point for proof construction and verification backed by one ledger source. +#[derive(Clone)] +pub struct PoiClient { + source: S, +} + +impl PoiClient { + /// Creates a client backed by `source`. + pub const fn new(source: S) -> Self { + Self { source } + } +} + +impl PoiClient +where + S: Source + Clone, +{ + /// Creates a fresh builder for one Proof of Inclusion. + pub fn proof(&self) -> ProofBuilder { + ProofBuilder::new(self.source.clone()) + } + + /// Creates a verifier using the selected committee-resolution strategy. + /// + /// Retain the returned resolver when verifying multiple proofs so anchored + /// resolutions can reuse their authenticated committee cache. + pub fn verifier(&self, resolution: CommitteeResolution) -> CommitteeResolver { + CommitteeResolver::new(self.source.clone(), resolution) + } +} + +#[cfg(feature = "native-grpc")] +impl PoiClient { + /// Creates a client connected to the public IOTA mainnet gRPC endpoint. + pub fn mainnet() -> iota_grpc_client::Result { + GrpcClient::new_mainnet().map(Self::new) + } + + /// Creates a client connected to the public IOTA testnet gRPC endpoint. + pub fn testnet() -> iota_grpc_client::Result { + GrpcClient::new_testnet().map(Self::new) + } + + /// Creates a client connected to the public IOTA devnet gRPC endpoint. + pub fn devnet() -> iota_grpc_client::Result { + GrpcClient::new_devnet().map(Self::new) + } + + /// Creates a client backed by an existing SDK gRPC client. + pub const fn from_grpc_client(client: GrpcClient) -> Self { + Self::new(client) + } + + /// Returns the underlying SDK gRPC client. + pub const fn grpc_client(&self) -> &GrpcClient { + &self.source + } +} diff --git a/poi-rs/src/committee.rs b/poi-rs/src/committee.rs new file mode 100644 index 00000000..a0d48519 --- /dev/null +++ b/poi-rs/src/committee.rs @@ -0,0 +1,838 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{io::Read, sync::Arc}; + +#[cfg(feature = "native-grpc")] +use iota_grpc_client::Client as GrpcClient; +use iota_sdk_types::CheckpointContents; +use iota_types::{ + committee::{Committee, EpochId}, + effects::{TransactionEffects, TransactionEvents}, + error::IotaError, + iota_system_state::{IotaSystemStateTrait, get_iota_system_state}, + messages_checkpoint::CertifiedCheckpointSummary, + object::Object, + transaction::Transaction, +}; +use serde::Deserialize; + +use crate::{ + BoxError, CommitteeCache, CommitteeCacheError, MemoryCommitteeCache, Proof, ProofVerifier, Source, VerifyError, +}; + +/// Error returned when a committee cannot be resolved for an epoch. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +#[error("failed to resolve committee for epoch {target_epoch}")] +pub struct CommitteeResolutionError { + /// Epoch whose committee was requested. + pub target_epoch: EpochId, + /// Committee resolution failure details. + #[source] + pub kind: CommitteeResolutionErrorKind, +} + +impl CommitteeResolutionError { + /// Associates a resolution failure with the committee epoch requested by the caller. + fn new(target_epoch: EpochId, kind: CommitteeResolutionErrorKind) -> Self { + Self { target_epoch, kind } + } +} + +/// Kind of committee resolution failure. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum CommitteeResolutionErrorKind { + /// Loading the initial committee from the trusted genesis blob failed. + #[error("failed to load the committee from the trusted genesis blob")] + LoadGenesisCommittee { + /// Genesis decoding or system-state extraction failure. + #[source] + source: BoxError, + }, + /// Fetching a committee directly from the trusted node failed. + #[error("failed to fetch committee for epoch {epoch} from the trusted node")] + FetchCommittee { + /// Epoch requested from the node. + epoch: EpochId, + /// Underlying source error. + #[source] + source: BoxError, + }, + /// The requested epoch predates the trusted committee anchor. + #[error("target epoch is before trusted anchor epoch {anchor_epoch}")] + TargetBeforeAnchor { + /// Earliest epoch authenticated by the resolver. + anchor_epoch: EpochId, + }, + /// Fetching the node's current epoch failed. + #[error("failed to fetch the node's current epoch")] + FetchCurrentEpoch { + /// Underlying source error. + #[source] + source: BoxError, + }, + /// The service information response omitted the current epoch. + #[error("service information is missing the current epoch")] + MissingCurrentEpoch, + /// The requested epoch is newer than the connected node's current epoch. + #[error("target epoch is ahead of node current epoch {current_epoch}")] + TargetAheadOfNode { + /// Current epoch reported by the connected node. + current_epoch: EpochId, + }, + /// Fetching the certified summary that closed an epoch failed. + #[error("failed to fetch end-of-epoch checkpoint information for epoch {epoch}")] + FetchEpochHistory { + /// Epoch whose certified closing summary was requested. + epoch: EpochId, + /// Underlying source error. + #[source] + source: BoxError, + }, + /// A closed epoch response omitted its epoch-close proof. + #[error("epoch {epoch} is missing its epoch-close proof")] + MissingEpochCloseProof { + /// Closed epoch whose proof was requested. + epoch: EpochId, + }, + /// The current trusted committee did not authenticate the end-of-epoch checkpoint. + #[error("failed to verify epoch {epoch} end-of-epoch checkpoint {sequence_number}")] + InvalidEndOfEpochCheckpoint { + /// Epoch whose committee was used for verification. + epoch: EpochId, + /// Checkpoint sequence number closing the epoch. + sequence_number: u64, + /// Underlying checkpoint verification error. + #[source] + source: BoxError, + }, + /// The epoch's last checkpoint did not contain next-epoch data. + #[error("checkpoint {sequence_number} is not an end-of-epoch checkpoint")] + NotEndOfEpoch { + /// Checkpoint sequence number returned by the epoch response. + sequence_number: u64, + }, + /// Incrementing the authenticated epoch would overflow an [`EpochId`]. + #[error("next epoch after {epoch} overflows u64")] + NextEpochOverflow { + /// Authenticated checkpoint epoch. + epoch: EpochId, + }, + /// Reading or writing an authenticated committee in a cache failed. + #[error("committee cache failed at epoch {epoch}")] + Cache { + /// Epoch being resolved through the cache. + epoch: EpochId, + /// Underlying cache error. + #[source] + source: CommitteeCacheError, + }, +} + +/// Error returned when committee resolution or proof verification fails. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum ProofVerificationError { + /// The committee required by the proof could not be resolved. + #[error("failed to resolve the committee required by the proof")] + CommitteeResolution { + /// Committee-resolution failure. + #[source] + source: CommitteeResolutionError, + }, + /// Offline proof verification failed. + #[error("proof verification failed")] + Proof { + /// Offline verification failure. + #[source] + source: VerifyError, + }, +} + +/// Selects how a resolver establishes trust in committee data. +#[derive(Clone)] +#[non_exhaustive] +pub enum CommitteeResolution { + /// Accept committee data returned directly by the connected node. + /// + /// This does not authenticate committee lineage. Use it only when the node + /// is inside the caller's trust boundary. + TrustedNode, + /// Authenticate committee lineage from an existing trust anchor. + Anchored { + /// First committee trusted by the caller. + committee: Committee, + /// Cache containing only committees authenticated for the same network. + cache: Arc, + }, +} + +impl CommitteeResolution { + /// Anchors committee resolution at an already trusted committee. + /// + /// Authenticated committees are retained in a fresh in-memory cache. + pub fn anchored(committee: Committee) -> Self { + Self::anchored_with_cache(committee, MemoryCommitteeCache::new()) + } + + /// Anchors committee resolution using a caller-provided committee cache. + /// + /// The cache is part of the caller's trust boundary and must return only + /// committees authenticated for the same network. + pub fn anchored_with_cache(committee: Committee, cache: impl CommitteeCache + 'static) -> Self { + Self::Anchored { + committee, + cache: Arc::new(cache), + } + } + + /// Anchors committee resolution at the committee contained in a trusted genesis blob. + /// + /// The reader must contain the BCS-encoded `genesis.blob` for the proof's + /// network. Authenticated committees are retained in a fresh in-memory cache. + pub fn from_genesis(reader: impl Read) -> Result { + Self::from_genesis_with_cache(reader, MemoryCommitteeCache::new()) + } + + /// Anchors committee resolution from a trusted genesis blob using a caller-provided cache. + /// + /// The reader must contain the BCS-encoded `genesis.blob` for the proof's + /// network. The cache is part of the caller's trust boundary. + pub fn from_genesis_with_cache( + reader: impl Read, + cache: impl CommitteeCache + 'static, + ) -> Result { + #[allow(dead_code)] + #[derive(Deserialize)] + struct GenesisBlob { + checkpoint: CertifiedCheckpointSummary, + checkpoint_contents: CheckpointContents, + transaction: Transaction, + effects: TransactionEffects, + events: TransactionEvents, + objects: Vec, + } + + let genesis: GenesisBlob = + bcs::from_reader(reader).map_err(|source| CommitteeResolutionErrorKind::LoadGenesisCommittee { + source: Box::new(source), + })?; + let objects = genesis.objects.as_slice(); + let system_state = + get_iota_system_state(&objects).map_err(|source| CommitteeResolutionErrorKind::LoadGenesisCommittee { + source: Box::new(source), + })?; + let committee = system_state.get_current_epoch_committee().committee().clone(); + + Ok(Self::anchored_with_cache(committee, cache)) + } +} + +/// Resolves the committee required to verify a checkpoint from a ledger source. +/// +/// A resolver either accepts committee data directly from a trusted node or +/// starts from a trusted committee, normally obtained from the network genesis +/// blob, and authenticates every end-of-epoch handoff up to the requested epoch. +#[derive(Clone)] +pub struct CommitteeResolver { + source: S, + mode: CommitteeResolution, +} + +impl CommitteeResolver +where + S: Source, +{ + /// Creates a resolver backed by `source` using `resolution` to establish committee trust. + pub const fn new(source: S, resolution: CommitteeResolution) -> Self { + Self { + source, + mode: resolution, + } + } + + /// Resolves the authenticated committee for `target_epoch`. + /// + /// Node mode returns the committee reported by the trusted node. Anchor + /// mode verifies each end-of-epoch checkpoint with the current committee + /// before accepting its successor. + pub async fn resolve(&self, target_epoch: EpochId) -> Result { + match &self.mode { + CommitteeResolution::TrustedNode => self.resolve_from_node(target_epoch).await, + CommitteeResolution::Anchored { committee, cache } => { + self.resolve_from_anchor(committee, cache.as_ref(), target_epoch).await + } + } + } + + /// Resolves the committee required by `proof` and verifies the proof with it. + /// + /// Committee resolution may fetch committee or epoch-close evidence from + /// the source. The final proof verification is performed locally by + /// [`ProofVerifier`]. + pub async fn verify(&self, proof: &Proof) -> Result<(), ProofVerificationError> { + let committee = self + .resolve(proof.checkpoint_summary.epoch()) + .await + .map_err(|source| ProofVerificationError::CommitteeResolution { source })?; + + ProofVerifier::new(&committee) + .verify(proof) + .map_err(|source| ProofVerificationError::Proof { source }) + } + + /// Fetches a committee directly from a node inside the caller's trust boundary. + async fn resolve_from_node(&self, target_epoch: EpochId) -> Result { + self.source.committee(target_epoch).await.map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::FetchCommittee { + epoch: target_epoch, + source: Box::new(source), + }, + ) + }) + } + + /// Resolves from trusted cached committees before walking authenticated epoch summaries. + async fn resolve_from_anchor( + &self, + trusted_committee: &Committee, + cache: &dyn CommitteeCache, + target_epoch: EpochId, + ) -> Result { + if target_epoch < trusted_committee.epoch { + return Err(CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::TargetBeforeAnchor { + anchor_epoch: trusted_committee.epoch, + }, + )); + } + + if target_epoch == trusted_committee.epoch { + return Ok(trusted_committee.clone()); + } + + if let Some(committee) = cache.committee(target_epoch).await.map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::Cache { + epoch: target_epoch, + source, + }, + ) + })? { + if committee.epoch != target_epoch { + return Err(CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::Cache { + epoch: target_epoch, + source: CommitteeCacheError::Conflict { epoch: target_epoch }, + }, + )); + } + + return Ok(committee); + } + + let mut committee = trusted_committee.clone(); + + while committee.epoch < target_epoch { + let next_epoch = committee.epoch + 1; + let Some(cached) = cache.committee(next_epoch).await.map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::Cache { + epoch: next_epoch, + source, + }, + ) + })? + else { + break; + }; + + if cached.epoch != next_epoch { + return Err(CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::Cache { + epoch: next_epoch, + source: CommitteeCacheError::Conflict { epoch: next_epoch }, + }, + )); + } + + committee = cached; + } + + if committee.epoch == target_epoch { + return Ok(committee); + } + + let current_epoch = self.current_epoch(target_epoch).await?; + if target_epoch > current_epoch { + return Err(CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::TargetAheadOfNode { current_epoch }, + )); + } + + while committee.epoch < target_epoch { + let next_committee = self.fetch_next_committee(target_epoch, &committee, cache).await?; + committee = next_committee; + } + + Ok(committee) + } + + /// Fetches the connected node's current epoch to reject unreachable targets early. + async fn current_epoch(&self, target_epoch: EpochId) -> Result { + self.source + .current_epoch() + .await + .map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::FetchCurrentEpoch { + source: Box::new(source), + }, + ) + })? + .ok_or_else(|| { + CommitteeResolutionError::new(target_epoch, CommitteeResolutionErrorKind::MissingCurrentEpoch) + }) + } + + /// Fetches and authenticates the committee elected for the next epoch. + async fn fetch_next_committee( + &self, + target_epoch: EpochId, + current_committee: &Committee, + cache: &dyn CommitteeCache, + ) -> Result { + let summary = self + .source + .epoch_close_summary(current_committee.epoch) + .await + .map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::FetchEpochHistory { + epoch: current_committee.epoch, + source: Box::new(source), + }, + ) + })? + .ok_or_else(|| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::MissingEpochCloseProof { + epoch: current_committee.epoch, + }, + ) + })?; + + let sequence_number = summary.sequence_number; + let summary_epoch = summary.epoch(); + if summary_epoch != current_committee.epoch { + return Err(CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::InvalidEndOfEpochCheckpoint { + epoch: current_committee.epoch, + sequence_number, + source: Box::new(IotaError::WrongEpoch { + expected_epoch: current_committee.epoch, + actual_epoch: summary_epoch, + }), + }, + )); + } + + if summary.end_of_epoch_data.is_none() { + return Err(CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::NotEndOfEpoch { sequence_number }, + )); + } + + let next_epoch = summary_epoch.checked_add(1).ok_or_else(|| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::NextEpochOverflow { epoch: summary_epoch }, + ) + })?; + + let verified = summary.try_into_verified(current_committee).map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::InvalidEndOfEpochCheckpoint { + epoch: current_committee.epoch, + sequence_number, + source: Box::new(source), + }, + ) + })?; + let next_epoch_committee = &verified + .end_of_epoch_data + .as_ref() + .expect("checked before signature verification") + .next_epoch_committee; + let next_committee = Committee::from_committee_members(next_epoch, next_epoch_committee); + + cache.store(&next_committee).await.map_err(|source| { + CommitteeResolutionError::new( + target_epoch, + CommitteeResolutionErrorKind::Cache { + epoch: next_committee.epoch, + source, + }, + ) + })?; + + Ok(next_committee) + } +} + +#[cfg(feature = "native-grpc")] +impl CommitteeResolver { + /// Returns the underlying SDK gRPC client. + pub const fn grpc_client(&self) -> &GrpcClient { + &self.source + } +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use iota_sdk_types::{ + CheckpointSummary, EndOfEpochData, ObjectId, TransactionDigest, Version, gas::GasCostSummary, + }; + use iota_types::{digests::ChainIdentifier, messages_checkpoint::CertifiedCheckpointSummary, object::Object}; + + use super::*; + use crate::{SourceCheckpoint, SourceError, SourceTransaction}; + + struct StaticCache { + committee: Committee, + } + + #[derive(Clone)] + struct EpochCloseSource { + summary: CertifiedCheckpointSummary, + } + + #[async_trait::async_trait] + impl Source for EpochCloseSource { + async fn chain_identifier(&self) -> Result { + unreachable!("committee transition does not resolve a chain identifier") + } + + async fn transaction( + &self, + _transaction_digest: TransactionDigest, + ) -> Result, SourceError> { + unreachable!("committee transition does not resolve transactions") + } + + async fn object(&self, _object_id: ObjectId, _version: Option) -> Result, SourceError> { + unreachable!("committee transition does not resolve objects") + } + + async fn checkpoint(&self, _sequence_number: u64) -> Result { + unreachable!("committee transition does not resolve checkpoints") + } + + async fn committee(&self, _epoch: EpochId) -> Result { + unreachable!("anchored committee transition does not trust node committees") + } + + async fn current_epoch(&self) -> Result, SourceError> { + unreachable!("direct committee transition test does not resolve the current epoch") + } + + async fn epoch_close_summary( + &self, + _epoch: EpochId, + ) -> Result, SourceError> { + Ok(Some(self.summary.clone())) + } + } + + #[derive(Clone, Default)] + struct RecordingCache { + stored: Arc>>, + } + + impl RecordingCache { + fn stored(&self) -> Vec { + self.stored.lock().unwrap().clone() + } + } + + #[async_trait::async_trait] + impl CommitteeCache for RecordingCache { + async fn committee(&self, _epoch: EpochId) -> Result, CommitteeCacheError> { + Ok(None) + } + + async fn store(&self, committee: &Committee) -> Result<(), CommitteeCacheError> { + self.stored.lock().unwrap().push(committee.clone()); + Ok(()) + } + } + + #[async_trait::async_trait] + impl CommitteeCache for StaticCache { + async fn committee(&self, epoch: EpochId) -> Result, CommitteeCacheError> { + Ok((self.committee.epoch == epoch).then(|| self.committee.clone())) + } + + async fn store(&self, _committee: &Committee) -> Result<(), CommitteeCacheError> { + Ok(()) + } + } + + fn signed_end_of_epoch_summary( + current_epoch: EpochId, + include_next_committee: bool, + ) -> (Committee, Committee, CertifiedCheckpointSummary) { + let (base_committee, keypairs) = Committee::new_simple_test_committee(); + let current_committee = Committee::new(current_epoch, base_committee.voting_rights.iter().cloned().collect()); + let (next_base_committee, _) = Committee::new_simple_test_committee_of_size(5); + let next_committee = Committee::new( + current_epoch.saturating_add(1), + next_base_committee.voting_rights.iter().cloned().collect(), + ); + let end_of_epoch_data = include_next_committee.then(|| EndOfEpochData { + next_epoch_committee: next_committee.committee_members(), + next_epoch_protocol_version: 1, + epoch_commitments: Vec::new(), + epoch_supply_change: 0, + }); + let summary = CheckpointSummary { + epoch: current_epoch, + sequence_number: 42, + network_total_transactions: 0, + content_digest: Default::default(), + previous_digest: None, + epoch_rolling_gas_cost_summary: GasCostSummary::default(), + timestamp_ms: 0, + checkpoint_commitments: Vec::new(), + end_of_epoch_data, + version_specific_data: Vec::new(), + }; + let certified_summary = + CertifiedCheckpointSummary::new_from_keypairs_for_testing(summary, &keypairs, ¤t_committee); + + (current_committee, next_committee, certified_summary) + } + + #[tokio::test] + async fn authenticated_summary_stores_exactly_the_verified_committee() { + let (current_committee, expected_committee, summary) = signed_end_of_epoch_summary(3, true); + let cache = RecordingCache::default(); + let resolver = CommitteeResolver::new( + EpochCloseSource { summary }, + CommitteeResolution::anchored(current_committee.clone()), + ); + + let committee = resolver + .fetch_next_committee(4, ¤t_committee, &cache) + .await + .unwrap(); + + assert_eq!(committee, expected_committee); + assert_eq!(cache.stored(), vec![expected_committee]); + } + + #[tokio::test] + async fn invalid_checkpoint_signature_never_reaches_the_cache() { + let (_, _, summary) = signed_end_of_epoch_summary(3, true); + let (wrong_committee, _) = Committee::new_simple_test_committee_of_size(6); + let wrong_committee = Committee::new(3, wrong_committee.voting_rights.iter().cloned().collect()); + let cache = RecordingCache::default(); + let resolver = CommitteeResolver::new( + EpochCloseSource { summary }, + CommitteeResolution::anchored(wrong_committee.clone()), + ); + + let error = resolver + .fetch_next_committee(4, &wrong_committee, &cache) + .await + .unwrap_err(); + + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::InvalidEndOfEpochCheckpoint { + epoch: 3, + sequence_number: 42, + .. + } + )); + assert!(cache.stored().is_empty()); + } + + #[tokio::test] + async fn checkpoint_without_end_of_epoch_data_never_reaches_the_cache() { + let (current_committee, _, summary) = signed_end_of_epoch_summary(3, false); + let cache = RecordingCache::default(); + let resolver = CommitteeResolver::new( + EpochCloseSource { summary }, + CommitteeResolution::anchored(current_committee.clone()), + ); + + let error = resolver + .fetch_next_committee(4, ¤t_committee, &cache) + .await + .unwrap_err(); + + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::NotEndOfEpoch { sequence_number: 42 } + )); + assert!(cache.stored().is_empty()); + } + + #[tokio::test] + async fn end_of_epoch_structure_is_checked_before_signatures() { + let (_, _, summary) = signed_end_of_epoch_summary(3, false); + let (wrong_committee, _) = Committee::new_simple_test_committee_of_size(6); + let wrong_committee = Committee::new(3, wrong_committee.voting_rights.iter().cloned().collect()); + let cache = RecordingCache::default(); + let resolver = CommitteeResolver::new( + EpochCloseSource { summary }, + CommitteeResolution::anchored(wrong_committee.clone()), + ); + + let error = resolver + .fetch_next_committee(4, &wrong_committee, &cache) + .await + .unwrap_err(); + + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::NotEndOfEpoch { sequence_number: 42 } + )); + assert!(cache.stored().is_empty()); + } + + #[tokio::test] + async fn wrong_epoch_summary_does_not_advance_or_reach_the_cache() { + let (signing_committee, _, summary) = signed_end_of_epoch_summary(4, true); + let expected_committee = Committee::new(3, signing_committee.voting_rights.iter().cloned().collect()); + let cache = RecordingCache::default(); + let resolver = CommitteeResolver::new( + EpochCloseSource { summary }, + CommitteeResolution::anchored(expected_committee.clone()), + ); + + let error = resolver + .fetch_next_committee(4, &expected_committee, &cache) + .await + .unwrap_err(); + + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::InvalidEndOfEpochCheckpoint { + epoch: 3, + sequence_number: 42, + .. + } + )); + assert!(cache.stored().is_empty()); + } + + #[tokio::test] + async fn overflowing_next_epoch_never_reaches_the_cache() { + let (current_committee, _, summary) = signed_end_of_epoch_summary(EpochId::MAX, true); + let cache = RecordingCache::default(); + let resolver = CommitteeResolver::new( + EpochCloseSource { summary }, + CommitteeResolution::anchored(current_committee.clone()), + ); + + let error = resolver + .fetch_next_committee(EpochId::MAX, ¤t_committee, &cache) + .await + .unwrap_err(); + + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::NextEpochOverflow { epoch: EpochId::MAX } + )); + assert!(cache.stored().is_empty()); + } + + #[tokio::test] + async fn node_resolution_mode_carries_no_anchored_cache() { + let resolver = CommitteeResolver::new( + GrpcClient::new("http://127.0.0.1:1").unwrap(), + CommitteeResolution::TrustedNode, + ); + + assert!(matches!(resolver.mode, CommitteeResolution::TrustedNode)); + } + + #[tokio::test] + async fn anchored_resolution_resumes_from_an_authenticated_cache() { + let (current_committee, next_committee, _) = signed_end_of_epoch_summary(3, true); + let cache = crate::MemoryCommitteeCache::new(); + cache.store(&next_committee).await.unwrap(); + let client = GrpcClient::new("http://127.0.0.1:1").unwrap(); + let resolver = + crate::PoiClient::new(client).verifier(CommitteeResolution::anchored_with_cache(current_committee, cache)); + + let resolved = resolver.resolve(4).await.unwrap(); + + assert_eq!(resolved, next_committee); + } + + #[tokio::test] + async fn anchor_mode_uses_a_committee_cache_by_default() { + let (current_committee, next_committee, _) = signed_end_of_epoch_summary(3, true); + let client = GrpcClient::new("http://127.0.0.1:1").unwrap(); + let resolver = CommitteeResolver::new(client, CommitteeResolution::anchored(current_committee)); + let CommitteeResolution::Anchored { cache, .. } = &resolver.mode else { + panic!("anchor resolver must have a committee cache"); + }; + cache.store(&next_committee).await.unwrap(); + + let resolved = resolver.resolve(4).await.unwrap(); + + assert_eq!(resolved, next_committee); + } + + #[tokio::test] + async fn memory_cache_rejects_a_conflicting_committee() { + let (_, next_committee, _) = signed_end_of_epoch_summary(3, true); + let cache = crate::MemoryCommitteeCache::new(); + cache.store(&next_committee).await.unwrap(); + let (conflicting_committee, _) = Committee::new_simple_test_committee_of_size(6); + let conflicting_committee = Committee::new(4, conflicting_committee.voting_rights.iter().cloned().collect()); + + let error = cache.store(&conflicting_committee).await.unwrap_err(); + + assert!(matches!(error, CommitteeCacheError::Conflict { epoch: 4 })); + } + + #[tokio::test] + async fn anchored_resolution_accepts_a_committee_from_a_trusted_cache() { + let (current_committee, next_committee, _) = signed_end_of_epoch_summary(3, true); + let cache = StaticCache { + committee: next_committee.clone(), + }; + let client = GrpcClient::new("http://127.0.0.1:1").unwrap(); + let resolver = CommitteeResolver::new( + client, + CommitteeResolution::anchored_with_cache(current_committee, cache), + ); + + let resolved = resolver.resolve(4).await.unwrap(); + + assert_eq!(resolved, next_committee); + } +} diff --git a/poi-rs/src/lib.rs b/poi-rs/src/lib.rs new file mode 100644 index 00000000..b09eb9e7 --- /dev/null +++ b/poi-rs/src/lib.rs @@ -0,0 +1,44 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#![doc = include_str!("../README.md")] +#![warn(missing_docs, rustdoc::all)] + +/// Shared boxed source error used by the crate's typed errors. +pub(crate) type BoxError = Box; + +/// Proof construction builders. +pub mod builder; +/// Verified committee lineage caches for anchored resolution. +pub mod cache; +/// Convenient source-backed client for proof construction and verification. +pub mod client; +/// Committee resolution for checkpoint verification. +pub mod committee; +/// Proof data types and offline verification. +pub mod proof; +/// Ledger evidence source abstraction. +pub mod source; + +pub use builder::{ProofBuilder, ProofBuilderError}; +pub use cache::{CommitteeCache, CommitteeCacheError, MemoryCommitteeCache}; +pub use client::PoiClient; +pub use committee::{ + CommitteeResolution, CommitteeResolutionError, CommitteeResolutionErrorKind, CommitteeResolver, + ProofVerificationError, +}; +pub use proof::{ + Proof, ProofTargets, ProofVerifier, ProofVersion, SerializationError, SerializationErrorKind, TransactionProof, + VerifyError, VerifyErrorKind, VersionError, +}; +pub use source::{Source, SourceCheckpoint, SourceError, SourceTransaction}; + +#[cfg(test)] +mod tests { + use crate::{PoiClient, Proof}; + + pub fn client_building_test() { + // Proof + let client = PoiClient::devnet().unwrap(); + } +} diff --git a/poi-rs/src/proof.rs b/poi-rs/src/proof.rs new file mode 100644 index 00000000..7df106a2 --- /dev/null +++ b/poi-rs/src/proof.rs @@ -0,0 +1,500 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Proof types and verification. +//! +//! A [`Proof`] contains a certified checkpoint and the data needed to prove that +//! a transaction, and optionally its objects or events, belong to that +//! checkpoint. [`ProofVerifier`] verifies this data against a caller-provided +//! [`Committee`] without making network requests. +//! +//! [`CertifiedCheckpointSummary`]: iota_types::messages_checkpoint::CertifiedCheckpointSummary +//! [`Committee`]: iota_types::committee::Committee + +use iota_sdk_types::{CheckpointContents, TransactionDigest}; +use iota_types::{ + committee::Committee, + digests::ChainIdentifier, + effects::{TransactionEffects, TransactionEffectsAPI, TransactionEffectsExt, TransactionEvents}, + event::EventID, + messages_checkpoint::{CertifiedCheckpointSummary, CheckpointContentsExt}, + object::Object, + transaction::Transaction, +}; +use serde::{Deserialize, Serialize}; + +use crate::BoxError; + +/// An unsupported proof format version. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +#[error("unsupported Proof of Inclusion proof format version: {version}")] +pub struct VersionError { + /// The unsupported version. + pub version: u16, +} + +/// An error serializing or deserializing a proof. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +#[error("failed to serialize or deserialize Proof of Inclusion proof")] +pub struct SerializationError { + /// The underlying error. + #[source] + pub kind: SerializationErrorKind, +} + +/// The cause of a [`SerializationError`]. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum SerializationErrorKind { + /// JSON encoding or decoding failed. + #[error("json serialization or deserialization failed")] + Json { + /// Error reported by `serde_json`. + #[source] + source: serde_json::Error, + }, +} + +/// An error verifying a proof. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +#[error("failed to verify Proof of Inclusion proof")] +pub struct VerifyError { + /// The reason verification failed. + #[source] + pub kind: VerifyErrorKind, +} + +/// The cause of a [`VerifyError`]. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum VerifyErrorKind { + /// The proof uses an unsupported wire-format version. + #[error("proof format version is not supported")] + Version { + /// The version error. + #[source] + source: VersionError, + }, + /// The committee signature or checkpoint-contents commitment is invalid. + #[error("checkpoint summary verification failed")] + CheckpointSummary { + /// The checkpoint verification error. + #[source] + source: BoxError, + }, + /// The proof does not declare a transaction, object, or event target. + #[error("proof does not contain a target")] + MissingTarget, + /// The selected transaction differs from the transaction packaged in the proof. + #[error("transaction target does not match the packaged transaction")] + TransactionTargetMismatch, + /// The packaged transaction does not match the transaction digest in its effects. + #[error("transaction digest does not match the execution digest")] + TransactionDigestMismatch, + /// The packaged transaction effects are absent from the authenticated checkpoint contents. + #[error("transaction digest not found in the checkpoint contents")] + TransactionNotInCheckpoint, + /// The packaged events do not match the events digest in the transaction effects. + #[error("events digest does not match the execution digest")] + EventsDigestMismatch, + /// Event targets are present but the proof does not contain transaction events. + #[error("event targets require transaction event data")] + MissingEvents, + /// An event claim identifies a transaction other than the one proven by the envelope. + #[error("event target does not belong to the transaction")] + EventTransactionMismatch, + /// An event claim refers to an index outside the packaged transaction events. + #[error("event sequence number {sequence} is out of bounds")] + EventSequenceOutOfBounds { + /// Transaction-local event index requested by the claim. + sequence: u64, + }, + /// A claimed object reference is absent from the packaged transaction effects. + #[error("object target was not found in the transaction effects")] + ObjectNotFound, +} + +/// The format version of a serialized [`Proof`]. +/// +/// Versions are encoded as unsigned integers. This crate currently supports +/// only [`ProofVersion::CURRENT`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ProofVersion(u16); + +impl ProofVersion { + /// The version produced and accepted by this crate. + pub const CURRENT: Self = Self(1); + + /// Creates a supported proof version. + /// + /// # Errors + /// + /// Returns [`VersionError`] when `version` is not [`Self::CURRENT`]. + pub fn new(version: u16) -> Result { + let version = Self(version); + version.validate()?; + Ok(version) + } + + /// Returns the numeric version. + pub const fn value(self) -> u16 { + self.0 + } + + /// Checks that this version is supported. + /// + /// # Errors + /// + /// Returns [`VersionError`] when the value is not [`Self::CURRENT`]. + pub fn validate(self) -> Result<(), VersionError> { + if self == Self::CURRENT { + Ok(()) + } else { + Err(VersionError { version: self.value() }) + } + } +} + +impl TryFrom for ProofVersion { + type Error = VersionError; + + fn try_from(version: u16) -> Result { + Self::new(version) + } +} + +/// Values the caller selected for a [`Proof`]. +/// +/// Objects and events must belong to the proven transaction. +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct ProofTargets { + /// Transaction explicitly selected by the caller. + pub transaction: Option, + + /// Objects explicitly selected by the caller. + pub objects: Vec, + + /// Events explicitly selected by the caller. + pub events: Vec, +} + +impl ProofTargets { + /// Creates an empty set of claims. + pub fn new() -> Self { + Self::default() + } + + /// Sets the selected transaction. + pub fn set_transaction(mut self, transaction: TransactionDigest) -> Self { + self.transaction = Some(transaction); + self + } + + /// Adds a selected object. + pub fn add_object(mut self, object: Object) -> Self { + self.objects.push(object); + self + } + + /// Adds a selected event. + pub fn add_event(mut self, event_id: EventID) -> Self { + self.events.push(event_id); + self + } + + /// Returns whether no target has been selected. + pub fn is_empty(&self) -> bool { + self.transaction.is_none() && self.objects.is_empty() && self.events.is_empty() + } +} + +/// Transaction-specific evidence carried by a [`Proof`]. +/// +/// The effects identify the transaction in its checkpoint. Event data is +/// included when the proof declares event targets. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransactionProof { + /// The transaction being proven. + pub transaction: Transaction, + /// The transaction's execution effects. + pub effects: TransactionEffects, + /// Complete event list included when the proof declares event targets. + pub events: Option, +} + +impl TransactionProof { + /// Creates transaction proof data. + pub fn new(transaction: Transaction, effects: TransactionEffects, events: Option) -> Self { + Self { + transaction, + effects, + events, + } + } +} + +/// Evidence that a transaction is included in a certified checkpoint. +/// +/// [`ProofTargets`] records the values selected by the caller. The checkpoint +/// and transaction proof fields contain the evidence for those targets. +/// +/// [`Proof::chain`] identifies the network reported by the proof source. It is +/// informational and is not checked during verification. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Proof { + /// The proof format version. + pub version: ProofVersion, + /// The network reported by the proof source. + pub chain: ChainIdentifier, + /// The values selected for this proof. + pub targets: ProofTargets, + /// The certified summary of the checkpoint containing the transaction. + pub checkpoint_summary: CertifiedCheckpointSummary, + /// Contents committed to by the checkpoint summary. + pub checkpoint_contents: CheckpointContents, + /// The transaction and its execution data + pub transaction_proof: TransactionProof, +} + +impl Proof { + /// Creates a proof using [`ProofVersion::CURRENT`]. + pub fn new( + chain: ChainIdentifier, + targets: ProofTargets, + checkpoint_summary: CertifiedCheckpointSummary, + checkpoint_contents: CheckpointContents, + transaction_proof: TransactionProof, + ) -> Self { + Self { + version: ProofVersion::CURRENT, + chain, + targets, + checkpoint_summary, + checkpoint_contents, + transaction_proof, + } + } + + /// Returns the proof format version. + pub const fn version(&self) -> ProofVersion { + self.version + } + + /// Returns the values selected for this proof. + pub const fn targets(&self) -> &ProofTargets { + &self.targets + } + + /// Serializes the proof as JSON. + /// + /// # Errors + /// + /// Returns an error if the proof cannot be serialized. + pub fn to_json_vec(&self) -> Result, SerializationError> { + serde_json::to_vec(self).map_err(|source| SerializationError { + kind: SerializationErrorKind::Json { source }, + }) + } + + /// Deserializes a proof from JSON. + /// + /// # Errors + /// + /// Returns an error if `bytes` do not contain a valid JSON representation of + /// a [`Proof`]. + pub fn from_json_slice(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(|source| SerializationError { + kind: SerializationErrorKind::Json { source }, + }) + } + + /// Checks that the proof format version is supported. + /// + /// # Errors + /// + /// Returns [`VersionError`] when [`Self::version`] is unsupported. + pub fn validate(&self) -> Result<(), VersionError> { + self.version.validate() + } +} + +/// Verifies proofs against a trusted committee. +/// +/// Verification is offline. The verifier does not resolve committee history or +/// fetch missing proof data. The caller is responsible for supplying the +/// committee that certified the proof's checkpoint. +/// +/// The value of [`Proof::chain`] is not used to select or validate the committee. +#[derive(Clone, Copy, Debug)] +pub struct ProofVerifier<'committee> { + committee: &'committee Committee, +} + +impl<'committee> ProofVerifier<'committee> { + /// Creates a verifier using `committee` as its trust root. + pub const fn new(committee: &'committee Committee) -> Self { + Self { committee } + } + + /// Returns the committee used to verify checkpoint signatures. + pub const fn committee(&self) -> &'committee Committee { + self.committee + } + + /// Verifies a proof and all of its claims. + /// + /// Verification checks that: + /// + /// - the proof uses a supported format version; + /// - the committee certifies the checkpoint summary; + /// - the checkpoint contents match the digest in that summary; + /// - the transaction, effects, and optional events are internally consistent; + /// - the transaction effects occur in the authenticated checkpoint contents; + /// - every selected target matches the authenticated proof data. + /// + /// # Errors + /// + /// Returns an error if any check fails. + pub fn verify(&self, proof: &Proof) -> Result<(), VerifyError> { + proof.validate().map_err(|source| VerifyError { + kind: VerifyErrorKind::Version { source }, + })?; + + if proof.targets.is_empty() { + return Err(VerifyError { + kind: VerifyErrorKind::MissingTarget, + }); + } + + let summary = &proof.checkpoint_summary; + let contents = Some(&proof.checkpoint_contents); + + summary + .verify_with_contents(self.committee, contents) + .map_err(|source| VerifyError { + kind: VerifyErrorKind::CheckpointSummary { + source: Box::new(source), + }, + })?; + + self.verify_transaction_proof(summary, &proof.checkpoint_contents, &proof.transaction_proof)?; + self.verify_targets(&proof.targets, &proof.transaction_proof)?; + + Ok(()) + } + + /// Checks the transaction-to-effects, effects-to-checkpoint, and effects-to-events links. + fn verify_transaction_proof( + &self, + summary: &CertifiedCheckpointSummary, + checkpoint_contents: &CheckpointContents, + transaction_proof: &TransactionProof, + ) -> Result<(), VerifyError> { + let execution_digests = transaction_proof.effects.execution_digests(); + + if transaction_proof.transaction.digest() != &execution_digests.transaction { + return Err(VerifyError { + kind: VerifyErrorKind::TransactionDigestMismatch, + }); + } + + let transaction_is_in_checkpoint = checkpoint_contents + .enumerate_transactions(summary) + .any(|(_, digests)| digests == execution_digests); + + if !transaction_is_in_checkpoint { + return Err(VerifyError { + kind: VerifyErrorKind::TransactionNotInCheckpoint, + }); + } + + if let Some(events) = &transaction_proof.events { + if transaction_proof.effects.events_digest() != Some(&events.digest()) { + return Err(VerifyError { + kind: VerifyErrorKind::EventsDigestMismatch, + }); + } + } + + Ok(()) + } + + /// Checks every declared target against the transaction proof. + fn verify_targets(&self, targets: &ProofTargets, transaction_proof: &TransactionProof) -> Result<(), VerifyError> { + let transaction_digest = transaction_proof.effects.execution_digests().transaction; + + if targets.transaction.is_some_and(|target| target != transaction_digest) { + return Err(VerifyError { + kind: VerifyErrorKind::TransactionTargetMismatch, + }); + } + + self.verify_event_targets(targets, transaction_proof)?; + self.verify_object_targets(targets, transaction_proof) + } + + /// Checks each event target against the proven transaction and its packaged events. + fn verify_event_targets( + &self, + targets: &ProofTargets, + transaction_proof: &TransactionProof, + ) -> Result<(), VerifyError> { + if targets.events.is_empty() { + return Ok(()); + } + + let Some(events) = &transaction_proof.events else { + return Err(VerifyError { + kind: VerifyErrorKind::MissingEvents, + }); + }; + + let execution_digests = transaction_proof.effects.execution_digests(); + for event_id in &targets.events { + if event_id.tx_digest != execution_digests.transaction { + return Err(VerifyError { + kind: VerifyErrorKind::EventTransactionMismatch, + }); + } + + let event_index = event_id.event_seq as usize; + let Some(_) = events.get(event_index) else { + return Err(VerifyError { + kind: VerifyErrorKind::EventSequenceOutOfBounds { + sequence: event_id.event_seq, + }, + }); + }; + } + + Ok(()) + } + + /// Checks each object target against the transaction effects. + fn verify_object_targets( + &self, + targets: &ProofTargets, + transaction_proof: &TransactionProof, + ) -> Result<(), VerifyError> { + if targets.objects.is_empty() { + return Ok(()); + } + + let changed_objects = transaction_proof.effects.all_changed_objects(); + for object in &targets.objects { + let object_ref = object.as_inner().object_ref(); + changed_objects + .iter() + .find(|changed_object_ref| changed_object_ref.0 == object_ref) + .ok_or(VerifyError { + kind: VerifyErrorKind::ObjectNotFound, + })?; + } + + Ok(()) + } +} diff --git a/poi-rs/src/source.rs b/poi-rs/src/source.rs new file mode 100644 index 00000000..40361d71 --- /dev/null +++ b/poi-rs/src/source.rs @@ -0,0 +1,127 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use iota_sdk_types::{CheckpointContents, ObjectId, TransactionDigest, Version}; +use iota_types::{ + committee::{Committee, EpochId}, + digests::ChainIdentifier, + effects::{TransactionEffects, TransactionEvents}, + messages_checkpoint::CertifiedCheckpointSummary, + object::Object, + transaction::Transaction, +}; + +use crate::BoxError; + +#[cfg(feature = "native-grpc")] +mod grpc; + +/// Error returned when a ledger source cannot provide requested data. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum SourceError { + /// A request to the source failed. + #[error("source request failed")] + Request { + /// Underlying source error. + #[source] + source: BoxError, + }, + /// A response could not be decoded or converted. + #[error("source returned an invalid response")] + InvalidResponse { + /// Underlying response or conversion error. + #[source] + source: BoxError, + }, + /// Required source data was omitted. + #[error("source response is missing required data")] + MissingData { + /// Underlying response error. + #[source] + source: BoxError, + }, +} + +impl SourceError { + /// Creates an error for a failed source request. + pub fn request(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::Request { + source: Box::new(source), + } + } + + /// Creates an error for an invalid source response. + pub fn invalid_response(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::InvalidResponse { + source: Box::new(source), + } + } + + /// Creates an error for required data omitted from a source response. + pub fn missing_data(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::MissingData { + source: Box::new(source), + } + } +} + +/// Decoded transaction evidence returned by a [`Source`]. +/// +/// This type contains IOTA domain values rather than transport-specific gRPC or +/// protobuf messages. +pub struct SourceTransaction { + /// Signed transaction being authenticated. + pub transaction: Transaction, + /// Effects produced by executing the transaction. + pub effects: TransactionEffects, + /// Events emitted by the transaction, when present. + pub events: Option, + /// Sequence number of the checkpoint that includes the transaction. + pub checkpoint_sequence_number: u64, +} + +/// Decoded checkpoint evidence returned by a [`Source`]. +/// +/// The certified summary authenticates the checkpoint contents used by the +/// transaction proof. +pub struct SourceCheckpoint { + /// Certified checkpoint summary. + pub summary: CertifiedCheckpointSummary, + /// Contents committed to by the checkpoint summary. + pub contents: CheckpointContents, +} + +/// Ledger-read boundary used by [`crate::ProofBuilder`] and [`crate::CommitteeResolver`]. +/// +/// Implementations may fetch evidence from native gRPC, a JavaScript client, +/// archive storage, fixtures, or another source. Proof assembly, target +/// validation, committee authentication, and caching remain outside the source. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Source { + /// Fetches the genesis-checkpoint digest that identifies the source chain. + async fn chain_identifier(&self) -> Result; + + /// Fetches and decodes one executed transaction. + async fn transaction( + &self, + transaction_digest: TransactionDigest, + ) -> Result, SourceError>; + + /// Fetches and decodes an object, optionally at an exact version. + async fn object(&self, object_id: ObjectId, version: Option) -> Result, SourceError>; + + /// Fetches and decodes one certified checkpoint and its contents. + async fn checkpoint(&self, sequence_number: u64) -> Result; + + /// Fetches the committee reported for `epoch`. + async fn committee(&self, epoch: EpochId) -> Result; + + /// Fetches the current epoch reported by the source. + async fn current_epoch(&self) -> Result, SourceError>; + + /// Fetches the certified checkpoint summary that closed `epoch`. + async fn epoch_close_summary(&self, epoch: EpochId) -> Result, SourceError>; +} diff --git a/poi-rs/src/source/grpc.rs b/poi-rs/src/source/grpc.rs new file mode 100644 index 00000000..9c5e3d4c --- /dev/null +++ b/poi-rs/src/source/grpc.rs @@ -0,0 +1,201 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use iota_grpc_client::{ + Client as GrpcClient, ReadMask, + read_mask_fields::{CheckpointResponseField, EpochField, ObjectField, ServiceInfoField, TransactionField}, +}; +use iota_grpc_types::proto::TryFromProtoError; +use iota_sdk_types::{ + CheckpointContents, CheckpointDigest, ObjectId, SignedCheckpointSummary, SignedTransaction, TransactionDigest, + Version, +}; +use iota_types::{ + committee::{Committee, EpochId}, + digests::ChainIdentifier, + effects::TransactionEffectsAPI, + messages_checkpoint::CertifiedCheckpointSummary, + object::Object, + transaction::Transaction, +}; + +use super::{Source, SourceCheckpoint, SourceError, SourceTransaction}; + +#[async_trait] +impl Source for GrpcClient { + async fn chain_identifier(&self) -> Result { + let service_info = self + .get_service_info(Some(ReadMask::from(ServiceInfoField::CHAIN_ID))) + .await + .map_err(SourceError::request)?; + let chain_identifier = service_info + .body() + .chain_identifier() + .map_err(SourceError::invalid_response)?; + + Ok(ChainIdentifier::from(CheckpointDigest::new( + chain_identifier.into_inner(), + ))) + } + + async fn transaction( + &self, + transaction_digest: TransactionDigest, + ) -> Result, SourceError> { + let transactions = self + .get_transactions( + &[transaction_digest], + Some(ReadMask::from(&[ + TransactionField::TRANSACTION_BCS, + TransactionField::SIGNATURES, + TransactionField::EFFECTS_BCS, + TransactionField::EVENTS_DIGEST, + TransactionField::EVENTS_EVENTS_BCS, + TransactionField::CHECKPOINT, + ])), + ) + .await + .map_err(SourceError::request)?; + let Some(executed_transaction) = transactions.body().first() else { + return Ok(None); + }; + let effects = executed_transaction + .effects() + .map_err(SourceError::invalid_response)? + .effects() + .map_err(SourceError::invalid_response)?; + + let transaction = executed_transaction + .transaction() + .map_err(SourceError::invalid_response)? + .transaction() + .map_err(SourceError::invalid_response)?; + let signatures = executed_transaction + .signatures() + .map_err(SourceError::invalid_response)? + .signatures + .iter() + .map(|signature| signature.signature().map_err(SourceError::invalid_response)) + .collect::, SourceError>>()?; + let transaction: Transaction = SignedTransaction { + transaction, + signatures, + } + .into(); + let events = if effects.events_digest().is_some() { + executed_transaction + .events() + .map_err(SourceError::missing_data)? + .events() + .map_err(SourceError::invalid_response) + .map(Some)? + } else { + None + }; + let checkpoint_sequence_number = executed_transaction + .checkpoint_sequence_number() + .map_err(SourceError::missing_data)?; + + Ok(Some(SourceTransaction { + transaction, + effects, + events, + checkpoint_sequence_number, + })) + } + + async fn object(&self, object_id: ObjectId, version: Option) -> Result, SourceError> { + let objects = self + .get_objects(&[(object_id, version)], Some(ReadMask::from(ObjectField::BCS))) + .await + .map_err(SourceError::request)?; + let Some(response) = objects.body().first() else { + return Ok(None); + }; + let object: Object = response.object().map_err(SourceError::invalid_response)?.into(); + + Ok(Some(object)) + } + + async fn checkpoint(&self, sequence_number: u64) -> Result { + let checkpoint = self + .get_checkpoint_by_sequence_number( + sequence_number, + Some(ReadMask::from(&[ + CheckpointResponseField::CHECKPOINT_SUMMARY_BCS, + CheckpointResponseField::CHECKPOINT_SIGNATURE, + CheckpointResponseField::CHECKPOINT_CONTENTS_BCS, + ])), + None, + None, + ) + .await + .map(|response| response.into_inner()) + .map_err(SourceError::request)?; + let summary: CertifiedCheckpointSummary = checkpoint + .signed_summary() + .map_err(SourceError::invalid_response)? + .try_into() + .map_err(SourceError::invalid_response)?; + let contents: CheckpointContents = checkpoint + .contents() + .map_err(SourceError::invalid_response)? + .contents() + .map_err(SourceError::invalid_response)?; + + Ok(SourceCheckpoint { summary, contents }) + } + + async fn committee(&self, epoch: EpochId) -> Result { + let epoch_info = self + .get_epoch(Some(epoch), Some(ReadMask::from(EpochField::COMMITTEE))) + .await + .map_err(SourceError::request)? + .into_inner(); + let committee = epoch_info.committee().map_err(SourceError::invalid_response)?; + + Ok(committee.into()) + } + + async fn current_epoch(&self) -> Result, SourceError> { + self.get_service_info(Some(ReadMask::from(ServiceInfoField::EPOCH))) + .await + .map(|response| response.body().epoch) + .map_err(SourceError::request) + } + + async fn epoch_close_summary(&self, epoch: EpochId) -> Result, SourceError> { + let epoch_info = self + .get_epoch( + Some(epoch), + Some(ReadMask::from(EpochField::EPOCH_CLOSE_PROOF_CHECKPOINT)), + ) + .await + .map_err(SourceError::request)? + .into_inner(); + let Some(epoch_close_proof) = epoch_info.epoch_close_proof().map_err(SourceError::invalid_response)? else { + return Ok(None); + }; + let checkpoint = epoch_close_proof.checkpoint().map_err(SourceError::missing_data)?; + let summary = checkpoint + .summary + .as_ref() + .ok_or_else(|| TryFromProtoError::missing("summary")) + .map_err(SourceError::missing_data)?; + let summary = summary.summary().map_err(SourceError::invalid_response)?; + let signature = checkpoint + .signature + .as_ref() + .ok_or_else(|| TryFromProtoError::missing("signature")) + .map_err(SourceError::missing_data)?; + let signature = signature.signature().map_err(SourceError::invalid_response)?; + let signed_summary = SignedCheckpointSummary { + checkpoint: summary, + signature, + }; + let certified_summary = signed_summary.try_into().map_err(SourceError::invalid_response)?; + + Ok(Some(certified_summary)) + } +} diff --git a/poi-rs/tests/committee_resolution.rs b/poi-rs/tests/committee_resolution.rs new file mode 100644 index 00000000..3919d740 --- /dev/null +++ b/poi-rs/tests/committee_resolution.rs @@ -0,0 +1,107 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod utils; + +use std::fs::File; + +use iota_config::IOTA_GENESIS_FILENAME; +use iota_grpc_client::Client as GrpcClient; +use poi_rs::{CommitteeCache, CommitteeResolution, CommitteeResolutionErrorKind, MemoryCommitteeCache, PoiClient}; +use utils::{advance_to_epoch, grpc_client, start_test_cluster}; + +use crate::utils::committee_at; + +fn disconnected_client() -> GrpcClient { + GrpcClient::new("http://127.0.0.1:1").expect("disconnected gRPC client must be constructed") +} + +#[tokio::test] +async fn genesis_anchored_client_authenticates_committee_across_epochs() { + let cluster = start_test_cluster().await; + let expected = advance_to_epoch(&cluster, 10).await; + let genesis_path = cluster.swarm.dir().join(IOTA_GENESIS_FILENAME); + let genesis = File::open(genesis_path).expect("test cluster genesis blob must be available"); + let cache = MemoryCommitteeCache::new(); + + let resolution = CommitteeResolution::from_genesis_with_cache(genesis, cache.clone()) + .expect("test cluster genesis committee must be extractable"); + let resolver = PoiClient::new(grpc_client(&cluster)).verifier(resolution); + + let resolved = resolver + .resolve(10) + .await + .expect("epoch 10 committee must resolve from genesis"); + + assert_eq!(resolved, expected[10]); + assert_eq!( + cache + .committee(10) + .await + .expect("caller-provided cache must remain readable"), + Some(expected[10].clone()) + ); +} + +#[tokio::test] +async fn committee_anchored_client_returns_its_trust_anchor_without_fetching() { + let trusted_committee = committee_at(7); + let resolver = + PoiClient::new(disconnected_client()).verifier(CommitteeResolution::anchored(trusted_committee.clone())); + + let resolved = resolver + .resolve(7) + .await + .expect("the trusted committee must resolve without fetching"); + + assert_eq!(resolved, trusted_committee); +} + +#[tokio::test] +async fn committee_anchored_client_rejects_epochs_before_its_anchor_without_fetching() { + let resolver = PoiClient::new(disconnected_client()).verifier(CommitteeResolution::anchored(committee_at(7))); + + let error = resolver + .resolve(6) + .await + .expect_err("an anchored resolver cannot walk backwards"); + + assert_eq!(error.target_epoch, 6); + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::TargetBeforeAnchor { anchor_epoch: 7 } + )); +} + +#[tokio::test] +async fn genesis_anchored_client_rejects_epochs_ahead_of_the_node() { + let cluster = start_test_cluster().await; + let genesis_path = cluster.swarm.dir().join(IOTA_GENESIS_FILENAME); + let genesis = File::open(genesis_path).expect("test cluster genesis blob must be available"); + let resolution = + CommitteeResolution::from_genesis(genesis).expect("test cluster genesis committee must be extractable"); + let resolver = PoiClient::new(grpc_client(&cluster)).verifier(resolution); + + let error = resolver + .resolve(1) + .await + .expect_err("an epoch beyond the node's current epoch must be rejected"); + + assert!(matches!( + error.kind, + CommitteeResolutionErrorKind::TargetAheadOfNode { current_epoch: 0 } + )); +} + +#[tokio::test] +async fn trusted_node_client_returns_the_committee_reported_by_its_source() { + let cluster = start_test_cluster().await; + let resolver = PoiClient::new(grpc_client(&cluster)).verifier(CommitteeResolution::TrustedNode); + + let resolved = resolver + .resolve(0) + .await + .expect("trusted node must return its genesis committee"); + + assert_eq!(resolved, *cluster.committee()); +} diff --git a/poi-rs/tests/fixtures/current/committee.json b/poi-rs/tests/fixtures/current/committee.json new file mode 100644 index 00000000..4358ddcf --- /dev/null +++ b/poi-rs/tests/fixtures/current/committee.json @@ -0,0 +1,15 @@ +{ + "epoch": 0, + "voting_rights": [ + [ + "mfJe9h+AMrkUY2RgmCxcxvE07x3a52ZX8sv+wev8jQlzdAgN9vzw3Li8Sw2OCvXYDrv/K0xZn1T0LWMS38MUJ2B4wcw0fru+xRmL4lhRPzhrkw0CwnSagD4jMJVevRoQ", + 10000 + ] + ], + "expanded_keys": { + "mfJe9h+AMrkUY2RgmCxcxvE07x3a52ZX8sv+wev8jQlzdAgN9vzw3Li8Sw2OCvXYDrv/K0xZn1T0LWMS38MUJ2B4wcw0fru+xRmL4lhRPzhrkw0CwnSagD4jMJVevRoQ": "mfJe9h+AMrkUY2RgmCxcxvE07x3a52ZX8sv+wev8jQlzdAgN9vzw3Li8Sw2OCvXYDrv/K0xZn1T0LWMS38MUJ2B4wcw0fru+xRmL4lhRPzhrkw0CwnSagD4jMJVevRoQ" + }, + "index_map": { + "mfJe9h+AMrkUY2RgmCxcxvE07x3a52ZX8sv+wev8jQlzdAgN9vzw3Li8Sw2OCvXYDrv/K0xZn1T0LWMS38MUJ2B4wcw0fru+xRmL4lhRPzhrkw0CwnSagD4jMJVevRoQ": 0 + } +} \ No newline at end of file diff --git a/poi-rs/tests/fixtures/current/event.json b/poi-rs/tests/fixtures/current/event.json new file mode 100644 index 00000000..164b1414 --- /dev/null +++ b/poi-rs/tests/fixtures/current/event.json @@ -0,0 +1,346 @@ +{ + "version": 1, + "chain": "J3x3hNka6L8T5y8VwEBHgJYLm5XgLm4FCQwmDcUpNAf5", + "targets": { + "transaction": null, + "objects": [], + "events": [ + { + "txDigest": "25zdMVEMRtg7pDGqgLgL1Hf3s8sL9YGuN9UVeo3dECG6", + "eventSeq": "0" + } + ] + }, + "checkpoint_summary": { + "data": { + "epoch": "0", + "sequence_number": "7", + "network_total_transactions": "40", + "content_digest": "C7AbY9VsMGiK43TCv6s8FnL77Wf97KQsdJ2AEVTkXdK1", + "previous_digest": "D8kX1nzCc2MGmXRyPTcP9iT6spLirMSU9BYfddkGUe8R", + "epoch_rolling_gas_cost_summary": { + "computation_cost": "2000000", + "computation_cost_burned": "2000000", + "storage_cost": "16537600", + "storage_rebate": "0", + "non_refundable_storage_fee": "0" + }, + "timestamp_ms": "1785157327014", + "checkpoint_commitments": [], + "end_of_epoch_data": null, + "version_specific_data": "AAECAAAAAAAAAA==" + }, + "auth_signature": { + "epoch": 0, + "signature": "uWe3felkoJULwW0GX+V1uIz2Zkp/DyNdilZVgBL5NYl3PBRcTx+PjNywaxF9ZSDY", + "signers_map": [ + 58, + 48, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 0, + 0, + 0, + 0, + 0 + ] + } + }, + "checkpoint_contents": { + "V1": [ + { + "transaction": "123ugpG5FWSQZ3yZkxVxNyw3RjDobNMGTXDom6aJsnDU", + "effects": "BBbiQ6R1A9QTCg4u3KrQw7nXtNJN9o4TFLZ3az93BEYJ", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "6Sjy1JpAgvYWp9NFZ6tYY3AMxjsB8d8Wjr7o7YDoThP6", + "effects": "JBMBLxwJWvnRaAbHVM1x1LRizxE8LqPmoLrfG1ZEikkv", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "5a2akdP8QpYw5Tr7d4iE4NbybHeJw8JgRQ3PPiPfJuk7", + "effects": "G8r2qRw4cCWx5zet4RE3qpHRGyywKHB378kQ9Ku1vdVe", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "25zdMVEMRtg7pDGqgLgL1Hf3s8sL9YGuN9UVeo3dECG6", + "effects": "fG1qHEADjgH1rbEMBy3kvLDhFcBi6akkRBf5eBsYHTt", + "signatures": [ + { + "scheme": "ed25519", + "signature": "ny3sKc8wQfcK3jOtxov8TvX3FsAQmaNE+/Kmhl5ODZyPrbiwog63/67vek1SpJqCLI8HU122VxiSuKB7G8NHDA==", + "public_key": "3tudne38ft4blQx4QMgi89Dbud82vs9JX0wqZJjR61k=" + } + ] + }, + { + "transaction": "B1yr1zybhNUwKeadAxCRvhnYdZhhk1BDLgkjRx693bbu", + "effects": "73QeVLRjs57CqEzR7Za1pSbFNJurnYZiZw8wbDwNcMsK", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "5JQhS5my3KrapKu2hKsbLRyqtHrLcB9RTggGsd5Pyx3F", + "effects": "CBqkCM2cP3xVHb9nEwNFk1QJ8veELTtBPdQYJGUxvvun", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "GbemSdr6vTaJ5q6KanqizjNQCy59fJgkHpexCRBfTHrp", + "effects": "EN4LoN2V7ffkQ2LoDnvXNL72pSbPb3335VYtVjScxULv", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + } + ] + }, + "transaction_proof": { + "transaction": { + "data": { + "transaction": { + "V1": { + "kind": { + "Programmable": { + "inputs": [ + { + "Shared": { + "object_id": "0x0000000000000000000000000000000000000000000000000000000000000005", + "initial_shared_version": "1", + "mutable": true + } + }, + { + "ImmutableOrOwned": { + "object_id": "0x7d2ecf75235b50ed8fa4b3286a28a8f9aa795d3ab267da775a6712e45d3787b4", + "version": "1", + "digest": "8mErr9cmex177LFsahrQAuBEpReKyLqhN1sAjnzyGunU" + } + }, + { + "Pure": "jEVfIpF8OWtGo/Tr1idyhopmbms4T08M7mjQBN5F7s8=" + } + ], + "commands": [ + { + "MoveCall": { + "package": "0x0000000000000000000000000000000000000000000000000000000000000003", + "module": "iota_system", + "function": "request_add_stake", + "type_arguments": [], + "arguments": [ + { + "Input": 0 + }, + { + "Input": 1 + }, + { + "Input": 2 + } + ] + } + } + ] + } + }, + "sender": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b", + "gas_payment": { + "objects": [ + { + "object_id": "0x97eb1bbdf3f02a31d920009da79477162af50544959db47a2dfefe9e7fbdd1b6", + "version": "1", + "digest": "HFCwd7tYY9ChQZay1eMGgVYYexdVUFacZ4TPhzX3cT1K" + } + ], + "owner": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b", + "price": "1000", + "budget": "5000000000" + }, + "expiration": "None" + } + }, + "signatures": [ + { + "scheme": "ed25519", + "signature": "ny3sKc8wQfcK3jOtxov8TvX3FsAQmaNE+/Kmhl5ODZyPrbiwog63/67vek1SpJqCLI8HU122VxiSuKB7G8NHDA==", + "public_key": "3tudne38ft4blQx4QMgi89Dbud82vs9JX0wqZJjR61k=" + } + ] + }, + "auth_signature": {} + }, + "effects": { + "V1": { + "status": { + "success": true + }, + "epoch": "0", + "gas_cost_summary": { + "computation_cost": "1000000", + "computation_cost_burned": "1000000", + "storage_cost": "14576800", + "storage_rebate": "0", + "non_refundable_storage_fee": "0" + }, + "transaction_digest": "25zdMVEMRtg7pDGqgLgL1Hf3s8sL9YGuN9UVeo3dECG6", + "gas_object_index": 5, + "events_digest": "9Y8mNCezYf67FvKWRYKBg57TZcqjApuxDU2ZQwXnFHSN", + "dependencies": [ + "54ohH6BW2vfMLD6r63KKuZhcgrMq2ty4XU9JHD7D2HAW" + ], + "lamport_version": "2", + "changed_objects": [ + { + "object_id": "0x0000000000000000000000000000000000000000000000000000000000000005", + "input_state": { + "Data": { + "version": "1", + "digest": "4ohgCSrvVpNCSaWJJU46aBZt3NZDJ3q1BLayT8NiFcMm", + "owner": { + "Shared": "1" + } + } + }, + "output_state": { + "ObjectWrite": { + "digest": "2KwThepY7sQfMY26i9JcB74CaqyQbGyZkgKdv3txJZMR", + "owner": { + "Shared": "1" + } + } + }, + "id_operation": "none" + }, + { + "object_id": "0x328ca92144d761e46332a48827e1bd457b84be93342c098a7accafbf0b400600", + "input_state": "Missing", + "output_state": { + "ObjectWrite": { + "digest": "7NkZCXoPjeFjAn2BiwqoWL8BBdHKHy66uX2PWFc5dzja", + "owner": { + "Address": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b" + } + } + }, + "id_operation": "created" + }, + { + "object_id": "0x5b890eaf2abcfa2ab90b77b8e6f3d5d8609586c3e583baf3dccd5af17edf48d1", + "input_state": "Missing", + "output_state": { + "ObjectWrite": { + "digest": "8nLcDJUXKTabUKDPjTisr3ytEJCZ23szL6P4r7X9UKLs", + "owner": { + "Object": "0x0000000000000000000000000000000000000000000000000000000000000005" + } + } + }, + "id_operation": "created" + }, + { + "object_id": "0x6af2a2b7ca60bf76174adfd3e9c4957f8e937759603182f9b46c7f6c5f19c6d2", + "input_state": { + "Data": { + "version": "1", + "digest": "GzB88P8Qmp4pikBvydmYGWVPb224hSi2gZgWbZUUvaU3", + "owner": { + "Object": "0x0000000000000000000000000000000000000000000000000000000000000005" + } + } + }, + "output_state": "Missing", + "id_operation": "deleted" + }, + { + "object_id": "0x7d2ecf75235b50ed8fa4b3286a28a8f9aa795d3ab267da775a6712e45d3787b4", + "input_state": { + "Data": { + "version": "1", + "digest": "8mErr9cmex177LFsahrQAuBEpReKyLqhN1sAjnzyGunU", + "owner": { + "Address": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b" + } + } + }, + "output_state": "Missing", + "id_operation": "deleted" + }, + { + "object_id": "0x97eb1bbdf3f02a31d920009da79477162af50544959db47a2dfefe9e7fbdd1b6", + "input_state": { + "Data": { + "version": "1", + "digest": "HFCwd7tYY9ChQZay1eMGgVYYexdVUFacZ4TPhzX3cT1K", + "owner": { + "Address": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b" + } + } + }, + "output_state": { + "ObjectWrite": { + "digest": "6Pu7xMgpN2URN5vccHzYF4BqCg3x39doN4HCPU1zGsgU", + "owner": { + "Address": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b" + } + } + }, + "id_operation": "none" + } + ], + "unchanged_shared_objects": [], + "auxiliary_data_digest": null + } + }, + "events": [ + { + "package_id": "0x0000000000000000000000000000000000000000000000000000000000000003", + "module": "iota_system", + "sender": "0xc9d530a1332da708bfad2d4b634cafa265b65b5d4de29e552d9200e4f117fc0b", + "type": "0x3::validator::StakingRequestEvent", + "contents": "Ls9FicZo2wce3fWuF76Ozh9MHFqDi/lbNDQo5sI7qjGMRV8ikXw5a0aj9OvWJ3KGimZuazhPTwzuaNAE3kXuz8nVMKEzLacIv60tS2NMr6JltltdTeKeVS2SAOTxF/wLAAAAAAAAAAAAAENP15RqAA==" + } + ] + } +} diff --git a/poi-rs/tests/fixtures/current/object.json b/poi-rs/tests/fixtures/current/object.json new file mode 100644 index 00000000..c69064fe --- /dev/null +++ b/poi-rs/tests/fixtures/current/object.json @@ -0,0 +1,278 @@ +{ + "version": 1, + "chain": "J3x3hNka6L8T5y8VwEBHgJYLm5XgLm4FCQwmDcUpNAf5", + "targets": { + "transaction": null, + "objects": [ + { + "data": { + "Struct": { + "object_type": "0x2::coin::Coin<0x2::iota::IOTA>", + "version": "2", + "contents": "H47z0pKUgqQ6vEPDaTppXiR9Vf2GVjZxm9QopgbTNmxf0hVP15RqAA==" + } + }, + "owner": { + "Address": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7" + }, + "previous_transaction": "W5a5vsCEVHTj5woXy1MymQYpe4UFzwEoor8k8PASDeq", + "storage_rebate": "980400" + } + ], + "events": [] + }, + "checkpoint_summary": { + "data": { + "epoch": "0", + "sequence_number": "2", + "network_total_transactions": "10", + "content_digest": "6EucatZAZSiXpJBkyeozmy1kzRERXWq1ty7N1grkTuSk", + "previous_digest": "Et5CyqsQpZSbHgai9vf9qhJspF2SvF1uV75CGsnRft36", + "epoch_rolling_gas_cost_summary": { + "computation_cost": "1000000", + "computation_cost_burned": "1000000", + "storage_cost": "1960800", + "storage_rebate": "0", + "non_refundable_storage_fee": "0" + }, + "timestamp_ms": "1785157325745", + "checkpoint_commitments": [], + "end_of_epoch_data": null, + "version_specific_data": "AAA=" + }, + "auth_signature": { + "epoch": 0, + "signature": "hU5kzMpMZw1NCnTOZtpdxH8pmxpk8gHbIpza8vveKcp3oqiM4J+OpqR5f7+Fn402", + "signers_map": [ + 58, + 48, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 0, + 0, + 0, + 0, + 0 + ] + } + }, + "checkpoint_contents": { + "V1": [ + { + "transaction": "GuWB2VN7iieJPXADJmdrPsEPXffkC2J36ynXKzh3Jyow", + "effects": "D9ZR6gfoHMBMQS6XtrotsPBMykcVUJwYQY9obfnUcXuz", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "84s7gmgJVA8uVtKi9HpGSJtkVfv838oirdMmEpMYhgog", + "effects": "EZYAw9QNDhFiTHaJ3W28jRMGRdvvhpy1q4g1x8BwgCKo", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "DhvRjECEkgd3PmmPpUwjFtnk9cSC2M3oDayagwvRVSYw", + "effects": "8PMXpKFk8uFGBEv2UyqGgycfSPE5wfnjtyEdt8LUEHtf", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "GuHQHaXjy8ZPipiYYYcrWX8g2bfkdDni2pEPoFJUSNwq", + "effects": "BoRtHiQf9kGur9XRHagdXq4kXN7huyibCeJ65Y2o55wY", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "W5a5vsCEVHTj5woXy1MymQYpe4UFzwEoor8k8PASDeq", + "effects": "4xbK8Ebw8yq5VMd4ztJbk1eb5AFyKvUtSoRGHRZ9jnCD", + "signatures": [ + { + "scheme": "ed25519", + "signature": "o6PoW/P7BpDmgVkkOKnmVEsS5OqU8+kUxaENWwRxGcAtYvt+BbybZ3bW57oE5v5/USn7Ag7S892tPw5PL/jdBQ==", + "public_key": "i2HWsF9A7vdmrnxpB6mNXFujxewIT2B/+IltXIpxkCg=" + } + ] + }, + { + "transaction": "7NShvzK9emiobPtBphmqZapFmww6rDPnGSJWU2U9f41E", + "effects": "AtRzYVJW8yrhQEAi8V8PutQz7rrwhR9sHz3hRXW6JfHr", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "Dnzx53gMFPuTu9DSbBDiBbVxqgf9DoJ9ep3j8CBTPogw", + "effects": "3vRAtw8276TpaSbNk8TMS12HCb2sFe1EWw7wkn7hecZc", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + } + ] + }, + "transaction_proof": { + "transaction": { + "data": { + "transaction": { + "V1": { + "kind": { + "Programmable": { + "inputs": [ + { + "Pure": "mY0mkbKsOUO9bNh5FeqbV7rBk8XcB1fyWn0rQSfIe/o=" + }, + { + "Pure": "AQAAAAAAAAA=" + } + ], + "commands": [ + { + "SplitCoins": { + "coin": "Gas", + "amounts": [ + { + "Input": 1 + } + ] + } + }, + { + "TransferObjects": { + "objects": [ + { + "Result": 0 + } + ], + "address": { + "Input": 0 + } + } + } + ] + } + }, + "sender": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7", + "gas_payment": { + "objects": [ + { + "object_id": "0x1f8ef3d2929482a43abc43c3693a695e247d55fd865636719bd428a606d3366c", + "version": "1", + "digest": "4VSj48G4aoTSAYwavNA2Y79gCLQ9uHFh5yQYEbppze7h" + } + ], + "owner": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7", + "price": "1000", + "budget": "10000000" + }, + "expiration": "None" + } + }, + "signatures": [ + { + "scheme": "ed25519", + "signature": "o6PoW/P7BpDmgVkkOKnmVEsS5OqU8+kUxaENWwRxGcAtYvt+BbybZ3bW57oE5v5/USn7Ag7S892tPw5PL/jdBQ==", + "public_key": "i2HWsF9A7vdmrnxpB6mNXFujxewIT2B/+IltXIpxkCg=" + } + ] + }, + "auth_signature": {} + }, + "effects": { + "V1": { + "status": { + "success": true + }, + "epoch": "0", + "gas_cost_summary": { + "computation_cost": "1000000", + "computation_cost_burned": "1000000", + "storage_cost": "1960800", + "storage_rebate": "0", + "non_refundable_storage_fee": "0" + }, + "transaction_digest": "W5a5vsCEVHTj5woXy1MymQYpe4UFzwEoor8k8PASDeq", + "gas_object_index": 0, + "events_digest": null, + "dependencies": [ + "54ohH6BW2vfMLD6r63KKuZhcgrMq2ty4XU9JHD7D2HAW" + ], + "lamport_version": "2", + "changed_objects": [ + { + "object_id": "0x1f8ef3d2929482a43abc43c3693a695e247d55fd865636719bd428a606d3366c", + "input_state": { + "Data": { + "version": "1", + "digest": "4VSj48G4aoTSAYwavNA2Y79gCLQ9uHFh5yQYEbppze7h", + "owner": { + "Address": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7" + } + } + }, + "output_state": { + "ObjectWrite": { + "digest": "GhdZ7GvETWWMYR5G8XmmWwZ6Ta3GEsNBX4ajzazfAPC1", + "owner": { + "Address": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7" + } + } + }, + "id_operation": "none" + }, + { + "object_id": "0xbf840e88aa464d25b4fb3164eacbf487a9c7391838b5f684a4a3a2a69c6fbdbf", + "input_state": "Missing", + "output_state": { + "ObjectWrite": { + "digest": "EdZxTKeh9pRZqSV7mACnTgBzmnj3hz9pwgdRfL7ofV1e", + "owner": { + "Address": "0x998d2691b2ac3943bd6cd87915ea9b57bac193c5dc0757f25a7d2b4127c87bfa" + } + } + }, + "id_operation": "created" + } + ], + "unchanged_shared_objects": [], + "auxiliary_data_digest": null + } + }, + "events": null + } +} diff --git a/poi-rs/tests/fixtures/current/transaction.json b/poi-rs/tests/fixtures/current/transaction.json new file mode 100644 index 00000000..3c9caf2d --- /dev/null +++ b/poi-rs/tests/fixtures/current/transaction.json @@ -0,0 +1,263 @@ +{ + "version": 1, + "chain": "J3x3hNka6L8T5y8VwEBHgJYLm5XgLm4FCQwmDcUpNAf5", + "targets": { + "transaction": "W5a5vsCEVHTj5woXy1MymQYpe4UFzwEoor8k8PASDeq", + "objects": [], + "events": [] + }, + "checkpoint_summary": { + "data": { + "epoch": "0", + "sequence_number": "2", + "network_total_transactions": "10", + "content_digest": "6EucatZAZSiXpJBkyeozmy1kzRERXWq1ty7N1grkTuSk", + "previous_digest": "Et5CyqsQpZSbHgai9vf9qhJspF2SvF1uV75CGsnRft36", + "epoch_rolling_gas_cost_summary": { + "computation_cost": "1000000", + "computation_cost_burned": "1000000", + "storage_cost": "1960800", + "storage_rebate": "0", + "non_refundable_storage_fee": "0" + }, + "timestamp_ms": "1785157325745", + "checkpoint_commitments": [], + "end_of_epoch_data": null, + "version_specific_data": "AAA=" + }, + "auth_signature": { + "epoch": 0, + "signature": "hU5kzMpMZw1NCnTOZtpdxH8pmxpk8gHbIpza8vveKcp3oqiM4J+OpqR5f7+Fn402", + "signers_map": [ + 58, + 48, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16, + 0, + 0, + 0, + 0, + 0 + ] + } + }, + "checkpoint_contents": { + "V1": [ + { + "transaction": "GuWB2VN7iieJPXADJmdrPsEPXffkC2J36ynXKzh3Jyow", + "effects": "D9ZR6gfoHMBMQS6XtrotsPBMykcVUJwYQY9obfnUcXuz", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "84s7gmgJVA8uVtKi9HpGSJtkVfv838oirdMmEpMYhgog", + "effects": "EZYAw9QNDhFiTHaJ3W28jRMGRdvvhpy1q4g1x8BwgCKo", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "DhvRjECEkgd3PmmPpUwjFtnk9cSC2M3oDayagwvRVSYw", + "effects": "8PMXpKFk8uFGBEv2UyqGgycfSPE5wfnjtyEdt8LUEHtf", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "GuHQHaXjy8ZPipiYYYcrWX8g2bfkdDni2pEPoFJUSNwq", + "effects": "BoRtHiQf9kGur9XRHagdXq4kXN7huyibCeJ65Y2o55wY", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "W5a5vsCEVHTj5woXy1MymQYpe4UFzwEoor8k8PASDeq", + "effects": "4xbK8Ebw8yq5VMd4ztJbk1eb5AFyKvUtSoRGHRZ9jnCD", + "signatures": [ + { + "scheme": "ed25519", + "signature": "o6PoW/P7BpDmgVkkOKnmVEsS5OqU8+kUxaENWwRxGcAtYvt+BbybZ3bW57oE5v5/USn7Ag7S892tPw5PL/jdBQ==", + "public_key": "i2HWsF9A7vdmrnxpB6mNXFujxewIT2B/+IltXIpxkCg=" + } + ] + }, + { + "transaction": "7NShvzK9emiobPtBphmqZapFmww6rDPnGSJWU2U9f41E", + "effects": "AtRzYVJW8yrhQEAi8V8PutQz7rrwhR9sHz3hRXW6JfHr", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + }, + { + "transaction": "Dnzx53gMFPuTu9DSbBDiBbVxqgf9DoJ9ep3j8CBTPogw", + "effects": "3vRAtw8276TpaSbNk8TMS12HCb2sFe1EWw7wkn7hecZc", + "signatures": [ + { + "scheme": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + ] + } + ] + }, + "transaction_proof": { + "transaction": { + "data": { + "transaction": { + "V1": { + "kind": { + "Programmable": { + "inputs": [ + { + "Pure": "mY0mkbKsOUO9bNh5FeqbV7rBk8XcB1fyWn0rQSfIe/o=" + }, + { + "Pure": "AQAAAAAAAAA=" + } + ], + "commands": [ + { + "SplitCoins": { + "coin": "Gas", + "amounts": [ + { + "Input": 1 + } + ] + } + }, + { + "TransferObjects": { + "objects": [ + { + "Result": 0 + } + ], + "address": { + "Input": 0 + } + } + } + ] + } + }, + "sender": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7", + "gas_payment": { + "objects": [ + { + "object_id": "0x1f8ef3d2929482a43abc43c3693a695e247d55fd865636719bd428a606d3366c", + "version": "1", + "digest": "4VSj48G4aoTSAYwavNA2Y79gCLQ9uHFh5yQYEbppze7h" + } + ], + "owner": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7", + "price": "1000", + "budget": "10000000" + }, + "expiration": "None" + } + }, + "signatures": [ + { + "scheme": "ed25519", + "signature": "o6PoW/P7BpDmgVkkOKnmVEsS5OqU8+kUxaENWwRxGcAtYvt+BbybZ3bW57oE5v5/USn7Ag7S892tPw5PL/jdBQ==", + "public_key": "i2HWsF9A7vdmrnxpB6mNXFujxewIT2B/+IltXIpxkCg=" + } + ] + }, + "auth_signature": {} + }, + "effects": { + "V1": { + "status": { + "success": true + }, + "epoch": "0", + "gas_cost_summary": { + "computation_cost": "1000000", + "computation_cost_burned": "1000000", + "storage_cost": "1960800", + "storage_rebate": "0", + "non_refundable_storage_fee": "0" + }, + "transaction_digest": "W5a5vsCEVHTj5woXy1MymQYpe4UFzwEoor8k8PASDeq", + "gas_object_index": 0, + "events_digest": null, + "dependencies": [ + "54ohH6BW2vfMLD6r63KKuZhcgrMq2ty4XU9JHD7D2HAW" + ], + "lamport_version": "2", + "changed_objects": [ + { + "object_id": "0x1f8ef3d2929482a43abc43c3693a695e247d55fd865636719bd428a606d3366c", + "input_state": { + "Data": { + "version": "1", + "digest": "4VSj48G4aoTSAYwavNA2Y79gCLQ9uHFh5yQYEbppze7h", + "owner": { + "Address": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7" + } + } + }, + "output_state": { + "ObjectWrite": { + "digest": "GhdZ7GvETWWMYR5G8XmmWwZ6Ta3GEsNBX4ajzazfAPC1", + "owner": { + "Address": "0x4341880802a84b5cab9a3a16754c52cc1ed8656b343a0a8dda0ce775da27bbe7" + } + } + }, + "id_operation": "none" + }, + { + "object_id": "0xbf840e88aa464d25b4fb3164eacbf487a9c7391838b5f684a4a3a2a69c6fbdbf", + "input_state": "Missing", + "output_state": { + "ObjectWrite": { + "digest": "EdZxTKeh9pRZqSV7mACnTgBzmnj3hz9pwgdRfL7ofV1e", + "owner": { + "Address": "0x998d2691b2ac3943bd6cd87915ea9b57bac193c5dc0757f25a7d2b4127c87bfa" + } + } + }, + "id_operation": "created" + } + ], + "unchanged_shared_objects": [], + "auxiliary_data_digest": null + } + }, + "events": null + } +} diff --git a/poi-rs/tests/proof_construction.rs b/poi-rs/tests/proof_construction.rs new file mode 100644 index 00000000..a795de4e --- /dev/null +++ b/poi-rs/tests/proof_construction.rs @@ -0,0 +1,260 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod utils; + +use std::sync::{Arc, Mutex}; + +use iota_sdk_types::TransactionDigest; +use iota_types::{event::EventID, object::Object}; +use poi_rs::{PoiClient, ProofBuilderError, ProofVerifier, SourceError}; +use utils::{ + genesis_chain_identifier, grpc_client, object_transfer_tx, + sources::{MissingSource, RecordingSource, RejectingSource}, + staking_tx, start_test_cluster, transfer_tx, +}; + +#[tokio::test] +async fn client_uses_a_custom_source_for_proof_building() { + let transaction_digest = TransactionDigest::random(); + + let error = PoiClient::new(RejectingSource) + .proof() + .transaction(transaction_digest) + .build() + .await + .expect_err("the custom source error must be returned"); + + let ProofBuilderError::Source { source } = error else { + panic!("custom source error must be preserved"); + }; + assert!(matches!(source, SourceError::Request { .. })); +} + +#[tokio::test] +async fn proof_requires_at_least_one_request() { + let error = PoiClient::new(RejectingSource) + .proof() + .build() + .await + .expect_err("a proof without a request must be rejected"); + + assert!(matches!(error, ProofBuilderError::MissingRequest)); +} + +#[tokio::test] +async fn stacked_requests_are_deduplicated_and_reuse_transaction_evidence() { + let cluster = start_test_cluster().await; + let staking = staking_tx(&cluster).await; + let object_id = staking.gas_object.object_id; + let event_id = EventID { + tx_digest: staking.digest, + event_seq: 0, + }; + let transactions = Arc::new(Mutex::new(Vec::new())); + let source = RecordingSource::new(grpc_client(&cluster), transactions.clone()); + + let proof = PoiClient::new(source) + .proof() + .transaction(staking.digest) + .objects([object_id, object_id]) + .object(object_id) + .events([event_id, event_id]) + .event(event_id) + .build() + .await + .expect("stacked requests from one transaction must produce a proof"); + + assert_eq!( + *transactions + .lock() + .expect("recorded transactions lock must not be poisoned"), + vec![staking.digest] + ); + assert_eq!(proof.targets.transaction, Some(staking.digest)); + assert_eq!(proof.targets.objects.len(), 1); + assert_eq!(proof.targets.events.len(), 1); + ProofVerifier::new(&cluster.committee()) + .verify(&proof) + .expect("the stacked-target proof must verify offline"); +} + +#[tokio::test] +async fn transaction_not_returned_by_the_source_is_reported_as_missing() { + let transaction_digest = TransactionDigest::random(); + + let error = PoiClient::new(MissingSource) + .proof() + .transaction(transaction_digest) + .build() + .await + .expect_err("a transaction omitted by the source must be rejected"); + + let ProofBuilderError::TransactionNotFound { + transaction_digest: missing_transaction, + } = error + else { + panic!("an omitted transaction must return a transaction-not-found error"); + }; + assert_eq!(missing_transaction, transaction_digest); +} + +#[tokio::test] +async fn proof_uses_the_genesis_checkpoint_as_its_chain_identifier() { + let cluster = start_test_cluster().await; + let transfer = transfer_tx(&cluster).await; + + let proof = PoiClient::from_grpc_client(grpc_client(&cluster)) + .proof() + .transaction(transfer.digest) + .build() + .await + .expect("transaction proof must be constructed"); + + assert_eq!(proof.chain, genesis_chain_identifier(&cluster)); +} + +#[tokio::test] +async fn object_not_returned_by_the_source_is_reported_as_missing() { + let object_id = Object::immutable_for_testing().id(); + + let error = PoiClient::new(MissingSource) + .proof() + .object(object_id) + .build() + .await + .expect_err("an object omitted by the source must be rejected"); + + let ProofBuilderError::ObjectNotFound { + object_id: missing_object, + } = error + else { + panic!("an omitted object must return an object-not-found error"); + }; + assert_eq!(missing_object, object_id); +} + +#[tokio::test] +async fn object_that_does_not_match_the_requested_reference_is_rejected() { + let cluster = start_test_cluster().await; + let transfer = transfer_tx(&cluster).await; + let object_id = transfer.gas_object.object_id; + let transactions = Arc::new(Mutex::new(Vec::new())); + let source = + RecordingSource::new(grpc_client(&cluster), transactions).with_object_override(Object::immutable_for_testing()); + + let error = PoiClient::new(source) + .proof() + .transaction(transfer.digest) + .object(object_id) + .build() + .await + .expect_err("an object that does not match the effects reference must be rejected"); + + assert!(matches!( + error, + ProofBuilderError::ObjectReferenceMismatch { + object_id: returned_object_id + } if returned_object_id == object_id + )); +} + +#[tokio::test] +async fn explicit_transaction_and_event_from_different_transactions_are_rejected_without_fetching() { + let transaction_digest = TransactionDigest::new([1; 32]); + let event_id = EventID { + tx_digest: TransactionDigest::new([2; 32]), + event_seq: 0, + }; + + let error = PoiClient::new(MissingSource) + .proof() + .transaction(transaction_digest) + .event(event_id) + .build() + .await + .expect_err("requests from different transactions must be rejected"); + + assert!(matches!( + error, + ProofBuilderError::TransactionMismatch { expected, actual } + if expected == transaction_digest && actual == event_id.tx_digest + )); +} + +#[tokio::test] +async fn event_sequence_outside_the_transaction_is_rejected() { + let cluster = start_test_cluster().await; + let staking = staking_tx(&cluster).await; + let event_id = EventID { + tx_digest: staking.digest, + event_seq: u64::MAX, + }; + + let error = PoiClient::from_grpc_client(grpc_client(&cluster)) + .proof() + .event(event_id) + .build() + .await + .expect_err("an event sequence outside the transaction must be rejected"); + + let ProofBuilderError::EventNotFound { + event_id: missing_event, + } = error + else { + panic!("missing event must return an event-not-found error"); + }; + assert_eq!(missing_event, event_id); +} + +#[tokio::test] +async fn object_outside_the_event_transaction_is_rejected() { + let cluster = start_test_cluster().await; + let transfer = object_transfer_tx(&cluster).await; + let staking = staking_tx(&cluster).await; + let object_id = transfer.objects[1].object_id; + let event_id = EventID { + tx_digest: staking.digest, + event_seq: 0, + }; + + let error = PoiClient::from_grpc_client(grpc_client(&cluster)) + .proof() + .object(object_id) + .event(event_id) + .build() + .await + .expect_err("an object outside the event transaction must be rejected"); + + let ProofBuilderError::ObjectNotChangedByTransaction { + object_id: returned_object_id, + transaction_digest, + } = error + else { + panic!("unrelated object must return a proof-builder error"); + }; + assert_eq!(returned_object_id, object_id); + assert_eq!(transaction_digest, staking.digest); +} + +#[tokio::test] +async fn object_requests_from_different_transactions_are_rejected() { + let cluster = start_test_cluster().await; + let first = object_transfer_tx(&cluster).await; + let second = object_transfer_tx(&cluster).await; + let first_object_id = first.objects[1].object_id; + let second_object_id = second.objects[1].object_id; + + let error = PoiClient::from_grpc_client(grpc_client(&cluster)) + .proof() + .objects([first_object_id, second_object_id]) + .build() + .await + .expect_err("objects from different transactions must be rejected"); + + let ProofBuilderError::TransactionMismatch { expected, actual } = error else { + panic!("mixed transactions must return a proof-builder error"); + }; + assert_eq!(expected, first.digest); + assert_eq!(actual, second.digest); +} diff --git a/poi-rs/tests/proof_serialization.rs b/poi-rs/tests/proof_serialization.rs new file mode 100644 index 00000000..b7ea9e39 --- /dev/null +++ b/poi-rs/tests/proof_serialization.rs @@ -0,0 +1,71 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_types::committee::Committee; +use poi_rs::{Proof, ProofVerifier, ProofVersion, VerifyErrorKind}; + +const COMMITTEE: &str = include_str!("fixtures/current/committee.json"); +const TRANSACTION: &str = include_str!("fixtures/current/transaction.json"); +const OBJECT: &str = include_str!("fixtures/current/object.json"); +const EVENT: &str = include_str!("fixtures/current/event.json"); + +fn assert_fixture_round_trips_and_verifies(fixture: &str) -> Proof { + let committee: Committee = serde_json::from_str(COMMITTEE).expect("committee fixture must deserialize"); + let proof = Proof::from_json_slice(fixture.as_bytes()).expect("proof fixture must deserialize"); + + ProofVerifier::new(&committee) + .verify(&proof) + .expect("proof fixture must verify offline"); + assert_eq!( + serde_json::from_slice::(&proof.to_json_vec().expect("proof fixture must serialize")) + .expect("serialized proof must be valid JSON"), + serde_json::from_str::(fixture).expect("proof fixture must be valid JSON") + ); + assert_eq!(proof.version(), ProofVersion::CURRENT); + + proof +} + +#[test] +fn transaction_fixture_round_trips_and_verifies() { + let proof = assert_fixture_round_trips_and_verifies(TRANSACTION); + + assert!(proof.targets().transaction.is_some()); + assert!(proof.targets().objects.is_empty()); + assert!(proof.targets().events.is_empty()); +} + +#[test] +fn object_fixture_round_trips_and_verifies() { + let proof = assert_fixture_round_trips_and_verifies(OBJECT); + + assert!(proof.targets().transaction.is_none()); + assert_eq!(proof.targets().objects.len(), 1); + assert!(proof.targets().events.is_empty()); +} + +#[test] +fn event_fixture_round_trips_and_verifies() { + let proof = assert_fixture_round_trips_and_verifies(EVENT); + + assert!(proof.targets().transaction.is_none()); + assert!(proof.targets().objects.is_empty()); + assert_eq!(proof.targets().events.len(), 1); +} + +#[test] +fn unsupported_fixture_version_returns_the_version_number() { + let committee: Committee = serde_json::from_str(COMMITTEE).expect("committee fixture must deserialize"); + let mut fixture: serde_json::Value = serde_json::from_str(TRANSACTION).expect("proof fixture must be valid JSON"); + fixture["version"] = serde_json::json!(2); + let proof: Proof = serde_json::from_value(fixture).expect("unsupported proof version must deserialize"); + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("an unsupported proof version must be rejected"); + let VerifyErrorKind::Version { source } = error.kind else { + panic!("unsupported proof version must return a version error"); + }; + + assert_eq!(source.version, 2); +} diff --git a/poi-rs/tests/proof_verification.rs b/poi-rs/tests/proof_verification.rs new file mode 100644 index 00000000..73f32196 --- /dev/null +++ b/poi-rs/tests/proof_verification.rs @@ -0,0 +1,121 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod utils; + +use iota_sdk_types::CheckpointContents; +use iota_types::{ + effects::TransactionEvents, event::EventID, messages_checkpoint::CheckpointContentsExt, object::Object, +}; +use poi_rs::{ProofTargets, ProofVerifier, VerifyErrorKind}; +use utils::proofs::{event, execution_data, proof_with_events, proof_with_targets, valid_transaction_proof}; + +#[test] +fn valid_transaction_proof_is_accepted() { + let (committee, proof) = valid_transaction_proof(); + + ProofVerifier::new(&committee) + .verify(&proof) + .expect("a valid transaction proof must verify"); +} + +#[test] +fn transaction_digest_must_match_the_effects() { + let (committee, mut proof) = valid_transaction_proof(); + proof.transaction_proof.effects = execution_data().effects; + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("mismatched transaction effects must be rejected"); + + assert!(matches!(error.kind, VerifyErrorKind::TransactionDigestMismatch)); +} + +#[test] +fn events_digest_must_match_the_effects() { + let (committee, mut proof) = valid_transaction_proof(); + proof.transaction_proof.events = Some(TransactionEvents(Vec::new())); + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("mismatched transaction events must be rejected"); + + assert!(matches!(error.kind, VerifyErrorKind::EventsDigestMismatch)); +} + +#[test] +fn checkpoint_contents_must_match_the_signed_summary() { + let (committee, mut proof) = valid_transaction_proof(); + let alternate = execution_data(); + proof.checkpoint_contents = CheckpointContents::new_with_digests_only_for_tests([alternate.digests()]); + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("checkpoint contents outside the signed summary must be rejected"); + + assert!(matches!(error.kind, VerifyErrorKind::CheckpointSummary { .. })); +} + +#[test] +fn transaction_must_be_present_in_the_checkpoint() { + let (committee, mut proof) = valid_transaction_proof(); + let alternate = execution_data(); + proof.transaction_proof.transaction = alternate.transaction; + proof.transaction_proof.effects = alternate.effects; + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("a transaction outside the checkpoint must be rejected"); + + assert!(matches!(error.kind, VerifyErrorKind::TransactionNotInCheckpoint)); +} + +#[test] +fn object_target_must_appear_in_the_transaction_effects() { + let object = Object::immutable_for_testing(); + let targets = ProofTargets::new().add_object(object); + let (committee, proof) = proof_with_targets(targets); + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("an object absent from the transaction effects must be rejected"); + + assert!(matches!(error.kind, VerifyErrorKind::ObjectNotFound)); +} + +#[test] +fn event_target_must_belong_to_the_proven_transaction() { + let target = event(vec![1, 2, 3]); + let (committee, _, mut proof) = proof_with_events(TransactionEvents(vec![target])); + let event_id = EventID { + tx_digest: iota_sdk_types::TransactionDigest::new([0xff; 32]), + event_seq: 0, + }; + proof.targets = ProofTargets::new().add_event(event_id); + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("an event from another transaction must be rejected"); + + assert!(matches!(error.kind, VerifyErrorKind::EventTransactionMismatch)); +} + +#[test] +fn event_sequence_must_exist_in_the_transaction() { + let target = event(vec![1, 2, 3]); + let (committee, transaction_digest, mut proof) = proof_with_events(TransactionEvents(vec![target])); + let event_id = EventID { + tx_digest: transaction_digest, + event_seq: 1, + }; + proof.targets = ProofTargets::new().add_event(event_id); + + let error = ProofVerifier::new(&committee) + .verify(&proof) + .expect_err("an event sequence outside the transaction must be rejected"); + + assert!(matches!( + error.kind, + VerifyErrorKind::EventSequenceOutOfBounds { sequence: 1 } + )); +} diff --git a/poi-rs/tests/proof_workflows.rs b/poi-rs/tests/proof_workflows.rs new file mode 100644 index 00000000..3c95e9fe --- /dev/null +++ b/poi-rs/tests/proof_workflows.rs @@ -0,0 +1,143 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod utils; + +use std::fs::File; + +use iota_config::IOTA_GENESIS_FILENAME; +use iota_types::event::EventID; +use poi_rs::{CommitteeResolution, PoiClient}; +use utils::{advance_to_epoch, grpc_client, object_transfer_tx, staking_tx, start_test_cluster, transfer_tx}; + +#[tokio::test] +async fn client_builds_and_verifies_a_transaction_proof_from_genesis() { + let cluster = start_test_cluster().await; + let genesis = File::open(cluster.swarm.dir().join(IOTA_GENESIS_FILENAME)) + .expect("test cluster genesis blob must be available"); + advance_to_epoch(&cluster, 1).await; + let transfer = transfer_tx(&cluster).await; + let client = PoiClient::from_grpc_client(grpc_client(&cluster)); + let proof = client + .proof() + .transaction(transfer.digest) + .build() + .await + .expect("transaction proof must be constructed"); + + assert_eq!(proof.targets.transaction, Some(transfer.digest)); + assert!(proof.targets.objects.is_empty()); + assert!(proof.targets.events.is_empty()); + assert!(proof.transaction_proof.events.is_none()); + + let resolution = CommitteeResolution::from_genesis(genesis).expect("test cluster genesis blob must load"); + client + .verifier(resolution) + .verify(&proof) + .await + .expect("anchored verification must authenticate the committee and verify the proof"); +} + +#[tokio::test] +async fn client_builds_and_verifies_an_object_proof_with_a_trusted_node() { + let cluster = start_test_cluster().await; + let transfer = transfer_tx(&cluster).await; + let client = PoiClient::from_grpc_client(grpc_client(&cluster)); + + let proof = client + .proof() + .object(transfer.gas_object.object_id) + .build() + .await + .expect("object proof must be constructed"); + + assert!(proof.targets.transaction.is_none()); + assert_eq!(proof.targets.objects[0].as_inner().object_ref(), transfer.gas_object); + assert!(proof.targets.events.is_empty()); + assert!(proof.transaction_proof.events.is_none()); + client + .verifier(CommitteeResolution::TrustedNode) + .verify(&proof) + .await + .expect("object proof must verify"); +} + +#[tokio::test] +async fn client_builds_and_verifies_an_event_proof_with_a_trusted_node() { + let cluster = start_test_cluster().await; + let staking = staking_tx(&cluster).await; + let client = PoiClient::from_grpc_client(grpc_client(&cluster)); + let event_id = EventID { + tx_digest: staking.digest, + event_seq: 0, + }; + + let proof = client + .proof() + .event(event_id) + .build() + .await + .expect("event proof must be constructed"); + + assert!(proof.targets.transaction.is_none()); + assert!(proof.targets.objects.is_empty()); + assert_eq!(proof.targets.events, vec![event_id]); + assert!(proof.transaction_proof.events.is_some()); + + client + .verifier(CommitteeResolution::TrustedNode) + .verify(&proof) + .await + .expect("event proof must verify"); +} + +#[tokio::test] +async fn client_builds_one_verified_proof_for_multiple_objects() { + let cluster = start_test_cluster().await; + let transfer = object_transfer_tx(&cluster).await; + let client = PoiClient::from_grpc_client(grpc_client(&cluster)); + + let proof = client + .proof() + .objects(transfer.objects.map(|object_ref| object_ref.object_id)) + .build() + .await + .expect("stacked object proof must be constructed"); + + assert_eq!(proof.transaction_proof.transaction.digest(), &transfer.digest); + assert_eq!(proof.targets.objects.len(), 2); + client + .verifier(CommitteeResolution::TrustedNode) + .verify(&proof) + .await + .expect("stacked object proof must verify"); +} + +#[tokio::test] +async fn client_builds_one_verified_proof_for_object_and_event_targets() { + let cluster = start_test_cluster().await; + let staking = staking_tx(&cluster).await; + let client = PoiClient::from_grpc_client(grpc_client(&cluster)); + let event_id = EventID { + tx_digest: staking.digest, + event_seq: 0, + }; + + let proof = client + .proof() + .object(staking.gas_object.object_id) + .event(event_id) + .build() + .await + .expect("mixed target proof must be constructed"); + + assert_eq!(proof.transaction_proof.transaction.digest(), &staking.digest); + assert_eq!(proof.targets.objects[0].as_inner().object_ref(), staking.gas_object); + assert_eq!(proof.targets.objects.len(), 1); + assert_eq!(proof.targets.events.len(), 1); + client + .verifier(CommitteeResolution::TrustedNode) + .verify(&proof) + .await + .expect("mixed target proof must verify"); +} diff --git a/poi-rs/tests/utils/mod.rs b/poi-rs/tests/utils/mod.rs new file mode 100644 index 00000000..e7f53d07 --- /dev/null +++ b/poi-rs/tests/utils/mod.rs @@ -0,0 +1,177 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// Each integration-test file is compiled as a separate crate, so helpers used +// by sibling test crates otherwise appear unused. +#![allow(dead_code)] + +use std::fs::File; + +use iota_config::{IOTA_GENESIS_FILENAME, genesis::Genesis}; +use iota_grpc_client::Client as GrpcClient; +use iota_sdk_types::{ObjectReference, TransactionDigest}; +use iota_types::iota_system_state::{IotaSystemStateTrait, get_iota_system_state}; +use iota_types::{committee::Committee, digests::ChainIdentifier}; +use test_cluster::{TestCluster, TestClusterBuilder}; + +pub mod proofs; +pub mod sources; + +pub fn committee_at(epoch: u64) -> Committee { + let (committee, _) = Committee::new_simple_test_committee(); + Committee::new(epoch, committee.voting_rights.iter().cloned().collect()) +} + +pub struct CheckpointedTransfer { + pub digest: TransactionDigest, + pub gas_object: ObjectReference, +} + +pub struct CheckpointedStaking { + pub digest: TransactionDigest, + pub gas_object: ObjectReference, +} + +pub struct CheckpointedObjectTransfer { + pub digest: TransactionDigest, + pub objects: [ObjectReference; 2], +} + +pub async fn start_test_cluster() -> TestCluster { + TestClusterBuilder::new() + .with_num_validators(1) + .with_fullnode_enable_grpc_api(true) + .disable_fullnode_pruning() + .build() + .await +} + +pub fn grpc_client(cluster: &TestCluster) -> GrpcClient { + GrpcClient::new(cluster.grpc_url()).expect("test cluster gRPC client must connect") +} + +pub async fn transfer_tx(cluster: &TestCluster) -> CheckpointedTransfer { + let builder = cluster.test_transaction_builder().await; + let gas_object = builder.gas_object(); + let transaction = builder.transfer_iota(Some(1), cluster.get_address_1()).build(); + let response = cluster.sign_and_execute_transaction(&transaction).await; + let checkpoint = response.checkpoint.expect("transfer transaction must be checkpointed"); + cluster.wait_for_checkpoint(checkpoint, None).await; + let gas_object = cluster + .wallet + .get_object_ref(gas_object.object_id) + .await + .expect("mutated gas object must be available"); + + CheckpointedTransfer { + digest: response.digest, + gas_object, + } +} + +pub async fn object_transfer_tx(cluster: &TestCluster) -> CheckpointedObjectTransfer { + let (sender, mut coins) = cluster + .wallet + .get_one_account() + .await + .expect("test cluster must contain a funded account"); + let gas = coins.pop().expect("funded account must have a gas coin"); + let object = coins.pop().expect("funded account must have an object to transfer"); + let gas_object_id = gas.object_id; + let transferred_object_id = object.object_id; + let transaction = cluster + .test_transaction_builder_with_gas_object(sender, gas) + .await + .transfer(object, cluster.get_address_1()) + .build(); + let response = cluster.sign_and_execute_transaction(&transaction).await; + let checkpoint = response.checkpoint.expect("object transfer must be checkpointed"); + cluster.wait_for_checkpoint(checkpoint, None).await; + let gas_object = cluster + .wallet + .get_object_ref(gas_object_id) + .await + .expect("mutated gas object must be available"); + let transferred_object = cluster + .wallet + .get_object_ref(transferred_object_id) + .await + .expect("transferred object must be available"); + + CheckpointedObjectTransfer { + digest: response.digest, + objects: [gas_object, transferred_object], + } +} + +pub async fn staking_tx(cluster: &TestCluster) -> CheckpointedStaking { + let (sender, mut coins) = cluster + .wallet + .get_one_account() + .await + .expect("test cluster must contain a funded account"); + let gas = coins.pop().expect("funded account must have a gas coin"); + let stake = coins.pop().expect("funded account must have a stake coin"); + let gas_object_id = gas.object_id; + let validator = cluster + .swarm + .active_validators() + .next() + .expect("test cluster must have a validator") + .config() + .iota_address(); + let transaction = cluster + .test_transaction_builder_with_gas_object(sender, gas) + .await + .call_staking(stake, validator) + .build(); + let response = cluster.sign_and_execute_transaction(&transaction).await; + let checkpoint = response.checkpoint.expect("staking transaction must be checkpointed"); + cluster.wait_for_checkpoint(checkpoint, None).await; + let gas_object = cluster + .wallet + .get_object_ref(gas_object_id) + .await + .expect("mutated gas object must be available"); + + CheckpointedStaking { + digest: response.digest, + gas_object, + } +} + +pub fn genesis_committee(cluster: &TestCluster) -> Committee { + let genesis_path = cluster.swarm.dir().join(IOTA_GENESIS_FILENAME); + let genesis = File::open(genesis_path).expect("test cluster genesis blob must be available"); + + committee_from_genesis(genesis).expect("test cluster genesis committee must be extractable") +} + +pub fn committee_from_genesis(genesis: impl std::io::Read) -> Result { + let genesis: iota_config::genesis::Genesis = bcs::from_reader(genesis).map_err(|_| ())?; + let system_state = get_iota_system_state(&genesis.objects()).map_err(|_| ())?; + + Ok(system_state.get_current_epoch_committee().committee().clone()) +} + +pub fn genesis_chain_identifier(cluster: &TestCluster) -> ChainIdentifier { + let genesis_path = cluster.swarm.dir().join(IOTA_GENESIS_FILENAME); + let genesis = Genesis::load(genesis_path).expect("test cluster genesis blob must load"); + + ChainIdentifier::from(*genesis.checkpoint().digest()) +} + +pub async fn advance_to_epoch(cluster: &TestCluster, target_epoch: u64) -> Vec { + let mut committees = vec![cluster.committee().as_ref().clone()]; + + for epoch in 1..=target_epoch { + cluster.force_new_epoch().await; + let committee = cluster.committee().as_ref().clone(); + assert_eq!(committee.epoch, epoch); + committees.push(committee); + } + + let _ = transfer_tx(cluster).await; + + committees +} diff --git a/poi-rs/tests/utils/proofs.rs b/poi-rs/tests/utils/proofs.rs new file mode 100644 index 00000000..03064717 --- /dev/null +++ b/poi-rs/tests/utils/proofs.rs @@ -0,0 +1,104 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk_types::{CheckpointContents, CheckpointSummary, Event, TransactionDigest, gas::GasCostSummary}; +use iota_types::{ + base_types::ExecutionData, + committee::Committee, + digests::ChainIdentifier, + effects::{TestEffectsBuilder, TransactionEvents}, + messages_checkpoint::{CertifiedCheckpointSummary, CheckpointContentsExt, FullCheckpointContents}, + sdk_types::{Address, Identifier, ObjectId, StructTag}, +}; +use poi_rs::{Proof, ProofTargets, TransactionProof}; + +pub fn execution_data() -> ExecutionData { + FullCheckpointContents::random_for_testing() + .into_iter() + .next() + .expect("test checkpoint contents must include a transaction") +} + +fn signed_checkpoint(contents: &CheckpointContents) -> (Committee, CertifiedCheckpointSummary) { + let summary = CheckpointSummary { + epoch: 0, + sequence_number: 0, + network_total_transactions: contents.len() as u64, + content_digest: contents.digest(), + previous_digest: None, + epoch_rolling_gas_cost_summary: GasCostSummary::default(), + timestamp_ms: 0, + checkpoint_commitments: Vec::new(), + end_of_epoch_data: None, + version_specific_data: Vec::new(), + }; + let (committee, keypairs) = Committee::new_simple_test_committee(); + let summary = CertifiedCheckpointSummary::new_from_keypairs_for_testing(summary, &keypairs, &committee); + + (committee, summary) +} + +pub fn valid_transaction_proof() -> (Committee, Proof) { + let execution = execution_data(); + let transaction_digest = *execution.transaction.digest(); + proof_from_execution(ProofTargets::new().set_transaction(transaction_digest), execution, None) +} + +pub fn proof_with_targets(targets: ProofTargets) -> (Committee, Proof) { + let execution = execution_data(); + proof_from_execution(targets, execution, None) +} + +fn proof_from_execution( + targets: ProofTargets, + execution: ExecutionData, + events: Option, +) -> (Committee, Proof) { + let contents = CheckpointContents::new_with_digests_only_for_tests([execution.digests()]); + let (committee, summary) = signed_checkpoint(&contents); + let chain = ChainIdentifier::from(*summary.digest()); + let proof = Proof::new( + chain, + targets, + summary, + contents, + TransactionProof::new(execution.transaction, execution.effects, events), + ); + + (committee, proof) +} + +pub fn proof_with_events(events: TransactionEvents) -> (Committee, TransactionDigest, Proof) { + let mut execution = execution_data(); + let transaction_digest = *execution.transaction.digest(); + execution.effects = TestEffectsBuilder::new(execution.transaction.data()) + .with_events_digest(events.digest()) + .build(); + let contents = CheckpointContents::new_with_digests_only_for_tests([execution.digests()]); + let (committee, summary) = signed_checkpoint(&contents); + let chain = ChainIdentifier::from(*summary.digest()); + let proof = Proof::new( + chain, + ProofTargets::new(), + summary, + contents, + TransactionProof::new(execution.transaction, execution.effects, Some(events)), + ); + + (committee, transaction_digest, proof) +} + +pub fn event(contents: Vec) -> Event { + Event { + package_id: ObjectId::SYSTEM, + module: Identifier::IOTA_SYSTEM_MODULE, + sender: Address::SYSTEM, + type_: StructTag::new( + Address::SYSTEM, + Identifier::IOTA_SYSTEM_MODULE, + Identifier::SYSTEM_EPOCH_INFO_EVENT, + Vec::new(), + ), + contents, + } +} diff --git a/poi-rs/tests/utils/sources.rs b/poi-rs/tests/utils/sources.rs new file mode 100644 index 00000000..d3e6b63f --- /dev/null +++ b/poi-rs/tests/utils/sources.rs @@ -0,0 +1,154 @@ +// Copyright 2020-2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use iota_grpc_client::Client as GrpcClient; +use iota_sdk_types::{ObjectId, TransactionDigest, Version}; +use iota_types::{ + committee::{Committee, EpochId}, + digests::ChainIdentifier, + messages_checkpoint::CertifiedCheckpointSummary, + object::Object, +}; +use poi_rs::{Source, SourceCheckpoint, SourceError, SourceTransaction}; + +#[derive(Clone)] +pub struct RejectingSource; + +#[async_trait] +impl Source for RejectingSource { + async fn chain_identifier(&self) -> Result { + unreachable!("rejected transactions do not resolve a chain identifier") + } + + async fn transaction( + &self, + _transaction_digest: TransactionDigest, + ) -> Result, SourceError> { + Err(SourceError::request(std::io::Error::other("transaction rejected"))) + } + + async fn object(&self, _object_id: ObjectId, _version: Option) -> Result, SourceError> { + Ok(None) + } + + async fn checkpoint(&self, _sequence_number: u64) -> Result { + unreachable!("rejected transactions do not resolve a checkpoint") + } + + async fn committee(&self, _epoch: EpochId) -> Result { + unreachable!("proof-only test source does not resolve committees") + } + + async fn current_epoch(&self) -> Result, SourceError> { + unreachable!("proof-only test source does not resolve the current epoch") + } + + async fn epoch_close_summary(&self, _epoch: EpochId) -> Result, SourceError> { + unreachable!("proof-only test source does not resolve epoch-close summaries") + } +} + +#[derive(Clone)] +pub struct RecordingSource { + source: GrpcClient, + transactions: Arc>>, + object_override: Option, +} + +impl RecordingSource { + pub fn new(source: GrpcClient, transactions: Arc>>) -> Self { + Self { + source, + transactions, + object_override: None, + } + } + + pub fn with_object_override(mut self, object: Object) -> Self { + self.object_override = Some(object); + self + } +} + +#[async_trait] +impl Source for RecordingSource { + async fn chain_identifier(&self) -> Result { + self.source.chain_identifier().await + } + + async fn transaction( + &self, + transaction_digest: TransactionDigest, + ) -> Result, SourceError> { + self.transactions + .lock() + .expect("recorded transactions lock must not be poisoned") + .push(transaction_digest); + + self.source.transaction(transaction_digest).await + } + + async fn object(&self, object_id: ObjectId, version: Option) -> Result, SourceError> { + if let Some(object) = &self.object_override { + return Ok(Some(object.clone())); + } + + self.source.object(object_id, version).await + } + + async fn checkpoint(&self, sequence_number: u64) -> Result { + self.source.checkpoint(sequence_number).await + } + + async fn committee(&self, epoch: EpochId) -> Result { + self.source.committee(epoch).await + } + + async fn current_epoch(&self) -> Result, SourceError> { + self.source.current_epoch().await + } + + async fn epoch_close_summary(&self, epoch: EpochId) -> Result, SourceError> { + self.source.epoch_close_summary(epoch).await + } +} + +#[derive(Clone)] +pub struct MissingSource; + +#[async_trait] +impl Source for MissingSource { + async fn chain_identifier(&self) -> Result { + unreachable!("missing targets do not resolve a chain identifier") + } + + async fn transaction( + &self, + _transaction_digest: TransactionDigest, + ) -> Result, SourceError> { + Ok(None) + } + + async fn object(&self, _object_id: ObjectId, _version: Option) -> Result, SourceError> { + Ok(None) + } + + async fn checkpoint(&self, _sequence_number: u64) -> Result { + unreachable!("missing targets do not resolve a checkpoint") + } + + async fn committee(&self, _epoch: EpochId) -> Result { + unreachable!("proof-only test source does not resolve committees") + } + + async fn current_epoch(&self) -> Result, SourceError> { + unreachable!("proof-only test source does not resolve the current epoch") + } + + async fn epoch_close_summary(&self, _epoch: EpochId) -> Result, SourceError> { + unreachable!("proof-only test source does not resolve epoch-close summaries") + } +}